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.
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.
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.
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.
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 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.
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:
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:
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" propositionall 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
driverssuch as Intersolv's ODBC drivers for dBASE, Paradox, and FoxPro databasessupport 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.
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
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.
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.
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.
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.
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.
[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
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
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:
dtcForm.Recordset.FindFirst "Name = '" & txtFind.Text & "*'"
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.
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 transactionyour 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.
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:
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.
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
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.
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.