Previous Page TOC Next Page



- 13 -
Designing Online Transaction-Processing Applications


Transaction-processing (TP) applications are classified in this book as database applications that update data contained in tables. An update to a table occurs when the application changes the value of data in existing records, adds new records, or deletes records from the table. You update data with bound controls or with SQL statements, which Microsoft calls action queries. Semantically, the term query is not an appropriate description of an SQL statement that does not return rows. For consistency with the Microsoft documentation for all versions of Visual Basic and Access, this book uses the term action query to include SQL UPDATE, INSERT, and DELETE queries. You also can use Visual Basic's Edit, AddNew, Delete, and Update methods to make changes to updatable Recordset objects directly.

Online transaction processing (OLTP) is a category of TP where the updates occur on a real-time basis. The alternative to OLTP is batch processing, in which updates to database tables are accumulated as rows of temporary tables. A separate database application processes the data in the temporary tables. The second application deletes the temporary tables when the batch update process is complete. Batch processing often is used in accounting applications, such as submitting daily collections of credit card vouchers to a bank. This chapter deals primarily with OLTP, although most of the techniques you learn here also are applicable to batch processing methods. Some OLTP applications, such as order entry with multiple line items, use a combination of batch and online TP methods. You add the line items to a local temporary table, then add the completed order information to multiple tables in a single transaction.

Categorizing Transaction-Processing Applications


All database applications fall into either the decision-support or transaction-processing class. Decision-support applications need only (and always should be restricted to) read-only access to the database tables. By definition, transaction-processing applications require read-write access to the tables being updated. This section defines some of the terms used in this book, as well as by the industry, to describe transaction-processing applications.

The majority of transaction-processing applications falls into one of the three following categories or subclasses:

Another category of transaction-processing applications that is becoming more widely used in conjunction with downsizing projects involves distributed databases. Transaction-processing applications that make almost-simultaneous updates to tables in more than one database are called distributed OLTP (DOLTP) applications. Transaction processing in a distributed database environment, where the databases involved are located on different servers that may be geographically dispersed, is one of the subjects of Chapter 18.

Transaction monitors (TMs or OLTMs) are a class of transaction-processing applications that were originally designed to manage very large numbers of simultaneous transactions against mainframe database-management systems. TMs are more robust and handle more simultaneous transactions than conventional client/server RDBMSs. IBM's Customer Information Control System (CICS) transaction-monitor application is undoubtedly the most widely used mainframe TM in North America. Use of TMs with today's high-end client/server databases of modest size is relatively uncommon because the server is capable of handling many of the duties of a TM by means of stored procedures and triggers.

Using SQL Statements for Transaction-Processing


As mentioned in the introduction to this chapter, you can use SQL action queries, bound (data-aware) controls, or Visual Basic code in transaction-processing applications. Traditional character-based desktop database applications such as dBASE, Clipper, and FoxPro use GET statements to assign entered data to a variable and REPLACE statements to update a table field with the variable's value. Visual Basic 4.0 is more flexible than DOS desktop database applications because Visual Basic offers a variety of methods of updating database tables with the SQL queries discussed in the sections that follow.

Determining When You Can Update Joined Tables


Action queries that involve joined tables, using either the SQL JOIN syntax or the equal operator in the WHERE clause must operate against updatable Recordset objects. Although action queries do not return Recordset objects, the rules for updating records of joined tables apply just as if action queries create "invisible" Recordsets (which, in fact, they do). Like Access's Datasheet view, Visual Basic 4.0's DBGrid OLE Control provides a visual clue that identifies a query as non-updatable by omitting the tentative append record (the blank record with an asterisk in the record selector button) as the last record in the datasheet view of a Recordset or omitting an empty record as the last record of a continuous form or subform.

[VB4_NEW]Figure 13.1 illustrates the difference between the appearance of an updatable and a non-updatable DBGrid control bound to a Data control through the DBGrid's DataSource property value. The RecordSource property of the Data1 control of Form1 (the upper image) is the Publishers table of Biblio.mdb. Unless you specify the value of the ReadOnly property of a Data control for a table as True, the Recordset object of the Dynaset or Table type derived from a single table is updatable by default, as indicated by the asterisk in the last (empty) record of DBGrid1, called the tentative append record. You determine the type of the Recordset by setting the value of the RecordsetType property to Table (0), Dynaset (1), or Snapshot (2). The Updatable property of the Recordset object to which the Data1 Data control is bound is True, except for Snapshot-type Recordset objects. Typing ? Data1.Recordset.Updatable in the Debug window returns True. (The Updatable property of the Recordset is read-only in run mode and is not available in design mode.) To make tentative append records appear in the DBGrid1 control, however, you must set the value of the EOF property of the Data1 control to 2 (Add New) and the AllowAppend property of the DBGrid1 to True. Both of these properties are new to Visual Basic 4.0.

Figure 13.1. Differences in the appearance of updatable (upper) and non-updatable (lower) bound DBGrid controls.

The RecordSource property of the Data2 Data control of Form2 (the lower image of Figure 13.1) is the following simple SQL statement:




SELECT * FROM Titles, Publishers



WHERE Titles.PubID = Publishers.PubID

The WHERE clause of the SQL statement creates a many-to-one relation between the Titles and Publishers tables based on the PubID fields of both tables. The resulting Recordset object of the Dynaset type is not updatable by default, as evidenced by the lack of a tentative append record after the last record in the Recordset object that underlies the Data2 control. In this case, the Updatable property of the Recordset object to which the Data2 Data control is bound is False. Typing ? Data2.Recordset.Updatable in the Debug window returns False. If you click the last record button of the Data control and then click the next record button in an attempt to append a blank record, you receive a Can't update. Database or object is Read-Only message.



Using a DBGrid control bound to a Data control to perform updates to entire tables or large query result sets is called browse editing. Browse editing is useful for adding test data to tables and experimenting with transaction processing techniques. Few production OLTP applications, however, use free-form browse editing because of the possibility of inadvertent data entry errors. In most cases, OLTP applications allow updating only a few very specific records, such as creating or editing a specific sales order or invoice.

Conventional views of one or more tables you establish with ANSI SQL's CREATE VIEW statement are equivalent to Visual Basic Recordset objects of the Snapshot type; views and Snapshot-type Recordset objects are not updatable under any circumstances. To be consistently updatable, a Recordset object of the Dynaset type created from joined tables must comply with all of the following rules:

The following SQL statement creates an updatable Recordset:




SELECT DISTINCTROW Titles.Title, Titles.ISBN, Titles.PubID,



      Titles.[Year Published], Titles.Description



   FROM Publishers INNER JOIN Titles



      ON Publishers.PubID = Titles.PubID

The following conventional SQL statement using the WHERE clause to create the join creates a non-updatable Recordset:




SELECT DISTINCTROW Titles.Title, Titles.ISBN, Titles.PubID,



      Titles.[Year Published], Titles.Description



   FROM Publishers, Titles



   WHERE Publishers.PubID = Titles.PubID

You cannot update directly a calculated field, but you can update the fields from which the values of data cells in a calculated field are derived. You can't update a field that would cause referential integrity to be violated if a relationship between the tables underlying the Recordset exists and you have elected to enforce referential integrity. As an example, you can't change the value of a record's Titles.PubID field to a value that doesn't exist in the Publishers.PubID field. If you attempt such a change, you receive a Can't add or change record. Referential integrity rules require a related record in table "Publishers" message.



You can use the value (16) of the dbInconsistent option, discussed later in this chapter, as the value of the Options property of the Data control to force a Recordset based on a one-to-many relationship to be updatable. This option is only applicable if the requirement for a primary key or unique index on the "one" table is observed. Using the dbInconsistent option does not override referential integrity rules you previously have applied to the tables involved in a query.


SQL and Set-Oriented Transactions


SQL is a set-oriented language. The SELECT queries you write define a set of records (rows) that Visual Basic and the Jet database engine or a Remote Data Object returns to your application as a Recordset object. Similarly, SQL action queries define a set of records that are to be updated, appended, or deleted. (Appending a new record to a table creates a set of one new record.) You can create SQL action queries with Visual Basic code or employ parameterized QueryDef objects to specify the set of records to which the action query applies. Using SQL action queries that act on multiple records is similar in concept to the batch-processing method described in the introduction to this chapter. SQL action queries attempt to update all records that meet the criteria of the SQL statement's WHERE clause. If you omit the WHERE criteria, the query applies to all records of the table(s).

Using Jet 3.0 SQL action queries is faster in almost all cases than AddNew. . .Update, Edit. . .Update, and Delete operations on Recordset objects. The speed improvement is most dramatic when the query affects a large number of records. An additional benefit of using SQL action queries is that Jet automatically "wraps" the SQL operation in a transaction, eliminating the need for BeginTrans. . .Commit|Rollback statements. Therefore, using a SQL action query can save writing a substantial amount of VBA code. The three sections that follow describe the syntax of SQL action queries, show you how to use the Data Manager add-in to implement referential integrity, and explain how to execute action queries with VBA code.

A Review of SQL Action Query Syntax

Chapter 5, "Learning Structured Query Language," and Chapter 7, "Running Crosstab and Action Queries," briefly discuss the SQL syntax for action queries. The following list provides a description of the Jet SQL and ANSI SQL reserved words that you use to create action queries in Visual Basic OLTP applications:


Adding Referential Integrity with Data Manager

To establish a relation and maintain referential integrity between related tables of your Jet 2.x or 3.0 database with the Data Manager add-in, follow these steps:

  1. Choose Add-Ins | Data Manager to open Data Manager's main window.

  2. Choose File | Open Database from Data Manager's menu and open your .mdb file.

  3. Click the Relations button of the Tables/QueryDefs window to open the Relationships dialog.

  4. Select the base table from the Primary Table drop-down list. The base table must have an unique index on the primary key to establish a relationship.

  5. Select the related table from the Related Table drop-down list.

  6. If the relationship between the base related tables is one-to-many, which is usually the case, click the Many option button.

  7. If the foreign key field name in the related table is the same as the primary key field of the base table, Data Manager automatically selects the foreign key field name in the Select Matching Fields drop-down list. Otherwise, select the name of foreign key field in the related table. The relation appears in the list box.

  8. Mark the Enforce Referential Integrity check box. (See Figure 13.2.)

  9. Click the Add button, click the Close button to close the Relationships dialog, and then close the Data Manager add-in.

Figure 13.2. The Data Manager add-in displaying the relationship between the Publishers and Titles tables of Biblio.mdb.



Enforcing referential integrity with Jet databases adds a set of hidden indexes to the base and related tables. In multiuser applications, Jet applies both read and write locks to index pages while updating tables. If you're creating a multiuser application with a large number of simultaneous users, you're likely to obtain better performance by handling referential integrity, including cascading updates and deletions, with VBA code.

When you use the Jet database engine and a Jet .mdb database with Visual Basic 4.0, an action query is an "all-or-nothing" proposition—all of the records in the set are updated, appended, or deleted, or the query fails and, under certain conditions, you receive an error message. When you use the ODBC API and a multiple-tier ODBC driver with a client/server RDBMS, your ANSI SQL statement and the RDBMS is responsible for determining if the action query can succeed. Some single-tier ODBC drivers—such as Intersolv's ODBC drivers for dBASE, Paradox, and FoxPro databases—support the three ANSI TPL reserved words. The 16-bit and 32-bit Microsoft ISAM and ODBC drivers for these desktop databases do not support TPL queries. Chapter 19, "Understanding the Open Database Connectivity API," explains the capabilities of ODBC drivers in greater detail.



Both ANSI and Jet SQL statements require that the source objects of all queries be persistent. Therefore, SQL statements only can operate on Table and QueryDef objects. You cannot execute an SQL statement with virtual tables (Recordsets) as source objects. Although you can include user-defined functions (UDFs) in Access's implementation of Jet SQL, UDFs are not permitted in SQL queries you execute with Visual Basic.


Executing SQL Action Queries with Visual Basic Code

You execute SQL action queries by applying the Execute method to either a QueryDef or a Database object, as described in the following list:

The general syntax for creating and executing a new action QueryDef object is this:




Dim qdfAction As QueryDef



Dim strSQL As String



strSQL = "{UPDATE|INSERT INTO|DELETE}..."



Set qdfAction = dbName.CreateQueryDef(strSQL)



qdfAction.Execute dbFailOnError [+ dbOtherQueryConstants]

The optional dbOtherQueryConstants flags can be any combination of the flags applicable to the Execute statement, such as dbInconsistent, separated by + or And.

For an existing parameterized QueryDef object with a Name property value of QueryName and two named parameters, CustomerID and OrderDate, use the following syntax:




Dim qdfParams As QueryDef



Set qdfParams = dbName.QueryDefs("QueryName")



qdfParams.Parameters("CustomerID") = "ABCDE"



qdfParams.Parameters("OrderDate") = "12/15/95"



qdfAction.Execute(dbFailOnError [+ dbOtherQueryConstants])

The Jet SQL statement for a parameterized query must be preceded by the PARAMETERS keyword, the name of the parameter, and its Jet data type. The PARAMETERS statement must be separated from the action query by a semicolon. Parameter name references in Jet SELECT and action SQL statements are surrounded by square brackets. The following is an example of an SQL statement to create qdfParams:




PARAMETERS CustomerID Text, OrderDate DateTime;



INSERT INTO Orders(CustomerID, OrderDate)



VALUES([CustomerID], [OrderDate])

An example of the creation and use of a parameterized SELECT query appears in the "Emulating an Access Bound Subform with the Data Bound Grid Control" section, later in this chapter.



[VB4_NEW]The Set qdfName = dbName.QueryDefs("QueryName") syntax replaces the obsolete Set qdfName = dbName.OpenQueryDef("QueryName") syntax of Jet 1.x. Use of the new Jet 2.x/3.0 syntax to open an existing QueryDef object is not well documented in Visual Basic 4.0 online help. Most of the examples use the CreateQueryDef method. If your application deletes (with the DeleteQueryDef method) and recreates new QueryDef objects each time it runs, your Jet 3.0 database expands greatly in size and requires more frequent compaction. (To gain better performance, Jet 3.0 databases don't delete persistent objects. Instead, the "deleted" object is marked for deletion and a new object is added. Compaction actually deletes the objects previously marked for deletion.)

To execute an action query directly from an SQL statement, use the following syntax:




Dim strSQL As String



strSQL = "{UPDATE|INSERT INTO|DELETE}..."



dbName.Execute strSQL, dbFailOnError [+ dbOtherQueryConstants]

Each of the preceding examples uses the dbFailOnError flag for the intOptions argument of the Execute method. If you don't add the dbFailOnError flag, action queries that cannot complete, such as when another user has a lock on an affected record or when a primary key violation occurs, do not raise a trappable error. Instead, the query fails silently and any changes to the underlying tables made prior to the failure are undone. Your error handler for failure of an action query to complete should open a message box that explains the reason for the failure and allow the user to choose whether to try again or abandon the update, as in the following example:




ActionQueryError:



   If MsgBox(Error$, vbRetryCancel + vbCritical, _



         "Database Update Error"[, strHelpFile, intContext]) _



         = vbCancel Then



      Exit Sub



   Else



      Resume



   End If

In a multiuser or client/server environment you may want to introduce a retry loop with a delay to automatically reattempt to execute the query if another user has the record locked. Error handling in multiuser environments is one of the subjects of Chapter 18.



The dbName.ExecuteSQL method of Visual Basic 2.0 and 3.0 for executing SQL passthrough queries against client/server databases is obsolete and should not be used in new Visual Basic 4.0 applications. Instead, substitute dbSQLPassThrough for dbOtherQueryConstants in the preceding syntax examples.

Listing 13.1 is an example of a subprocedure that uses an INSERT query to add a new customer record to the Customers table of Access 95's Northwind.mdb sample database, based on the values of unbound controls. If you specify values for every field of a table, you don't need to include the field names in your SQL statement.

Listing 13.1. Using an SQL INSERT query to add a record to the Customers table.




Private Sub AddNewCustomer()



   Dim dbCurrent As Database



   Dim qdfNewCustomer As QueryDef



   Dim strSQL As String



   On Error GoTo AddNewCustomerError



   Set dbCurrent = _



      dbEngine.Workspaces(0).OpenDatabase("Northwind.mdb")



   Set qdfNewCustomer = dbCurrent.QueryDefs("qdfNewCustomer")



   strSQL = "INSERT INTO Customers VALUES('" & _



      txtCustomer_ID & "', '"



   strSQL = strSQL & txtCompanyName & "', '"



   strSQL = strSQL & txtContactName & "', '"



   strSQL = strSQL & txtContactTitle & "', '"



   strSQL = strSQL & txtAddress & "', '"



   strSQL = strSQL & txtCity & "', '"



   strSQL = strSQL & txtRegion & "', '"



   strSQL = strSQL & txtPostalCode & "', '"



   strSQL = strSQL & txtCountry & "', '"



   strSQL = strSQL & txtPhone & "', '"



   strSQL = strSQL & txtFax & "')"



   qdfNewCustomer.SQL = strSQL



   qdfNewCustomer.Execute dbFailOnError



   Exit Sub



AddNewCustomerError:



   MsgBox Error$, vbCritical, "Unable to Add New Customer"



   Exit Sub



End Sub

Creating a parameterized query with 11 parameters of the text data type is an alternative to the technique shown in Listing 13.1. If you use a control array of txtField(0. . .10) text boxes and a parameterized query, you can simplify the code considerably, as illustrated by Listing 13.2. In addition, the code executes faster because Jet doesn't have to compile the query. If you need a reminder of the field name, set the value of the Tag property of the TextBox control to the name of the field.

Listing 13.2. Replacing a long SQL statement with replaceable query parameters.




Private Sub AddNewCustomer()



   Dim dbCurrent As Database



   Dim qdfNewCustomer As QueryDef



   Dim intCtr As Integer



   On Error GoTo AddNewCustomerError



   Set dbCurrent = _



      dbEngine.Workspaces(0).OpenDatabase("Northwind.mdb")



   Set qdfNewCustomer = dbCurrent.QueryDefs("qdfNewCustomer")



   For intCtr = 0 To 10



      qdfNewCustomer.Parameters(intCtr).Value = _



         txtField(intCtr).Value



   Next intCtr



   qdfNewCustomer.Execute dbFailOnError



Exit Sub



AddNewCustomerError:



   MsgBox Error$, vbCritical, "Unable to Add New Customer"



   Exit Sub



End Sub

Recordset Updates with Bound Controls


You can use bound text boxes or other controls that can be bound to a Data control to update those fields of an updatable Recordset object that are not calculated fields. Using bound text boxes is the most common (but not necessarily the best) method of updating table data in Visual Basic and Access applications. You also can use the DBGrid control with Visual Basic 4.0 transaction-processing applications. The sections that follow describe a typical application that uses these two types of bound controls for data entry.

Using the Data Form Designer to Add Bound Text Boxes for Data Entry


The simplest method of creating a data-entry form for transaction processing is to use the Data Form Designer (DFD) add-in. The DFD adds a Data control to your form, and then adds a control array of labels and bound text boxes to the form, one for each field of the table or column of the query to which the Data control is bound. DFD automatically sets the DataSource property of the TextBox control to the name of the Data control and the DataField property to the name of the field. Finally, DFD adds a set of standard buttons (Add, Delete, Refresh, Update, and Close) to the form, plus a substantial amount of code for the buttons and control objects to get you started on an OLTP form. DFD is quite similar to Access 95's Form Wizard, except that the Form Wizard requires several steps and doesn't add the buttons or code for you. Figure 13.3 shows the DFD settings to create a simple form based on the Publishers table of Biblio.mdb. Figure 13.4 illustrates the result of clicking DFD's Build the Form button.

Figure 13.3. Using Data Form Designer to create a standard OLTP form based on the Publishers table of Biblio.mdb.

Figure 13.4. The OLTP form for the Publishers table created by the Data Form Designer settings shown in Figure 13.3.

The bound text boxes that DFD adds to the OLTP form comprise a control array of txtFields(0. . .9) control corresponding to the iteration of Field objects in the Fields collection of the TableDef object for the Publishers table. DFD sets the Multiline property of the TextBox control to True for Memo fields. DFD uses the SourceField value of the Field object to set the value of the DataField property of each bound text box. Similarly, the value of the Caption property of the lblLabels(0. . .9) control array is obtained from the value of the Name property of the Field object. DFD applies a tag prefix for the labels (lbl) and text boxes (txt), but uses the default Data1 name for the Data control. For consistency with the Leszynski naming conventions, it's a good practice to rename the Data control to dtcName, then set the value of the DataSource property of each TextBox control to dtcName. You need to use the Find and Replace feature of the Visual Basic code editor to replace Data1 with dtcName in the VBA code DFD adds to the form prior to running the modified form.



Even if you intend to use parameterized action queries with unbound text boxes, it's faster than manual operations to let DFD create your form's labels and text boxes. To unbind the text boxes, simply delete the values of the DataField and DataSource properties of each TextBox control, then delete the Data1 control.

Data Form Designer doesn't translate the Size property of the field object into a width that's appropriate for all of the controls that DFD adds to the form. Therefore, you're likely to want to rearrange the form to suit your own taste and adjust the size of the text boxes. Figure 13.5 shows the rearranged Publishers form with text boxes and labels sized to suit their contents, in preparation for adding a bound data grid below the Comments text box. (The entry for Sams Publishing in Figure 13.5 has been updated from the original data in the Publishers table of Biblio.mdb.)

Figure 13.5. The OLTP form rearranged in preparation for adding a DBGrid control to display each publisher's book titles.

Emulating an Access Bound Subform with the Data Bound Grid Control


Using datasheet views and continuous subforms to update data in related tables is a common practice in commercial Access database applications. Under some circumstances, such as entering data from time cards (or emulating a paper time card form with a grid), a DBGrid or third-party spreadsheet control is the best choice for data entry. Access provides a bound subform that's easy to link to a main form with the LinkMasterField and LinkChildField properties. Although not publicized in the Visual Basic documentation or an online help file, its almost as easy to emulate an Access bound subform of the datasheet style with a Visual Basic DBGrid control. To synchronize the contents of the DBGrid control with the active record of the main form, you use a parameterized SELECT query. Figure 13.6 shows the form/subform combination in operation. The Data control to which the DBGrid subform is bound is hidden, because the user can employ the vertical scrollbar when there are more book titles than visible rows in the subform. The Update button has a new caption, and Cancel and Find buttons have been added to the main form shown in Figure 13.6. The sections that follow describe how to add a synchronized subform to your main form and discuss the reasons for the three button changes.

Figure 13.6. A synchronized bound DBGrid control displaying book titles available from the selected publisher.



The 32-bit version of the OLTPGrid application (OLTPGrid.vbp) and an updated Jet 3.0 version of the Biblio.mdb sample database (Biblio95.mdb) is located in the \DDG_VB4\32_Bit\Chaptr13 folder of the accompanying CD-ROM.

The PubID field is the primary key field of the Publishers table (corresponding to the LinkMasterField value in Access) and the foreign key field of Titles table (Access's LinkChildField value). The following is the SQL statement for the parameterized QueryDef object, qdfSubform, that creates the one-to-many Recordset object for the hidden dtcSubform Data control:




PARAMETERS [ParamPubID] Long;



SELECT DISTINCTROW Titles.*



   FROM Publishers INNER JOIN Titles



      ON Publishers.PubID = Titles.PubID"



   WHERE Titles.PubID = [ParamPubID]"

You pass the value of the PubID field (txtFields(0).Text) to qdfSubform.Parameters("qdfSubform").Value, and then open a Recordset (rsdSubform) over the result set of the newly parameterized QueryDef object.

The important differences between a conventional bound Data control and dtcSubform is that the DatabaseName and RecordSource property values of dtcSubform are Null (empty), and the value of the Data control's Visible property is False. You replace the value of the Recordset property of the Data control at runtime with a Set dtcSubform.Recordset = dbBiblio.OpenRecordset("qdfSubform") or equivalent statements. (The object.Recordset [= value] syntax shown in the online help file is incorrect for assignment of value; the Set reserved word, as shown in the online help sample code, is required.)

Listing 13.3 shows the VBA subprocedure code required to create the qdfSubform QueryDef if it does not exist, populate the dbgSubform grid, and repopulate dbgSubform upon repositioning of the record pointer of the dtcForm Data control. Code and comments added by the Data Form Designer are included. Error trapping is included in the code behind frmOLTPGrid only where required to make the application workable; in a production application, all of the subprocedures should include error trapping.



Unlike Access, Visual Basic 4.0 does not include parameterized QueryDef objects in its design-mode lists of persistent objects in a database. As an example, the qdfSubform QueryDef does not appear in the combo list for the RecordSource property of Data controls bound to the Biblio95.mdb database. Parameterized QueryDef objects also don't appear in the Tables/Queries list of the Data Manager add-in. Fortunately, parameterized QueryDef objects are included in the QueryDefs collection.

Listing 13.3. The Declarations section of frmOLTPGrid and the Form_Activate, PopulateGrid, and dtcSubform_Reposition subprocedures, which synchronize the subform with the Publisher record selected in the main form.




Option Explicit



Dim dbBiblio As Database    'Biblio.mdb



Dim qdfSubform As QueryDef  'QueryDef for subform



Dim rsdSubform As Recordset 'Recordset for subform



Dim strSQL As String        'SQL for QueryDef



Dim fActivated As Boolean   'Form_Activate flag



Dim fEditing As Boolean     'Form editing flag



Dim intCtr As Integer       'General purpose counter



Private Sub Form_Activate()



   'Handles all activity that's normally in Form_Load



   'Do not execute after form has been activated



   If fActivated Then



         Exit Sub



   End If



   Dim fCreateQDF As Boolean



   Screen.MousePointer = ccHourglass



   DoEvents



   'Lock the bound text boxes



   For intCtr = 1 To 9



      txtFields(intCtr).Locked = True



      txtFields(intCtr).TabStop = False



   Next intCtr



   'Retrieve all the records in the form's Recordset



   dtcForm.Recordset.MoveLast



   dtcForm.Recordset.MoveFirst



   'The database has been opened by the dtcForm control



   Set dbBiblio = DBEngine.Workspaces(0).Databases(0)



   'Interate QueryDefs collection to see if QueryDef exists



   fCreateQDF = True



   For intCtr = 0 To dbBiblio.QueryDefs.Count - 1



      If dbBiblio.QueryDefs(intCtr).Name = "qdfSubform" Then



         fCreateQDF = False



         Exit For



      End If



   Next intCtr



   If fCreateQDF Then



      'Create the SQL statement for the parameter query



      strSQL = "PARAMETERS [ParamPubID] Long; "



      strSQL = strSQL & "SELECT DISTINCTROW Titles.* "



      strSQL = strSQL & "FROM Publishers INNER JOIN Titles "



      strSQL = strSQL & "ON Publishers.PubID = Titles.PubID "



      strSQL = strSQL & "WHERE Titles.PubID = [ParamPubID];"



      'Create the parameterized QueryDef



      Set qdfSubform = dbBiblio.CreateQueryDef("qdfSubform", strSQL)



   Else



      'The following syntax replaces the OpenQueryDef method



      Set qdfSubform = dbBiblio.QueryDefs("qdfSubform")



   End If



   fActivated = True



   'Populate the subform for the first record



   Call PopulateGrid



   'Set the column widths of the first three columns



   dbgSubform.Columns(0).Width = 2000



   dbgSubform.Columns(1).Width = 500



   dbgSubform.Columns(2).Width = 1200



   'Don't display the PubID field in the subform



   dbgSubform.Columns(3).Visible = False



   'Reposition and hide the Find drop-down list



   dbcFind.Left = txtFields(1).Left



   dbcFind.Top = txtFields(1).Top - 50



   dbcFind.Visible = False



   Screen.MousePointer = ccDefault



End Sub



Private Sub PopulateGrid()



   'Create a new Recordset from the parameterized query



   qdfSubform.Parameters("ParamPubID") = txtFields(0).Text



   Set rsdSubform = qdfSubform.OpenRecordset



   'Replace the Recordset of the Data control



   Set dtcSubform.Recordset = rsdSubform



End Sub



Private Sub dtcForm_Reposition()



   'Updates the subform unless adding a new record



   If fActivated And dtcForm.EditMode <> dbEditAdd Then



      Call PopulateGrid



   End If



   'Code added by Data Form Designer follows



   Screen.MousePointer = vbDefault



   On Error Resume Next



   'This will display the current record position



   'for dynasets and snapshots



   dtcForm.Caption = "Record: " & _



      (dtcForm.Recordset.AbsolutePosition + 1)



   'for the table object you must set the index property when



   'the recordset gets created and use the following line



End Sub


The Form_Activate event handler must contain code that acts on the Recordset objects of Data controls; if you use the Form_Load event handler to manipulate these Recordset objects, you receive an Object variable or With block variable not set error message when the VBA interpreter reaches the dtcForm.Recordset.MoveLast instruction. Data bound controls are not guaranteed to be populated until the Form_Activate event triggers.

Data entry in the dbgSubform control follows the rules applicable to Access datasheets. As an example, you position the record pointer to the tentative append record of the grid to add a new title. For consistency with the form controls, it is a good design practice to add a similar set of buttons below the grid control that apply the Edit, AddNew, and Cancel methods to the grid control. The event-handling code for buttons that manipulate a DBGrid control is similar to that for the buttons on frmOLTPGrid.



The OLTPGrid application doesn't append a new record to the TitleAuthor and Authors table when appending a new record to the Titles table. A production OLTP application should provide the data entry operator the opportunity to update all tables that are related to the primary table underlying the OLTP form, regardless of whether the relationships between the tables are direct or indirect.


Adding an Rich Text Editing Form for the Comments Field of the Subform


[VB4_NEW]Adding or updating the content of Memo fields in a DBGrid control is difficult for data entry operators; a separate pop-up form with a multiline TextBox control solves the basic Memo field data entry problem. If you want to format the comments, or import or export formatted comments from or to a word processing application, the 32-bit version of Visual Basic 4.0 provides the new Rich TextBox control, which is one of the Windows 95 common controls. Using the Rich TextBox control (RichTx32.ocx) requires running your application under Windows 95 or Windows NT 3.51. (The 32-bit Windows 95 common controls were added to Windows NT as part of the 3.51 upgrade).

The following list describes the most important features of the Rich TextBox control that aren't supported by the standard TextBox control:

Figure 13.7 shows the frmComments form open for editing the Comments field of the Titles table of Biblio.mdb. Buttons are included on the frmComments form only for applying the bold and italic attributes to selected text, but you can add buttons to a toolbar or supply menu choices for other attributes, such as color, underline, and strikethrough. Listing 13.4 shows the VBA code to open the frmComments dialog, apply attributes, and save or discard the changes to the Comments field. The dbgSubform_DblClick and dbgSubform_Keydown event handlers of Listing 13.4 are included in frmOLTPGrid; the balance of the subprocedures are in frmComments. When you edit conventional text in a Comments field, the rtbComments control automatically changes the format from conventional ANSI text to RTF.

Figure 13.7. Entering formatted data for the Comments field in the frmComments form.

Listing 13.4. Code to enable the Rich TextBox control to edit the Comments field of the Title table using RTF.




Private Sub dbgSubform_DblClick()



   'Double click the Comments column to



   'open the Comments editing form



   If dbgSubform.Col = 7 Then



      'Add a prompt if Comments field is empty



      If IsNull(dtcSubform.Recordset.Fields(7).Value) Then



         frmComments!rtbComments.TextRTF = _



            "Replace with publisher's propaganda"



      Else



         frmComments!rtbComments.TextRTF = _



            dtcSubform.Recordset.Fields(7).Value



      End If



      frmComments.Show 1



   End If



End Sub



Private Sub dbgSubform_KeyDown(KeyCode As Integer, _



      Shift As Integer)



   'Allow use of F2 to open Comments editing form



   If KeyCode = vbKeyF2 Then



      Call dbgSubform_DblClick



   End If



End Sub



Private Sub cmdBold_Click()



   'Set the selection bold



   If Not IsNull(rtbComments.SelRTF) Then



      rtbComments.SelBold = True



   End If



End Sub



Private Sub cmdItalic_Click()



   'Set the selection bold



   If Not IsNull(rtbComments.SelRTF) Then



      rtbComments.SelItalic = True



   End If



End Sub



Private Sub cmdCancel_Click()



   'Close the form



   Unload Me



End Sub



Private Sub cmdOK_Click()



   'Make the changes to the Comments field



   frmOLTPGrid.dtcSubform.Recordset.Edit



   frmOLTPGrid.dtcSubform.Recordset.Fields(7).Value = _



      rtbComments.TextRTF



   frmOLTPGrid.dtcSubform.Recordset.Update



   Unload Me



End Sub

Enhancing the Command Buttons Added by the Data Form Designer


One of the most important features of any transaction-processing form is the capability to cancel additions or edits to the data. The event-handling code created by the Data Form Designer for the Add and Update buttons doesn't give the data entry operator a choice of aborting an addition to or an edit of the current Publisher data. Thus, a Cancel button, cmdCancel, is added to the form. As a rule, edits to important data should not be made allowed to occur by accident. Thus, the txtFields(0. . .9) text boxes are locked until the operator clicks the cmdUpdate button, which initially appears with an Edit caption. In editing mode (indicated by the fEditing flag set True) the text boxes are unlocked, the cmdUpdate button's caption changes to Update, and the cmdCancel button is enabled.

Listing 13.5 shows the code required to enhance the editing operations of frmOLTPGrid. Most of the code involves command button housekeeping functions to enable or disable buttons that are not appropriate to the operation being conducted. As an example, the Cancel button is enabled only in record-editing or addition mode; the Add button is disabled in addition mode to prevent inadvertent addition of another record. The cmdAdd_Click event handler creates a Recordset clone that's used to determine the last PubID value in order to automatically add an incremented PubID value for the new record. Using a Recordset clone eliminates the need to reposition the record pointer of the dtcForm control to find the last PubID value.

Listing 13.5. Code to enhance the operation of the Add, Update, and Refresh buttons and to allow edits or additions to be canceled.




Private Sub cmdUpdate_Click()



   'Sets edit or update mode



   Dim fAdded As Boolean



   If fEditing Then



      'Update mode



      If dtcForm.Recordset.EditMode = dbEditAdd Then



         'Flag for added record



         fAdded = True



      End If



      dtcForm.Recordset.Update



      'Lock the form text boxes



      For intCtr = 1 To 9



         txtFields(intCtr).Locked = True



         txtFields(intCtr).TabStop = False



      Next intCtr



      fEditing = False



      'Button housekeeping



      cmdAdd.Enabled = True



      cmdFind.Enabled = True



      cmdCancel.Enabled = False



      cmdUpdate.Caption = "&Edit"



      cmdDelete.Enabled = True



      cmdRefresh.Enabled = True



      'Required to remain on the last edited record



      If fAdded Then



         dtcForm.Recordset.MoveLast



      End If



   Else



      'Editing mode



      dtcForm.Recordset.Edit



      'Unlock the form text boxes



      For intCtr = 1 To 9



         txtFields(intCtr).Locked = False



         txtFields(intCtr).TabStop = True



      Next intCtr



      fEditing = True



      'Button housekeeping



      cmdAdd.Enabled = False



      cmdFind.Enabled = False



      cmdCancel.Enabled = True



      cmdDelete.Enabled = False



      cmdRefresh.Enabled = False



      cmdUpdate.Caption = "&Update"



      'Hide the Find drop-down list



      dbcFind.Visible = False



      txtFields(1).Visible = True



      txtFields(1).SetFocus



   End If



   'Code added by Data Form Designer follows



   'Data1.UpdateRecord



   'Data1.Recordset.Bookmark = Data1.Recordset.LastModified



End Sub



Private Sub cmdAdd_Click()



   'Create a Recordset clone to get the last PubID value



   Dim rscClone As Recordset



   Set rscClone = dtcForm.Recordset.Clone()



   rscClone.MoveLast



   For intCtr = 1 To 9



      txtFields(intCtr).Locked = False



      txtFields(intCtr).TabStop = True



   Next intCtr



   'Button housekeeping



   cmdAdd.Enabled = False



   cmdFind.Enabled = False



   cmdCancel.Enabled = True



   cmdDelete.Enabled = False



   cmdRefresh.Enabled = False



   cmdUpdate.Caption = "&Update"



   'Hide the Find drop-down list



   dbcFind.Visible = False



   txtFields(1).Visible = True



   txtFields(1).SetFocus



   'Set the editing flag and add a tentative append record



   fEditing = True



   dtcForm.Recordset.AddNew



   'Emulate an AutoIncrement field for PubID



   txtFields(0).Text = rscClone.Fields(0).Value + 1



   Set rscClone = Nothing



   'Code added by Data Form Designer follows



   'Data1.Recordset.AddNew



End Sub



Private Sub cmdCancel_Click()



   'Cancel an update or edit (added button)



   fEditing = False



   dtcForm.Recordset.CancelUpdate



   'Button housekeeping



   cmdCancel.Enabled = False



   cmdDelete.Enabled = True



   cmdRefresh.Enabled = True



   cmdAdd.Enabled = True



   cmdFind.Enabled = True



   cmdUpdate.Caption = "&Edit"



End Sub



Private Sub cmdDelete_Click()



   'Following code solves the last record error problem



   Dim fLastRecord As Boolean



   On Error GoTo DeleteError



   If dtcForm.Recordset.AbsolutePosition + 1 = _



         dtcForm.Recordset.RecordCount Then



      fLastRecord = True



   End If



   dtcForm.Recordset.Delete



   If fLastRecord Then



      dtcForm.Recordset.MovePrevious



   Else



      dtcForm.Recordset.MoveNext



   End If



   Exit Sub



   'Code added by Data Form Designer follows



   'this may produce an error if you delete the last



   'record or the only record in the recordset



   'Data1.Recordset.Delete



   'Data1.Recordset.MoveNext



DeleteError:



   MsgBox Error$, vbCritical, "Can't Delete Record"



   Screen.MousePointer = ccDefault



   Exit Sub



End Sub



Private Sub cmdRefresh_Click()



   'Note: dtcSubform.Refresh fails with "Too few parameters" message



   Call PopulateGrid



   'Code added by Data Form Designer follows



   'this is really only needed for multi user apps



   dtcForm.Refresh



End Sub

Adding a Hidden Find Drop-Down Combo List to the Publishers Form


Using the record selectors of the Data control to locate a particular record becomes increasingly less efficient as the number of rows in the underlying Recordset object grows. For Recordset objects of less than 100 or 200 rows, a drop-down combo list offers a quick navigation method. Figure 13.8 shows a combo list added to frmOLTPGrid; the combo list displays the values of the Name field of the Publishers table in alphabetic order. Clicking the Find button hides the txtFields(1) text box, makes the combo list visible, and opens the combo list. Selecting an item in the list (other than the item appearing in the text element of the combo) triggers a Change event. You use the dbcName_Change event handler to position the record pointer of the Data control underlying the form. A combo list of this type eliminates the need to display the Data control.

Figure 13.8. The Find drop-down bound combo list for selecting a publisher by name.

The value of the RecordSource property of the dtcFind Data control for the dbcFind bound combo list is a conventional SQL statement, SELECT PubID, Name FROM Publishers ORDER BY Name. Both the PubID and Name fields are included so that you can use either the value of the PubID or the Name field to set the record pointer of dtcForm. Bound combo lists and bound list boxes usually set the value of a column of another Data control's Recordset; thus, these two controls most commonly are bound to two Data controls. In this case, dbcFind is bound only to dtcFind, so you must leave empty the DataField and DataSource properties of dbcFind. Listing 13.6 shows the code required to display the dbcFind combo list and to position the record pointer of the Recordset object of the dtcForm to the desired record.

Listing 13.6. Code to display the Find drop-down combo list and set the record pointer of the Recordset object of the Publishers table.




Private Sub cmdFind_Click()



   'Controls the Find bound combo box



   If dbcFind.Visible = False Then



      'Display the Find drop-down list



      dbcFind.Visible = True



      txtFields(1).Visible = False



      DoEvents



      'Open the combo box



      dbcFind.SetFocus



      SendKeys "{F4}"



   Else



      'Hide the Find drop-down list



      dbcFind.Visible = False



      txtFields(1).Visible = True



   End If



End Sub



Private Sub dbcFind_Change()



   'Use the FindFirst method after initial form activation



   If fActivated Then



      dtcForm.Recordset.FindFirst "Name = '" & _



         dbcFind.BoundText & "'"



      dbcFind.Visible = False



      txtFields(1).Visible = True



   End If



End Sub


If the Recordset contains more than 100 or 200 rows, it's faster to use an unbound text box into which the operator enters the first few characters of the Name field. Pressing Enter or clicking the Find button applies the FindFirst method with a wildcard, using an instruction such as this:

Add the Option Compare Text instruction to the Declaration section of your form to make the search case insensitive. A more sophisticated method is to execute a SELECT query based on the text box entry. If the query returns more than one record, provide a combo list or a list box to select the desired record.


Processing Multitable Transactions


In everyday English, a transaction infers a business deal, such as trading cash for a new CD player or for a tank car of acrylonitrile. The dictionary defines "to transact" as "to drive through" or "to complete." Database transactions can involve changes to or additions of one or more records in a single table or in several tables. When more than one record or table is involved in a transaction, it is vital that either all the records be updated simultaneously or as close to simultaneously as possible. The database is said to be in an inconsistent state until the records of each of the tables involved in the transaction have been updated successfully.

If hardware or software errors occur, if a domain or referential integrity violation is detected or if the application is unable to alter or add a record because of locks placed on one or more of the records involved by others in a multiuser environment, the updates to all tables must be canceled. Any changes made to tables before the transaction operation terminates must be undone. An example is an automatic teller transaction—your bank credits their cash account and debits your checking account whenever you make a cash withdrawal at an ATM terminal. Obviously, your bank does not want a one-sided transaction to occur wherein you receive the cash but your account is not debited. Canceling or undoing a transaction is called rolling back the transaction.

Visual Basic 4.0 provides the BeginTrans, CommitTrans, and Rollback instructions to maintain database consistency in transaction-processing applications. These three instructions actually are methods that apply to an invisible Recordset (a buffer) that contains the pending update(s) to the database tables. Another invisible Recordset, the transaction log, stores a copy of the data contained in the affected rows of the Recordset before the updates occur. You can use Visual Basic's three TPL instructions to ensure that all updates to database tables proceed to completion and the database returns to a consistent state when the transaction completes or when the updates are rolled back. The following sections describe the structure of code that employs Visual Basic's transaction-processing instructions and the limited applicability of the Rollback instruction to supported database types.

Structure of the Transaction Instructions


The general structure of the Visual Basic transaction-processing commands, expressed in meta-code, is as follows:




{Sub|Function} Name()



   ...



   On Error GoTo RollbackLabel



   Workspace.BeginTrans



      Do While Condition



        [Recordset.Edit|AddNew]



        [Field update code...]



         Recordset.{Update|Delete}



      Loop



   Workspace.CommitTrans



   Exit {Sub|Function}



RollbackLabel:



   Workspace.Rollback



   [MsgBox Error$, vbCritical, strTitle]



   Exit {Sub|Function}



End {Sub|Function}

The following is the meta-code for an alternate structure that incorporates error processing within a loop structure:




{Sub|Function} Name()



   ...



   Workspace.BeginTrans



      Do While Condition



        [Recordset.Edit|AddNew]



        [Field update code...]



         Recordset.{Update|Delete}



         If Error Then



            fError = True



            Exit Do



         End If



      Loop



      If fError Then



         Error = 0



         Workspace.Rollback



      Else



   Workspace.CommitTrans



      End If



   'End of transaction



End {Sub|Function}

The preceding example is illustrative only; the BeginTrans. . .CommitTrans operation automatically detects errors for you. You need only provide conventional error handling code for failure of the transaction as a whole, not for errors that occur within loops.

The BeginTrans and CommitTrans methods of the Workspace object always are used in pairs. If you use BeginTrans and forget the CommitTrans method, the transaction log fills with all the pre-update Recordset values that occur after the BeginTrans statement is encountered. The accumulated data ultimately consumes all of your computer's memory, and a trappable run mode error (error number 2004) occurs. Read locks are placed on all of the records; read locks aren't removed until the user closes the database. Code within the BeginTrans. . .CommitTrans structure ordinarily is indented to identify the elements that constitute the transaction. You can nest transactions applied to Jet databases up to five levels deep. When you nest transactions, you need to write code that rolls back each set of transactions, beginning with the innermost nested transaction.

When the BeginTrans statement is executed, the following operations occur:

  1. Visual Basic instructs the Jet database engine to open a temporary update buffer and a temporary transaction log. Both the update buffer and the transaction log are virtual tables that are stored in memory.

  2. All records for transactions that occur prior to the execution of the CommitTrans statement are stored in the update buffer.

  3. When the CommitTrans statement is reached, the Jet database engine commences the execution of the transactions stored in the update buffer.

  4. Prior to replacing or deleting records, the record that is to be updated or deleted is saved in the transaction log.

  5. Jet then attempts to update, delete, or add new records to the table. If no errors occur, the changes are made permanent in the tables, and the temporary transaction log is cleared.

  6. If an error is generated during the transaction process, the update buffer is cleared, program execution jumps to your error-handling routine, and the Rollback instruction is executed.

  7. The Rollback instruction replaces records that were updated or deleted with records from the transaction log file. Any records added to tables during with the AddNew method are deleted.

  8. When the Rollback operation is completed, the temporary transaction log file is cleared.

One of the advantages of using Jet's transaction-processing instructions is that bulk updates to tables occur much faster than when you apply the Update or Delete methods to a single record. Each time you apply the Update or Delete methods singly, Jet adds, modifies, or deletes parts of the physical table file, and then flushes all disk write buffers in the process. When you use the transaction-processing instructions, all the operations are conducted in buffers (in memory), with a single write-buffer flush operation at the end of the process.

Executing a Multitable Transaction with VBA Code


Listing 13.7 illustrates VBA code that adds an order with a variable number of line items to the Orders and Order Details tables of Northwind.mdb. The line items are added to a temporary local table prior to execution of the transaction. Because referential integrity is enforced between the Orders and Order Details tables, the order must be entered first, followed by the line items.

Listing 13.7. Executing an order entry transaction with VBA code.




Private Sub AddNewOrder()



   'Obtain a tentative order ID



   lngOrder_ID = DMax("Order_ID", "Orders") + 1



   txtOrder_ID.Enabled = True



   txtOrder_ID.Value = lngOrder_ID



   'Add order and detail items as a transaction



   On Error GoTo RollbackOrder



   wsCurrent.BeginTrans



      'Add the order to the Orders table



      rsdOrders.AddNew



      rsdOrders!OrderID = lngOrderID



      rsdOrders!CustomerID = txtCustomerID



      rsdOrders!EmployeeID = cboEmployeeID



      rsdOrders!ShipName = txtShipName



      rsdOrders!ShipAddress = txtShip_Address



      rsdOrders!ShipCity = txtShipCity



      rsdOrders!ShipRegion = txtShipRegion



      rsdOrders!ShipPostal_Code = txtShipPostalCode



      rsdOrders!ShipCountry = txtShipCountry



      rsdOrders!ShipVia = cboShipVia



      rsdOrders!OrderDate = txtOrderDate



      rsdOrders!RequiredDate = txtRequiredDate



      'Following fields are not updated for a new order



      'rsdOrders!Shipped_Date = txtShippedDate



      'rsdOrders!Freight = txtFreight



      rsdOrders.Update



      'Add the line item(s) to the Order Details table



      rsdLineItems.MoveFirst



      Do Until rsdLineItems.EOF



         rsdOrderDetails.AddNew



         rsdOrderDetails!Order_ID = lngOrder_ID



         rsdOrderDetails!Product_ID = rsdLineItems!Product_ID



         rsdOrderDetails!Quantity = rsdLineItems!Quantity



         rsdOrderDetails!Unit_Price = rsdLineItems!Unit_Price



         rsdOrderDetails!Discount = rsdLineItems!Discount



         rsdOrderDetails.Update



         rsdLineItems.MoveNext



      Loop



   wsCurrent.CommitTrans



   Exit Sub



RollbackOrder:



   MsgBox Error$, 48, "Unable to Add Order " & lngOrder_ID



   wsCurrent.Rollback



   cmdAddNewOrder.Enabled = True



   cmdCancelNewOrder.Enabled = True



   Exit Sub



End Sub

Applicability of Jet Transaction Methods


Only Jet databases fully support transaction-processing methods when you connect these databases with the Jet database engine. Transaction-processing support is inherent in Jet databases, but Btrieve databases require the special network transactions file, BTRIEVE.TRN, discussed in Chapter 6, "Connecting to Other Desktop Database Tables." FoxPro, dBASE, and Paradox databases do not support the rolling back of transactions. You can test whether a Recordset object of the Table or Dynaset type supports transactions by testing the value of the Recordset object's Transactions property. You can roll back changes to most tables if the value of the Transactions property is True.

To take maximum advantage of the transaction-processing capabilities of client/server databases, you need to apply the Execute method with the SQL passthrough option. Instead of using the BeginTrans. . .CommitTrans code structure, you use the ANSI SQL BEGIN TRANS[ACTION]. . .[ COMMIT TRANS[ACTION] structure (or its equivalent) to enclose the action query statements. The method of detecting the failure of COMMIT TRANS[ACTION] in order to execute the ROLLBACK TRANS[ACTION] statement varies with the RDBMS you use.

The SQL statement you pass to the server RDBMS must correspond to the transaction syntax requirements of the particular RDBMS in use. Some client-server RDBMSs, such as SQL Server, require that you identify the beginning of a transaction with a BEGIN TRANS[ACTION] statement. DB2, on the other hand, uses intrinsic transactions; a DB2 unit of work commences with the first change you make to a database that is in a consistent condition and terminates with a COMMIT statement. If the COMMIT statement is unsuccessful, the ROLLBACK [WORK] statement undoes the unit of work. Listing 13.7 performs the operations of the code in preceding listing on the tables of Northwind.mdb exported to Microsoft SQL Server 6.0. It is not necessary to send the ROLLBACK statement to SQL Server; the transaction automatically is rolled back if the COMMIT TRANS[ACTION] statement fails.

Listing 13.7. Executing a SQL passthrough transaction on a server RDBMS.




Private Sub AddNewOrder()



   On Error GoTo PassthroughError



   'Obtain the tentative order ID



   strSQL = "SELECT MAX(Order_ID) Last_Order FROM Orders"



   Set qdfOrderID = dbCurrent.OpenQueryDef("qdfOrderID")



   qdfOrderID.Connect = strConnect



   qdfOrderID.SQL = strSQL



   qdfOrderID.ReturnsRecords = True



   Set rsdOrders = dbCurrent.OpenRecordset("qdfOrderID")



   lngOrder_ID = rsdOrders.Last_Order + 1



   txtOrder_ID.Enabled = True



   txtOrder_ID.Value = lngOrder_ID



   'INSERT the order information



   strSQL = "BEGIN TRAN INSERT Orders VALUES(" & _



      txtOrderID & ", '"



   strSQL = strSQL & txtCustomerID & "', "



   strSQL = strSQL & cboEmployeeID & ", '"



   strSQL = strSQL & txtShipName & "', '"



   strSQL = strSQL & txtShipAddress & "', '"



   strSQL = strSQL & txtShipCity & "', '"



   strSQL = strSQL & txtShipRegion & "', '"



   strSQL = strSQL & txtShipPostalCode & "', '"



   strSQL = strSQL & txtShipCountry & "', "



   strSQL = strSQL & cboShipVia & ", '"



   strSQL = strSQL & txtOrderDate & "', '"



   strSQL = strSQL & txtRequiredDate & "', "



   strSQL = strSQL & "null, null) "



   'INSERT the line item information for each record



   rsLineItems.MoveFirst



   Do Until rsLineItems.EOF



      strSQL = strSQL & "INSERT Order_Details VALUES(" _



         & lngOrder_ID & ", "



      strSQL = strSQL & rsLineItems!ProductID & ", "



      strSQL = strSQL & rsLineItems!UnitPrice & ", "



      strSQL = strSQL & rsLineItems!Quantity & ", "



      strSQL = strSQL & rsLineItems!Discount & ") "



      rsLineItems.MoveNext



      If rsLineItems.EOF Then



         'Finish the transaction



         strSQL = strSQL & "COMMIT TRAN"



      End If



   Loop



   'Execute the SQL statement



   Set qdfNewOrder = dbCurrent.QueryDefs("qdfNewOrder")



   qdfNewOrder.Connect = strConnect



   qdfNewOrder.SQL = strSQL



   qdfNewOrder.ReturnsRecords = False



   qdfNewOrder.Execute dbFailOnError And dbSQLPassThrough



PassthroughError:



   MsgBox Error$, vbCritical, "Unable to Add Order " _



      & lngOrder_ID



   Exit Sub



End Sub

Regardless of whether you can roll back changes you make to tables, using the BeginTrans. . . CommitTrans structure for bulk changes to tables almost always improves the performance of your application. However, there is substantial risk inherent in using BeginTrans. . . CommitTrans when you can't roll back changes. If your transaction fails, there is no means of determining at what point the failure occurred.

Summary


This chapter covered the basic principles of the design of Visual Basic 4.0 transaction-processing applications using either SQL action queries or Visual Basic code to perform updates on database tables. A sample application, OLTPGrid.vbp, showed you how to write VBA code to take advantage of Visual Basic's new and improved 32-bit data bound controls. The chapter concluded with a discussion of the Visual Basic 4.0 transaction-processing instructions BeginTrans, CommitTrans, and Rollback, which you can use in conjunction with updates to tables in Jet databases, and an example of writing transaction processing code for client/server databases.

The next two chapters, "Integrating Database Front-Ends with OLE 2.1" and "Using OLE Automation with Productivity Applications," deal primarily with interprocess communication (IPC) between Visual Basic database applications that act as OLE 2.1 clients and 32-bit OLE 2.1 servers.

Previous Page Page Top TOC Next Page