Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Access Programming Week 7 7. Programming the DAO (continued) Recordsets (summary so far) We have seen that Recordset objects provide a way to work with the data in a database. Recordsets are not persistent; they are destroyed when you close the application. To summarise so far: Recordsets can be based on: Tables; queries; SQL statements; other recordsets Recordsets can be of the following types: Table-type; Dynaset; Snapshot; Forward-only Recordsets are navigated using: logical navigation the Find.. methods (dynaset, snapshot, forward-only) the Seek methods (table-type only) physical navigation the Move method, the Move.. methods, Do Loops You can create bookmark variables for two main reasons: - to save a place in the recordset - to move to a record in the recordset. To remind you, the basic technique is as follows: strMark = rst.Bookmark Then supposing rst.MoveNext To return to the saved place rst.Bookmark = strMark Remember, bookmarks are declared as string variables, not object variables Use multiple bookmark variables if you need to keep track of a complex set of movements AbsolutePosition and PercentPosition properties The AbsolutePosition and PercentPosition properties also help you keep track of where you are in a recordset (dynasets and snapshots only). The AbsolutePosition property can be used to return the record number, if you add one to it (reason being the system is zero-based i.e., the first record is numbered zero, the second record is numbered one, and so on). Start a new standard module, save it as Week7 and type the following: Listing 7.1 Public Sub Navigate() Dim rst As Recordset Set rst = CurrentDb.OpenRecordset("qryOrderStatus", dbOpenSnapshot) MsgBox "The current record is " & rst.AbsolutePosition + 1 & _ " which is " & rst.PercentPosition & "%" rst.MoveLast rst.MoveFirst rst.Move 5 MsgBox "The current record is now " & rst.AbsolutePosition + 1 & _ " which is " & rst.PercentPosition & "%" MsgBox "The current Sales Order is " & rst!OrderNo MsgBox "The number of records is " & rst.RecordCount rst.Close End Sub Remember, in a dynaset or snapshot, the recordset consists only of records accessed, hence the use of the MoveLast and MoveFirst methods. 1 Access Programming Week 7 Physical Navigation of Recordsets You can move from one record to another according to physical location in two ways: you can use the Move.. methods to duplicate the effect of using the navigation buttons, or you can save your place in a recordset by setting a bookmark and then returning later to the same record BOF, EOF properties You use the BOF (Beginning of File) and EOF (End of File) properties of the recordset object to determine whether you’ve gone beyond the limits of the recordset. Both properties have the value False as long as there is a current record. If you move after the last record the EOF property is set to True; if you move before the first record the BOF property is set to True. If the recordset has no records at all, both the BOF and EOF properties are True.You can test the following: Set rst = CurrentDb.OpenRecordset("Customers", dbOpenDynaset) ?rst.CustomerName The first customer name should be displayed, Boots. rst.MoveLast The last record is now the current record. Execute ?rst.CustomerName Should be Tooting Cycles. Execute rst.MoveNext You have now moved beyond the last record of the recordset ?rst.CustomerName An error message should indicate there is no current record ?rst.EOF should return the value True rst.MoveFirst ?rst.EOF should show False ?rst.BOF should show False rst.MovePrevious ?rst.BOF should show True Looping through recordsets It is a standard procedure for working with a set of records to create a Recordset object and use the MoveNext method to loop through the records one by one. To determine when you are finished, you test the value of the EOF property at the beginning of each pass through the loop. As long as EOF is False, you make another pass through the loop, but as soon as EOF is True you have moved beyond the last record of the recordset and the loop is finished. The use of Do Until and Loop keywords is shown. Do Until rst.EOF statement(s) rst.MoveNext Loop The following listings all loop through a recordset, and display a variety of syntax that can be used. The first is a basic procedure, the recordset created is table type by default, and exclamation point syntax is used to refer to the field. Listing 7.2 Public Sub TableRecordset() Dim db as Database, rst as Recordset Set db = CurrentDb Set rst = db.OpenRecordset("Customers") Do Until rst.EOF Debug.Print rst!CustomerName rst.MoveNext Loop End Sub 2 Access Programming Week 7 In the next procedure the fields are referred to by position. Listing 7.3 Public Sub TableRecordset2() Dim rst As Recordset Set rst = CurrentDb.OpenRecordset("Customers") Do Until rst.EOF Debug.Print rst(0), rst(1) rst.MoveNext Loop End Sub Next we see how to assign an SQL statement to a string variable, and open the recordset using the variable name. SQL techniques may be faster than Find or Seek methods. Note the first field is referred to using string syntax, the others using exclamation point syntax. Listing 7.4 Public Sub SQLRecordset() Dim db As Database Dim rst As Recordset Dim strSQL As String strSQL = "SELECT * FROM Customers WHERE City = 'Brighton' " Set db = CurrentDb Set rst = db.OpenRecordset(strSQL) Do Until rst.EOF Debug.Print rst("CustomerID"), rst!CustomerName, rst!Address1, rst!City rst.MoveNext Loop End Sub The SQL statement must be converted into a string that Jet will accept. You must identify the inner string within the outer string with single quotes (or with pairs of double quotes). The next listing opens a recordset in another Access database. Listing 7.5 Public Sub RecordsetOtherDb() Dim db As Database Dim rst As Recordset Set db = DBEngine(0).OpenDatabase("d:\Databases\Accounts.mdb") Set rst = db.OpenRecordset("Invoices", dbOpenTable) Do Until rst.EOF Debug.Print rst(0), rst(1), rst(2), rst(3), rst(4), rst(5) rst.MoveNext Loop End Sub You can open a recordset on a TableDef, QueryDef or another Recordset. Listing 7.6 Public Sub TableDefRecordset() Dim rst As Recordset Set rst = CurrentDb.TableDefs("Customers").OpenRecordset Do Until rst.EOF Debug.Print rst!CustomerID rst.MoveNext Loop End Sub 3 Access Programming Week 7 Programming Exercise Problem Statement The problem is to find out when our sales orders reached a certain target, say £1000. Bear in mind that it’s most unlikely that this was a discrete event; our target will have been exceeded during the course of adding an order to the system. For example, our grand total may have been £947.35 after order no. 0022, and may be £1072.88 after order no. 0023. With a difficult problem it’s sensible to divide it into subtasks. Use your judgement to select which task to work on, write and test your solution. Then proceed to the next subtask, write and then test that. Then you will need to test the tasks together. Rather than present the solution in one go and explain it, the following exercises step through some of the thought processes involved in writing the solution. First step is to identify or create a data source that will give us the information necessary to achieve our result. That’s easy enough; we will create a query and get it over and done with. Check the query result, and make a note of the information, there should be one record for each order. Save the query as qryOrderTotalsByDate, and close it. Next we need to create a recordset. As we are using a query it could be a dynaset, or it could be a snapshot, or it could be a forward-only recordset. There’s nothing in the problem statement that says we have to change the data (you can’t edit a Totals query anyway). Perhaps at this stage we don’t know how we are going to navigate the recordset, so we will leave the decision about forward-only till later. Type the following program into the module Week7. The first logical step is to create the recordset. Listing 7.7 a Public Sub SalesTarget() Dim rs As Recordset Set rs = CurrentDb.QueryDefs("qryOrderTotalsByDate").OpenRecordset End Sub To test the recordset you could navigate it from beginning to end using a Do Until loop and print the result to the Immediate window. The lines for you to add are marked for your convenience. Listing 7.7 b Public Sub SalesTarget() Dim rs As Recordset Set rs = CurrentDb.QueryDefs("qryOrderTotalsByDate").OpenRecordset | | Do Until rs.EOF Debug.Print rs(0), rs(1), rs(2) rs.MoveNext Loop End Sub NB: from now save the module every now and then. 4 Access Programming Week 7 The next logical step is to put in a mechanism in the loop to recognise when our target is achieved, and stop looping. You have to go back to the problem statement and think what information we need to track. We need to specify our target, and keep track of the cumulative order total. That implies two variables, marked with *. We can assign the variables while we are at it. Listing 7.7 c * * Public Sub SalesTarget() Dim rs As Recordset Dim SalesTotal As Single Dim Target As Single * * Target = 500 SalesTotal = 0 ‘we use a small-ish number so it is easier to check Set rs = CurrentDb.QueryDefs("qryOrderTotalsByDate").OpenRecordset | | | Do Until rs.EOF Do While SalesTotal <= Target SalesTotal = SalesTotal + rs(1) Debug.Print rs(0), rs(1), rs(2), SalesTotal rs.MoveNext Loop Note there are now two loops ! ‘ Debug.Print rs(0), rs(1), rs(2) ‘comment this line out or delete it rs.MoveNext Loop ‘this refers to the outer loop End Sub Our tentative solution is to put a Do While loop within the loop that navigates the whole recordset. The conditional clause for the inner loop is that SalesTotal is less than Target. The next line adds the value of rs(1), which is the amount of the order, to the variable SalesTotal. Next we put our debug.print statement, so we can check if the previous two lines are working properly. The MoveNext method moves to the next record, then the Loop statement checks the condition – if SalesTotal is still less than Target, the program runs through the loop again. If SalesTotal exceeds Target, the flow of control passes to the outer loop which loops through to rs.EOF Clear the Immediate window before running the program. Assuming you haven’t previously tampered with the data, the result should be as illustrated on the right What does it mean? It means that the fourth row is the order that took us past the £500 threshold, and the inner loop baled out as planned. 5025 5026 5027 5028 79 29.2 280 335 13/05/2001 13/05/2001 13/05/2001 04/06/2001 79 108.2 388.2 723.2 The next logical step is to ‘catch’ the date so that we can satisfy the problem statement. That implies another variable, which we can call TargetDate. The logic of the problem dictates we should assign to the variable TargetDate the date when the cumulative value of SalesTotal exceeds the Target threshold. Add the marked lines to your code, and comment out the debug.print statement from the previous stage. Listing 7.7 d * Public Sub SalesTarget() Dim rs As Recordset Dim SalesTotal As Single Dim Target As Single Dim TargetDate As Date Target = 500 SalesTotal = 0 Set rs = CurrentDb.QueryDefs("qryOrderTotalsByDate").OpenRecordset Do Until rs.EOF Do While SalesTotal <= Target SalesTotal = SalesTotal + rs(1) If SalesTotal >= Target Then 5 Access Programming Week 7 | | TargetDate = rs(2) End If Debug.Print TargetDate rs.MoveNext Loop rs.MoveNext Loop End Sub Run the code; it’s getting there but it’s not quite right. You should see lines of zeros: 00:00:00 00:00:00 00:00:00 04/06/2001 The reason is that the print statement is outside of the If ..End If block, and will print something for every pass of the loop, hence the zeros. This is a very typical hitch you run into when you learn to program. You can move the debug.print TargetDate statement to just before the End If statement. On the other hand you are on much safer ground if the print statement is outside of any loops – move it to just before the End Sub statement, and the zeros should not show. At this stage you could say that you should succeed. But you must test it for a range of values – suppose the target is a figure which is not yet met, like £5000. Amend your code so that you assign 5000 to the variable Target, and then run the program. You get an error because the inner loop has ‘overshot’ the end of the recordset. You must test for EOF within the loops and bale out if it is encountered. Amend the program as indicated – the main change is that the keyword Exit is used in two places to jump out of the loop prematurely. Also a boolean variable is declared and used to announce True if the target is reached and False if not. Run the program Listing 7.7 e * * Public Sub SalesTarget() Dim rs As Recordset Dim SalesTotal As Single Dim Target As Single Dim TargetDate As Date Dim TargetReached As Boolean TargetReached = False Target = 5000 SalesTotal = 0 Set rs = CurrentDb.QueryDefs("qryOrderTotalsByDate").OpenRecordset Do Until rs.EOF Do While SalesTotal <= Target If rs.EOF Then Exit Do SalesTotal = SalesTotal + rs(1) If SalesTotal >= Target Then TargetReached = True TargetDate = rs(2) End If rs.MoveNext Loop If rs.EOF Then Exit Do rs.MoveNext Loop Debug.Print TargetDate Debug.Print TargetReached End Sub 6 Access Programming Week 7 Embedding a long SQL statement in code The recordset is based on a query; being lazy it’s easier to refer to rs(2) than rs!OrderDate. But what happens if someone changes the order of the columns? You are on safer ground with exclamation point syntax. But then, what happens if someone sorts the query on another column? This too will scupper your program. Your program will be much more secure if the recordset is an SQL statement that you define in the code. Just copy and paste the SQL from the Access query into a Notepad file. Break it up into manageable chunks, like the following: SELECT PurchaseItem.OrderNo, Sum([quantity]*[PurchaseItem.UnitPrice]) AS ItemTotal, SalesOrders.OrderDate FROM SalesOrders INNER JOIN (Products INNER JOIN PurchaseItem ON Products.ProductID = PurchaseItem.ProductID) ON SalesOrders.OrderNo = PurchaseItem.OrderNo GROUP BY PurchaseItem.OrderNo, SalesOrders.OrderDate ORDER BY SalesOrders.OrderDate ; Observe Listing 7.2f. Declare the variable where marked with * Then create the next group of statements as marked, by pasting from your Notepad file a line at a time. Take care to type a space and the finishing double quote at the end of each line. What is happening is that you are progressively concatenating each line of the SQL statement to a string variable strSQL, until it contains the whole statement. It’s cumbersome, but not that complicated! Finally, the statement to create the recordset is much simplified. Listing 7.7 f Public Sub SalesTarget() Dim rs As Recordset Dim SalesTotal As Single Dim Target As Single Dim TargetDate As Date Dim TargetReached As Boolean * | | | | | Dim strSQL As String strSQL = "SELECT PurchaseItem.OrderNo, Sum([quantity]*[PurchaseItem.UnitPrice]) " strSQL = strSQL & "AS ItemTotal, SalesOrders.OrderDate " strSQL = strSQL & "FROM SalesOrders INNER JOIN (Products INNER JOIN " strSQL = strSQL & "PurchaseItem ON Products.ProductID = PurchaseItem.ProductID) " strSQL = strSQL & "ON SalesOrders.OrderNo = PurchaseItem.OrderNo " strSQL = strSQL & "GROUP BY PurchaseItem.OrderNo, SalesOrders.OrderDate " strSQL = strSQL & "ORDER BY SalesOrders.OrderDate ;" TargetReached = False Target = 5000 SalesTotal = 0 Set rs = CurrentDb.OpenRecordset(strSQL) Do Until rs.EOF Do While SalesTotal <= Target If rs.EOF Then Exit Do SalesTotal = SalesTotal + rs(1) If SalesTotal >= Target Then TargetReached = True TargetDate = rs(2) End If rs.MoveNext Loop If rs.EOF Then Exit Do rs.MoveNext Loop Debug.Print TargetDate Debug.Print TargetReached End Sub 7 Access Programming Week 7 Programming Example: The Seek method contains many example programs which give you a start in using various objects and methods; this program is adapted from one. You can run these programs against Northwind.mdb, or you can try to adapt them to your own databases. Help Listing 7.8 Sub SeekX() Dim Dim Dim Dim Dim Dim Dim db As Database rstProducts As Recordset intFirst As Integer intLast As Integer strMessage As String strSeek As String varBookmark As Variant Set db = CurrentDb ' You must open a table-type Recordset to use an index, and hence the Seek method. Set rstProducts = db.OpenRecordset("Products", dbOpenTable) With rstProducts ' Set the index. .Index = "PrimaryKey" ' Get the lowest and highest product IDs. .MoveLast intLast = !ProductID .MoveFirst intFirst = !ProductID Do While True ' Display current record information and ask user for ID number. strMessage = "Product ID: " & !ProductID & vbCr & _ "Name: " & !ProductName & vbCr & vbCr & _ "Price: " & Format(!UnitPrice, "£#.00") & vbCr & vbCr & _ "Enter a product ID between " & intFirst & _ " and " & intLast & "." strSeek = InputBox(strMessage) If strSeek = "" Then Exit Do ' Store current bookmark in case the Seek fails. varBookmark = .Bookmark .Seek "=", Val(strSeek) ' Return to the current record if the Seek fails. If .NoMatch Then MsgBox "Product ID not found!" .Bookmark = varBookmark End If Loop .Close End With End Sub 8 Access Programming Week 7 DAO Connectivity We’ve looked at methods of DoCmd to transfer data; there are also DAO methods. Connections can be of two types: connecting to another Jet database or SQL Server bypassing Jet by using the Connection object.1 The following example connects one Jet database to another. It uses the Connect and SourceTableName properties of the TableDef object. It doesn’t use the Connection object. Specifically it puts a link to the Customers table into the file Accounts.mdb. (Just modify pathnames and table names to reuse the code). The task of the program is creating a connection string, and assigning it to the Connect property of the TableDef object. The connection string looks like: ";DATABASE=path to .mdb file" Listing 7.9 Sub Connect_1() Dim db As Database Dim tdf As TableDef Dim rst As Recordset Dim strConnect As String ' Open a Microsoft Jet database to which you will link a table. Set db = OpenDatabase("\\nsq024vs\u8\csathfc\Desktop\Accounts.mdb") Modify path ' Put connection string in a variable strConnect = ";DATABASE=\\nsq024vs\u8\csathfc\Desktop\Company2004.mdb" ' Create a new TableDef, set its Connect and SourceTableName properties Set tdf = db.CreateTableDef("Cust") 'this is the link name tdf.SourceTableName = "Customers" 'this is the source table tdf.Connect = strConnect 'append it to the TableDefs collection db.TableDefs.Append tdf db.Close End Sub You could run this code from any database, not just the database that provides the source table. As a test, here I run this program from a new database and incorporate User Id and Password information. I open a database called Madrid.mdb and create a link called ronny to a table named Ronaldo in a database called Manc.mdb. The user ID is andy and the logon password is niceday. Listing 7.10 Sub Connect_2() Dim db As Database Dim tdf As TableDef Dim rst As Recordset Dim strConnect As String ' Open a Microsoft Jet database to which you will link a table. Set db = OpenDatabase("d:\databases\Madrid.mdb", , , "JetTable;uid=andy;pwd=niceday") strConnect = ";DATABASE=d:\databases\Manc.mdb" Set tdf = db.CreateTableDef("ronny") tdf.SourceTableName = "Ronaldo" tdf.Connect = strConnect 1 A Connection object represents a connection to an ODBC database (ODBCDirect workspaces only). ODBCDirect workspaces are not supported in Microsoft Access 2010 9 Access Programming Week 7 db.TableDefs.Append tdf db.Close End Sub Providing password information in the OpenDatabase method: The Connect argument is expressed in two parts: the database type, followed by a semicolon (;) and the optional arguments e.g "JetTable;uid=andy;pwd=niceday" The following example demonstrates a blank password. Set db = OpenDatabase("D:\Databases\Accounts.mdb", , , "JetTable;uid=andy;pwd=") Documentation: Name Required/Optional Description Name Required the name of an existing Microsoft Access database file, or the data source name (DSN) of an ODBC data source. See the Name property for more information about setting this value. Options Optional Sets various options for the database, as specified in Remarks. ReadOnly Optional True if you want to open the database with read-only access, or False (default) if you want to open the database with read/write access. Connect Optional Specifies various connection information, including passwords. Using CreateWorkspace method Here is an alternative procedure to incorporate passwords using the CreateWorkspace method of DBEngine. A workspace is created on line 8, and the user id and password information are provided in the CreateWorkspace method. Otherwise the program is the same as previously. Listing 7.11 Sub Connect_X() Dim db As Database Dim tdf As TableDef Dim rst As Recordset Dim strConnect As String Dim wrkJet As Workspace Set wrkJet = CreateWorkspace("", "andy", "niceday", dbUseJet) ' Open a Microsoft Jet database to which you will link a table. Set db = wrkJet.OpenDatabase("d:\databases\accounts.mdb") ' put connection string in a variable strConnect = ";DATABASE=d:\databases\company2004.mdb" ' Create a new TableDef, set its Connect and SourceTableName properties Set tdf = db.CreateTableDef("tblCustomers") tdf.SourceTableName = "Customers" tdf.Connect = strConnect 'append it to the TableDefs collection db.TableDefs.Append tdf db.Close End Sub 10 Access Programming Week 7 Name Required/Optional Description Name Required A String that uniquely names the new Workspace object. See the Name property for details on valid Workspace names. UserName Required A String that identifies the owner of the new Workspace object. See the UserName property for more information. Password Required A String containing the password for the new Workspace object. The password can be up to 20 characters long and can include any characters except ASCII character 0 (null). UseType Optional One of the WorkspaceTypeEnum values. Note ODBCDirect workspaces are not supported in Microsoft Access 2010. Setting the type argument to dbUseODBC will result in a run-time error. Use ADO if you want to access external data sources without using the Microsoft Access database engine. Exercise Write a program that opens the Accounts database, creates a recordset based on an SQL statement using the Invoices table and debug prints the ChequeNumber field to the Immediate Window 11 Access Programming Week 7 VBA: Do Loops If Then statements and Select Case statements are conditional control structures i.e., statements are executed depending on whether or not one or more conditions are true. Loops are control structures that are concerned with repetition. Loops also often test a condition to determine whether a statement should be repeated – how many times does a statement need to be executed? There are two categories of loops in VBA, Do loops and For loops. Both Do loops and For loops offer variations, giving the programmer strategic options, but often the same result can be accomplished by using one or other. There are four varieties of the Do Loop. Do While . . . Loop Do . . . Loop While Do Until . . . Loop Do . . . Loop Until To test some of these loops, start a new standard module, and save it as LoopTests Do While .. Loop The syntax is: Do While condition statement(s) Loop Public Sub DWL() Dim x As Integer x = 0 Do While x < 5 Debug.Print x x = x + 1 Loop End Sub x < 5 is the condition which evaluates to True two statements to be executed The key word Loop returns control to the Do While statement which re-evaluates the condition; there should be a statement somewhere before the Loop statement which changes the condition otherwise the loop will run ‘forever’ (Note: Ctrl + Break should terminate most runaway loops; if not Ctl Alt + Delete and end Access - and lose unsaved work!) The key characteristic of the Do While Loop is that, depending on circumstances, it may never execute the statements at all. For instance, change the above code by assigning the value 5 to the variable x, and run the code again – nothing prints. If the condition evaluates to False, program execution jumps to the statement after Loop – which in this case is the End Sub statement. Do .. Loop While The syntax is: Do statement(s) Loop While condition Public Sub DLW() Dim x As Integer x = 0 Do Debug.Print x x = x + 1 Loop While x < 5 there is no condition at this stage so the statements to be executed happen at least once. The Loop Statement returns control to the Do statement only if the condition evaluates to true. End Sub 12 Access Programming Week 7 Using Do Loop While is guaranteed to execute the statements at least once. To illustrate, assign 6 to the variable x immediately before the Do statement. The sequence of events is as follows: 6 is printed x is incremented by one, therefore equals 7 Loop While tests the condition, which evaluates to False Program execution continues to the statement after Loop While, which happens to be End Sub The Do While… Loop is generally very useful – many programming tasks can be handled that way Do Until . . Loop and Do . . Loop Until These can be used in ways which are functionally equivalent to the While loops above. If you replace Do While by Do Until the condition is initially False and the statements are repeated until the condition is True. In our example you have to change the direction of the comparison operator to get the equivalent result. Public Sub DUL() Dim x As Integer x = 0 Do Until x > 5 Debug.Print x x = x + 1 Loop End Sub Public Sub DLU() Dim x As Integer x = 0 Do Debug.Print x x = x + 1 Loop Until x > 5 End Sub In these examples we are controlling how many times a loop executes but in general Do Loops are used when you need a statement to be repeated an unspecified number of times. The Do Until loop is the standard way to loop through recordsets. In general programming, using Until can occasionally seem unintuitive. For example consider the following: Public Sub DUL() Dim x As Integer x = 0 Do Until x < 5 Debug.Print x x = x + 1 Loop End Sub The statements in the loop are never executed because the condition is already True. Points to Note: Loops can contain control statements such as If Then or other loops If statements can contain loops You can break out of a loop if necessary by using the Exit Do keywords 13 Access Programming Week 7 Suggested solution to exercise Sub Dim Dim Dim Dim myConnect() stSQL As String db As Database rs As Recordset dbname As String dbname = "C:\Users\andy\Desktop\Accounts.mdb" Set db = Workspaces(0).OpenDatabase(dbname) stSQL = "SELECT ChequeNumber FROM Invoices" Set rs = db.OpenRecordset(stSQL, dbOpenDynaset) Do While Not rs.EOF Debug.Print rs!ChequeNumber rs.MoveNext Loop rs.Close End Sub READING http://oreilly.com/catalog/progacdao/chapter/ch08.html Sample chapter from DAO Object Model: The Definitive Reference By Helen Feddema 14