Dbexpress Tutorial
Dbexpress Tutorial
Dbexpress Tutorial
by
Martin Rudy
One of the many data connectivity options for the Borland RAD products is dbExpress. This set of drivers and components provide connectivity to databases for the Windows, .NET and Linux platforms. This paper covers an introduction to using the dbExpress technology in both Win32 and .NET. NOTE: Not all of the material in this paper will be covered in the Conference session. The intent is to provide more detail in this paper that can be referenced during the session.
Contents
The major topics covered are:
Data Connectivity Overview dbExpress Introduction Basic Data Connectivity Sending Updates to a Database The Basics Reconciling Errors Master / Detail NestedDataSets Setting UpdateMode and ProviderFlags properties DataSetProvider Properties and Events Empty Field List for Update Statement Summary
the data at runtime, and how to connect the two. This is done using a DataSet, DataSource, and data-aware controls.
Figure 1: Basic requirements to display data in a form Figure 1 shows the dependencies between a form and the data displayed in the form. The data files are defined using a DataSet component. The data can be in a table, the result of a query, or defined by a stored procedure in a SQL database server. The form contains data-aware components that display the data. A DataSource is the component used to connect data-aware components to DataSet components.
dbExpress Introduction
Contents dbExpress is a cross-platform, database-independent set of drivers and components that provide high-performance and a small footprint. It consists of database connectivity technologies introduced in Delphi 6 and Kylix and is one of the data connectivity options in Delphi 2005 for both Win32 and .NET applications. For .NET applications, it is called dbExpress.NET which is a .NET version of the same capabilities found in Delphi 7 dbExpress. Connecting to data requires the database to be identified, the data table(s) and columns selected and a request for the defined data. The major difference between dbExpress and the other RAD data connectivity options is dbExpress only supports unidirectional, readonly cursors for data retrieval. This means no data is buffered and some of the features available in the other data connectivity options are not applicable. The following are some of the features not available with unidirectional cursors: Forward movement only, cannot use Last and Prior methods No editing of data
No filter support No batch moves No heterogeneous queries No cached updates No lookup fields
At first glance you might think dbExpress is not for you, but wait, it is really a more efficient way to retrieve and update data. The technique used by dbExpress to manipulate data is the same as building multi-tier applications using DataSnap (formerly known as MIDAS) you use ClientDataSets. The light-weight, fast dbExpress drivers quickly retrieve data from the database and ClientDataSets provide the ability to modify data, change sort orders, and maintain a change log, support lookups, filtering, plus much more. Using dbExpress as it is designed requires you to perform a fetch of only the data that users need to immediately use, not the entire table. No longer will a SELECT * FROM tablename be sufficient (unless the result set is not too large). ClientDataSets are in-memory tables, all data retrieved, as the result set is stored in the local workstations memory. This basic architecture is called provide/resolve. In Win32, two ini files are used to store information about drivers and database connections. The installed driver types are contained in dbxdrivers. For each driver installed, the required libraries (DLLs for Windows and shared objects in Linux) are listed along with default connection parameter settings. The second ini file used, dbxconnections contains the named configuration connection sets. The components available in dbExpress and dbExpress.NET are essentially the same. Figures 2 and 3 show the Component/Tool Palettes available in Delphi 7 and Delphi 2005. Table 1 lists the components on the dbExpress component palette page and a brief description.
Figure 3: dbExpress components in Delphi 2005 Table 1 dbExpress components Component Name Description SQLConnection Defines a connection to a database, similar to TDatabase SQLDataSet General-purpose unidirectional dataset that executes the SQL statement defined by the CommandText property. This can be a SELECT statement that returns a dataset, a SQL statement that does not return data or executes a stored procedure. SQLQuery Supports SQL statements to be executed that return a unidirectional result set or update data or database schemas. SQLStoredProc Executes a stored procedure. If there is a result set, it is unidirectional. SQLTable Provides unidirectional access to a database table. SQLMonitor Used to intercept and display messages passed between a SQLConnection and database. SimpleClientDataSet Combines a SQLDataSet and DataSetProvider internally in the component to support data cached in memory. The SQLConnection is where the database connectivity is defined. One of the next four components in the list is used to specify the data to be retrieved if you intend to use a unidirectional cursor. If data is to be edited and browsed, the SimpleClientDataSet needs to be used, or a ClientDataSet component which is covered later. In the next section the basics of retrieving data using dbExpress is covered.
With dbExpress you start by using a SQLConnection component. The first step is to define the database connection. This can be done using an existing connection definition, creating a new connection definition, or using the Params property of the SQLConnection to define a connection dynamically. We will start using an existing definition. The ConnectionName property is assigned the name of an existing definition. Connection definitions are stored in an ini file named dbxconnections. This file stores the connection configurations settings and specifies which dbExpress driver to use. The available drivers are maintained in an ini file named drivers. This file contains the DLL or SO name required for connection and the default settings for all the connection parameters. Setting the ConnectionName property can be done by selecting an entry in the drop-down or double-clicking on the SQLConnection component which displays the dbExpress Connection Editor. This editor is essentially the same in both Delphi 7 and 8 as shown in Figures 4 and 5.
Figure 5: dbExpress Connection Editor dialog Delphi 2005 In this dialog you select the database drive, connection name and modify properties used to connect to the database. The Driver Name combo box specifies which connections are displayed in the Connection Name list box. By default, all defined connections are listed. Changing Driver Name value reduces the Connection Name listing to only those for the selected driver. The Connection Settings vary depending on the driver selected. Any of the entries in the Value column can be modified. You cannot enter new Key values or delete any of the existing ones.
Figure 6: CommandText Editor in Delphi 7 and Delphi 2005 One of easiest components to use for displaying data is the DBGrid. It is located on the Data Controls tab. If you attempt to connect a DataSource and a DBGrid to a unidirectional dataset, you get an exception raised with the text Operation not allowed on unidirectional dataset. A grid requires the ability to scroll forwards and backwards through the dataset, which cannot be done with a unidirectional dataset. If you connect other data-aware components (e.g. DBEdit), the exception is not raised until you attempt to go to the prior or last record in the dataset.
Figure 7: Basic dbExpress app in design mode Retrieving data is done when the Active property of the SimpleClientDataSet is set to True. This can be done at design time or when the form is created. In addition to issuing SimpleClientDataSet.Active := True, the Open method (SimpleClientDataSet.Open) can be used on the SimpleClientDataSet which is the equivalent of setting the Active property to True. When Active is set to True, if the SQLConnection has not opened access to the database, it will initiate a connection, attempt to set its Connected property to True. The SQLConnection component also has a LoginPrompt property. By default this property is True, indicating a login dialog will be displayed where a user name and password can be entered before attempting to connect to the database. You can also include those values in the connection parameters and remove the login dialog by setting the LoginPrompt property to False.
Limitations of SimpleClientDataSet
The SimpleClientDataSet is great for quick demos and basic data retrieval. Beyond these two uses, there are limitations that may prevent you from fully using the power of dbExpress. Properties and events of the internal components are not surfaced. This prevents you from creating multi-tier applications, create nested datasets, utilize any of the DataSetProvider features and if an internal SQLConnection is used it cannot be shared with other datasets. A more appropriate approach is to use a ClientDataSet (CDS) and DataSetProvider.
Below is a simple example of how a button can be used to send updates to the database. The value returned by ApplyUpdates is saved in the variable iErrCnt. Since -1 is used as the parameter, there is no limit on the number of errors. All records that can be posted will be committed and all error records will remain in the Data property of the CDS. If the value is > 0 then a message is displayed along with the number of remaining errors.
procedure TfrmMain.pbUpdateClick(Sender: TObject); var iErrCnt: Integer; begin iErrCnt := cdsCustomer.ApplyUpdates(-1); if iErrCnt > 0 then MessageDlg('Problem in apply updates, ' + IntToStr(iErrCnt) + ' error(s).',mtWarning,[mbOK],0); end;
Undoing changes
Changes can be reversed in a CDS to a variety of levels. They include an incremental undo, a record undo, all changes, and refreshing the data currently in the provider. Incremental undo is accomplished using the UndoLastChange method. This method reverts an entire record to it previous set of values. Note, you cannot undo to the fieldlevel, only the record-level. Each call to the method reverts in sequence the previous change to the dataset. There is one parameter used with the UndoLastChange method that is a Boolean data type. When the value is True, the cursor is repositioned on the record that is restored. The cursor remains in its current position when the parameter is False. Below is an example of UndoLastChange where the cursor follows the change.
cdsCustomer.UndoLastChange(True);
Calling RevertRecord can reverse all changes for the current record. This method removes all changes for the current record from the change log. Below is an example for RevertRecord.
cdsCustomer.RevertRecord;
All changes can be canceled using CancelUpdates. When this method is called, every entry in the change log is removed. Before calling CancelUpdates, the ChangeCount property can be used to determine if there are any changes to the dataset. The following is an example of using this property with CancelUpdates.
if cdsCustomer.ChangeCount > 0 then begin if MessageDlg('Are you sure you want to cancel all changes?', mtConfirmation,[mbYes,mbNo],0) = mrYes then begin cdsCustomer.CancelUpdates; cdsCustomer.Refresh; end; end else
In this example, a preemptive test is done before calling Refresh. If there are any changes in the log, the user is prompted to cancel all changes before refreshing the data.
StatusFilter can be an empty set, one of the values, or multiple values. When StatusFilter is an empty set, all records are displayed. If more than one value is used, the combination of the options included in the set determines which records are displayed.
Figure 8: Reconcile Error Dialog option in the Object Repository for Delphi 7
Figure 9: Reconcile Error Dialog option in the Object Repository for Delphi 2005 .NET projects Adding the Reconcile Error Dialog to a project requires you to do three things. First, the unit for the dialog must be removed from the list of auto-created forms for the project, which is done automatically for you when you add the dialog to a project unless you have turned off the auto create feature. Second, you must modify the OnReconcileError event of the ClientDataSet component. Below is an example of the minimum code for the event. The last step is to add the unit for the dialog to the form or data module where the CDS component resides. In the sample code the uses clause in the data module is updated.
procedure TdmMain.cdsCustomerReconcileError(DataSet: TCustomClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction); begin Action := HandleReconcileError(DataSet, UpdateKind, E); end;
The Reconcile Error Dialog is displayed after a call to ApplyUpdates when one or more error records exist. Each record that cannot be posted is displayed in the dialog box, one error at a time. Figure 10 shows an example when there is an attempt to save a record and another user has already made a modification to the record.
Figure 10: Reconcile Error Dialog at runtime The dialog shows errors for records you have inserted, modified and attempted to delete. The Update Type label indicates which of the three actions you took on the record. The grid has a maximum of three columns. They are the value you are attempting to save (Modified Value), the value of the field as it is currently in the database (Conflicting Value) and the value when the client application originally received the data packet (Original Value). The grid changes display based on the type of problem, the update action, and which of the two check boxes at the bottom of the dialog are checked. The first checkbox, Show conflicting fields only, limits the records in the grid to only those fields that have been changed and are in conflict with current database values. Figure 10 shows an example of this selection. If the Show changed fields only checkbox is checked, the fields in all records that have been changed are listed. If there is no conflicting value, the Conflicting Value column contains <Unchanged>. If you attempt to delete a record that has already been deleted by another user, only the Modified Value column is displayed. The radio buttons in the upper right provide the type of action you can with the error record. Table 3 contains a description that is directly from the Delphi Help system. Table 3 Action options in the Reconcile Error Dialog Value Skip Description Skips updating the record that raised the error condition, and leaves the
unapplied changes in the change log. Abort Merge Correct Cancel Refresh Aborts the entire reconcile operation. Merges the updated record with the record on the server. Replaces the current updated record with the value of the record in the event handler. Backs out all changes for this record, reverting to the original field values. Backs out all changes for this record, replacing it with the current values from the server.
The action for each error can be set before clicking the OK button. Based on the type of error, you can determine to leave the record in the change log, cancel it, merge the updated record, or abort the entire reconcile operation.
Figure 11: Nested dataset master / detail setup in design mode The first step to setup up master / detail relationship is to select two unidirectional dataset components. In this example, two SQLDataSet components are used to define which data to retrieve. A DataSource component is also needed to link the two datasets. The first SQLDataSet (named sdsCustomer) retrieves all the records from the Customer table using select * from Customer as the SQL statement. The DataSource component (named dsCust) uses the SQLDataSet for the Customer data as its DataSet. The second dataset (named sdsSales) is used for the detail data from the Sales table. To link the two SQLDataSet components, the DataSource property of the Sales SQLDataSet component is used along with the WHERE clause of the SQL statement. The DataSource property is set to dsCust, the name of the DataSource component for the Customer table. This defines the dataset to use as the master data. The linkage between the two tables is defined in the SQL property using the WHERE clause as follows: SELECT * FROM Orders WHERE CUST_NO = :cust_no In this example, all of the columns from the Sales table are selected. The rows are limited to the value in the cust_no parameter. The key to using this technique is the parameter names must match the names of the linking fields in the master table. In this example, CUST_NO is the field name in both tables. Therefore, :cust_no is used as the parameter in the WHERE clause. Lowercase characters are used to visually seeing a difference between the field name and parameter name, but they are not required. The value of
:cust_no is automatically updated each time the master record changes which in turn generates a new SQL statement and the appropriate detail data is returned. Viewing the data with ClientDataSets only requires a single TDataSetProvider. It is linked to the SQLDataSet for the master table. No provider is required for the detail data. The data for the Sales table will automatically be made available by the nesting of the detail data as a separate field for each master record. The major difference in setting up master / detail using nested datasets is in setting the ClientDataSet properties. Two CDS components are required. The first ClientDataSet, used as the master table, is linked directly to the provider. An additional step is required to create TField descendants for the fields in the master dataset. When this is done, a new field is listed that does not exist in the Customer table. Figure 12 shows the TFields Editor for the Customer CDS. The additional field contains the detail records for the Sales table. It is a TDataSetField descendant. The name of this field is the same as the name of the SQLDataSet.
Figure 12: Fields Editor dialog box A second CDS is used for the detail data. Instead of linking the CDS used for the detail data to a provider, the DataSetField property of the ClientDataSet is used. This property is set to the TDataSetField instantiated in the master data Fields Editor. When both CDS components are opened the data for the Customer and Sales are displayed. Figure 13 shows an example of using nested datasets in a DBGrid.
Figure 13: Visual display for nested datasets In the DBGrid the column for the detail dataset is displayed similar to memo and graphic data. The text (DATASET) represents the values for each record in the dataset. An ellipsis button is displayed with a double-click or F2 keypress. Clicking the button displays another window containing the detail data displayed in a grid. This window will always stay on top. You can also see in Figure 13 that the bottom grid in the form displays the same data. You are not required to include the DataSetField in the master table. The TField Visible property can be set to False and, using a second CDS and DataSource, all detail can be displayed in a grid. Another difference when ClientDataSets are used instead of SQLClientDataSets is in saving the data. The Save Customer button on the form is the same as in example projects using SQLClientDataSets. When changes are made here and the ApplyUpdates is executed, all modifications for both the master and detail data sets are wrapped into a single transaction and committed or rolled back based on the parameter passed in the ApplyUpdates method.
Careful consideration should be given to the value selected for the UpdateMode. Each setting has its good and bad side. The default, upWhereAll, is the most restrictive and has the worst performance. Using all the fields in the WHERE clause to locate the original record ensures the record has not been changed since it was originally read. If you get an error message stating the query is too complex when you attempt an update, the SQL database is indicating it cannot handle a WHERE clause with all the fields in the table being used. The upWhereKeyOnly is the least restrictive. This allows anyone to change any field (except the primary key) without consideration of what the original values where. In between these two property options is the upWhereChanged. This can be a problem where the values in multiple fields taken together have a specific meaning, like in a multi-field primary key where users are allowed to change the values. Having a good database design and a well thought out set of business rules assist in determining the correct UpdateMode setting. Testing this feature can be done by changing the UpdateMode property and running two instances of an application making modifications in each. The ProviderFlags properties for TField components are also available to use in determining how an update is to be processed. Table 5 lists the possible options.
Table 4 UpdateMode property settings Value pfInUpdate pfInWhere pfInKey pfHidden Description Field is included in update Field is used in finding the original record to be updated Field is used in finding the current record after an update fails Field is included in the data packet to ensure uniqueness of the record. The field is used to find the original record to update. The field is not visible to the application.
Each field in the dataset used by the provider can set which of the ProviderFlags are applicable. Changing the ProviderFlags modifies the SQL UPDATE and DELETE statements created by the provider to change and remove records from the database. To demonstrate the impact on updating data and the usage of the UpdateMode and ProviderFlags properties, the following SQL statement will be used with the InterBase Employee database. This example is in the ProviderFlags projects which is using dbExpress.
SELECT CUST_NO, CUSTOMER, CONTACT_FIRST, CONTACT_LAST, PHONE_NO, ADDRESS_LINE1, ADDRESS_LINE2, CITY, STATE_PROVINCE, COUNTRY, POSTAL_CODE, ON_HOLD, ((CITY || ',') || STATE_PROVINCE) AS CITYSTATE FROM CUSTOMER
All of the fields from the Customer table are returned and a calculated field combining the City and State_Province field which is named CityState. Any attempt to modify an existing record or delete a record generates an error with the message Column unknown CITYSTATE. The problem is in the WHERE clause that is generated. Below is the SQL generated for deleting a record. The dbExpress SQLMonitor is used in the project to track SQL statements.
delete from CUSTOMER where CUST_NO = ? and CUSTOMER = ? and CONTACT_FIRST = ? and CONTACT_LAST = ? and PHONE_NO = ? and ADDRESS_LINE1 = ? and ADDRESS_LINE2 is null and CITY = ? and STATE_PROVINCE = ? and COUNTRY = ? and POSTAL_CODE = ? and
The WHERE clause includes all fields listed in the SELECT statement, including CITYSTATE. This DELETE will always fail because of the calculated field, CITYSTATE is not a field in the base Customer table. Changing the UpdateMode property from the default of upWhereAll to upWhereChanged or upWhereAll prevents this type of error depending on the type of modification performed. Either option will eliminate the column unknown error when modifying an existing record. Below is an example of the SQL generated to update the Contact_First field and UpdateMode is set to upWhereChanged. The WHERE clause includes the primary key (Cust_No) and all fields changed, in this example just Contact_First.
update CUSTOMER set CONTACT_FIRST = ? where CUST_NO = ? and CONTACT_FIRST = ?
Using upWhereChanged when attempting to delete a record still causes the error. This is due to the inclusion of all fields in the WHERE clause. To get a delete to work, you need to set the UpdateMode to upWhereKeyOnly. With this setting, only the Cust_No field will be used in the WHERE clause as shown below.
delete from CUSTOMER where CUST_NO = ?
An alternative to solving the CityState unknown column problem is to use the TField ProviderFlags. The dataset used by the provider are the TFields that need to be changed. Setting all the ProviderFlags to False for CityState prevents its usage in the WHERE clause for any SQL statement. An UPDATE statement where the Contact_First is modified now is as follows:
update CUSTOMER set CONTACT_FIRST = ? where CUST_NO = ? and CUSTOMER = ? and CONTACT_FIRST = ? and CONTACT_LAST = ? and PHONE_NO is null and ADDRESS_LINE1 = ? and ADDRESS_LINE2 is null and CITY = ? and STATE_PROVINCE = ? and COUNTRY = ? and POSTAL_CODE = ? and ON_HOLD is null
The ProviderFlags can also be used to reduce the field used in the WHERE clause, even to the point of specifying just the primary key.
two-tier application, this same technique can be used but the code resides in the client application. In reality, you will use constraints at all three levels. Some of the database-server level constraints can be passed directly to client applications from the server. You can also have application server constraints available in the client application each time a connection is made. This provides the best of all three levels. For this section, the EMPLOYEE.GDB InterBase database that ships with Delphi is used as the data source. Most of the examples use the SALES table. The server and client projects are found in the BusRulesCnstrnts directory of the code examples.
CustomConstraint
TField properties reduces the amount of coding required to support additional constraints not supplied by the database server and allow for ease in setting custom error messages. Error messages displayed based on violation of the properties differ based on the data type and what property is set. The following are a few notes that you should be aware of when using these properties: Required property is set to True when a TTable is used and the database schema specifies a field cannot contain a null. It will be set to False if a TQuery is used as the dataset. Required and ReadOnly properties set on the application server are copied to the client persistent TFields on the initial TField creation in the client. If you change the TField settings for these two fields on the server, you will need to either manually change the values for the CDS TFields or delete and re-add them to the CDS. When the Required property is set to True, you will get one of two error messages displayed when the record is saved and no value exists in the field: 1. Field value required Required property is set to True on the server No persistent TField for CDS or field allows null values and you manually set Required to True 2. Field <fldName> must have a value Field cannot contain null values as defined in table create Unless the Required property is explicitly set to False in the CDS If the Required property is set to True on the CDS, but the table definition allows nulls or the server TField has Required set to False If you want field-level validation, you need to set the CustomConstraint property. The message displayed will be the value of the ConstraintErrorMessage property.
specified in the DataSet property of the TDataSetProvider. The following are the TField properties that can be used and are passed to the client based on the TField data type:
o o o o o o o o o o
Alignment Currency DisplayFormat DisplayLabel DisplayWidth EditFormat EditMask MaxValue MinValue Visible
The poReadOnly setting prevents any modifications to the data. No visible indicator or message is given to the user; the data just cannot be modified. When poDisableInserts, poDisableEdits, or poDisableDeletes are set to True, an exception is raised when users attempt to insert, modify, or delete records. The exception message that is displayed contains the name of the CDS and Inserts are not allowed for Inserts, Modifications are not allowed for edits, and Deletes are not allowed for deletes. If you do not want to have the exception dialog displayed, you need to trap for the exception and change how the error is displayed. For example the OnEditError or OnDeleteError events can be used for edits and deletes. In the client application, there
are events for the OnEditError and OnDeleteError that exist in the code but the events have not been assigned. You can assign them yourself to see how you might trap for the error and display a customized message.
In this section each of the three events are covered with some simple examples on how they could be used.
Using BeforeUpdateRecord
The BeforeUpdateRecord event is used when you want to validate data on an individual record basis before changes are applied to the database. It can also be used to modify the client data before being saved. Below are the procedure parameters and a brief description of what is passed to the event:
BeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean);
Sender SourceDS DeltaDS UpdateKind TDataSetProvider the triggered the event the source data data packet from client type of update: ukInsert, ukModify, ukDelete
Applied indicator that you set if you apply the update in the BeforeUpdateRecord code yourself
Some dos and donts for this event: Dont use Edit and Post methods to modify and save data in the record Use TField NewValue property to change the value of a field Use TField OldValue to get original value Use VarIsNull(Field.NewValue) to determine if the field is NULL Use VarIsEmpty(Field.NewValue) to determine if the field has not changed since it was retrieved
In the demo application, the BeforeUpdateRecord is used to validate the order date field. If the order date is not empty, its value is compared to the ship date to ensure the order date is not after the ship date. To simplify the example, only this condition is checked, there is no check to see if the ship date is modified. The code below shows the procedure used.
procedure TBusRulesContstraints.dspSalesBeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean); begin { NOTE: This example is only checking for changes to Order Date. } if UpdateKind <> ukDelete then { Check to see if Order Date is NULL using VarIsNull } if ((VarIsNull(DeltaDS.FieldByName('ORDER_DATE').NewValue) = False) and { Check to see if Order Date has changed using VarIsEmpty } (VarIsEmpty(DeltaDS.FieldByName('ORDER_DATE').NewValue) = False)) then if DeltaDS.FieldByName('ORDER_DATE').NewValue > DeltaDS.FieldByName('SHIP_DATE').OldValue then raise Exception.Create('Order Date cannot be after ' + 'Ship Date. <BeforeUpdateRecord>'); end;
The first comparison is to see if the action on the record was either an insert or update, there is no need to validate if the record is to be deleted. The UpdateKind parameter is used to see if the action to be taken is not ukDelete (meaning either an insert or update is to be performed). If this is true, the NewValue property of the ORDER_DATE TField is used. NewValue is unassigned when no modification is made to the field. VarIsNull and VarIsEmpty are used to ensure the value is assigned and not null. If this condition is true, the OldValue property of the SHIP_DATE TField is used to compare to the updated ORDER_DATE value. If the order date is after the ship date, an exception is raised. In this demo, OldValue is used because there is no checking to see if the SHIP_DATE field has also changed. In a complete validation, you would need to check to see if both fields changed. OldValue will always be the value that is currently in the client field. You cannot use the Value property. This property will be null when there is no change to the field, therefore the need for the OldValue property.
Unlike the BeforeUpdateRecord event, there is no parameter that indicates the type of update. Since you have the entire dataset, you need to look at the status for each individual record. To determine the status for the record, UpdateStatus, a property of TClientDataSet is used. Table 8 lists the possible values for UpdateStatus with a brief description. Table 8 - UpdateStatus options and their description Value usUnmodified usInserted usModified usDeleted Description Record has not been modified. Record is an insert. Record has been modified. Note: this record is the second of a matching pair. The first will have a status of usUnmodified. Record to be deleted.
Before looking at a code example of the OnUpdateData event, the contents of what is passed should be covered. A change log is maintained by the CDS for each insert, update, and delete. The CDS Delta property contains all records in the change log. A separate record is added to the log for each insert and delete. When an existing record is modified, two records are entered in the log. The first record, with a status of usUnmodified, contains all field values for the record before any modification was made. The second record, with a status of usModified, contains only the field values that have changed. All non-modified fields are null in the second record. The CDS Delta property is what the provider receives as the DataSet property in the OnUpdateData event.
In the demo client project, the second tab displays the contents of the change log. Figure 14 shows the log after performing an edit, insert, delete, and a second edit to a different record. The UpdateStatus field does not exist in the data, it is a calculated field used to display the status of each record.
Figure 14: Client form displaying change log The first two records are a matching pair. The first record contains all the values of the record before any changes were made. The second record, with an UpdateStatus of Modified, contains the values for every field in the record that changed. In this example, the SHIP_DATE field is changed from 3/6/1991 to 3/7/1991. The next two records are for an inserted and deleted record. On an insert, any field where data is entered is placed in the change log. For deleted records, the entire original field values are placed in the log. The last two records are again a matching pair. Here two fields are changed, the SALES_REP and the SHIP_DATE. Displaying the change log requires an extra CDS in the application and a small amount of code. The code that is used in the demo client is as follows:
procedure TfrmMain.PageControl1Change(Sender: TObject); begin if PageControl1.ActivePage = tbsDelta then with dmMain do try cdsSalesDelta.Close; cdsSalesDelta.Data := cdsSales.Delta; cdsSalesDelta.Open; except MessageDlg('No delta records exist.',mtWarning,[mbOK],0); end; end;
The CDS cdsSales contains the data from the provider. The CDS for showing the change log is named cdsSalesDelta. When the second tab is selected, the Delta property of cdsSales is assigned to the Data property of cdsSalesDelta. The try except block is used to display a simple message when no modification has been made to the data. To demonstrate the use of OnUpdateData, a similar validation used in the BeforeUpdateRecord on the ORDER_DATE field is replicated. The code for the demo OnUpdateData event is shown below.
procedure TBusRulesContstraints.dspSalesUpdateData(Sender: TObject; DataSet: TClientDataSet); var Old_ShipDate: TDateTime; begin with DataSet do begin First; while not EOF do begin if UpdateStatus = usUnmodified then { In this demo, only the modified records are being evaluated. There will be two records in the data packet for each modification. The first record is the values of all fields in the record before any changes are made. The second record of the pair contains fields that are modified. Any field that did not change is null. } begin { Save the old Ship_Date field from the first record to compare to the modified Order_Date field. } Old_ShipDate := DataSet.FieldByName('SHIP_DATE').NewValue; Next; { Check to see if the modified record has Order Date modified. If so, then validate the difference and raise an exception if invalid. } if not VarIsEmpty(DataSet.FieldByName('ORDER_DATE').NewValue) then if FieldByName('ORDER_DATE').NewValue > Old_ShipDate then raise Exception.Create('Order Date cannot be after Ship Date. ' + '<OnUpdateData>'); end; Next; // Go the next record in the delta packet end; end; end;
A while loop is required to process the entire dataset. You start at the first record and check its status. If it is a non-modified record, you know that this record is the first of a matching pair. The first record contains the original record values; therefore the SHIP_DATE needs to be saved. To get the updated field values, Next is called for the
dataset to move the modified record. If the ORDER_DATE NewValue is not empty, then compare it to the saved SHIP_DATE value. An exception is raised if the order date is after the ship date. In this example, only modified records are checked. If a record is to be deleted or inserted, no validation is done. The while loop forces all records to be checked until EOF or the exception is raised.
OnUpdateError(Sender: TObject; DataSet: TClientDataSet; E: EUpdateError; UpdateKind: TUpdateKind; var Response: TResolverResponse);
Sender DataSet E UpdateKind Response TDataSetProvider that triggered the event temporary dataset to access error record exception object type of update what action to take on the error when the event exits
Like the previous two events, you use NewValue and OldValue TField properties. You also use the CurValue property, which indicates the current value in the database. This allows you to see the currently stored value, the original value the client received, and the updated value the client wants to apply. The exception parameter has a property named OriginalException. It allows you to get the original exception class. If you are using the BDE and the original exception class is EDBEngineError you can use ErrorCode property to get the error code value, otherwise you have to parse the message. It is important to note that you should not change the current record pointer in the OnUpdateError event.
The Response parameter has a different default value depending on the parameter supplied in the ApplyUpdates. If the maximum number of errors allowed is zero, Response defaults to rrAbort otherwise rrSkip is assigned the default. In the demo server application, OnUpdateError is used to change the error messages returned from the database server. With Interbase, there is no specific error number to evaluate for each individual error. You need to look at the error text and determine from it what error is raised. Two examples are used in the demo: INTEG_65 is the error when the ORDER_STATUS field does not equal new, open, shipped, or waiting and INTEG_67 is the error when the order date is after the ship date. The error message General SQL error. Operation violates CHECK constraint INTEG_67 on view or table SALES is the default error message for an order date that is after the ship date. Most users would not be able to identify the problem with the database error message. To improve the information displayed in the error messages, the code below is used for the OnUpdateError event:
procedure TBusRulesContstraints.dspSalesUpdateError(Sender: TObject; DataSet: TClientDataSet; E: EUpdateError; UpdateKind: TUpdateKind; var Response: TResolverResponse); begin if E.OriginalException is EDatabaseError then begin if Pos('INTEG_65',E.OriginalException.Message) <> 0 then E.Message :=('Order Status must be new, open, ' + 'shipped, or waiting <OnUpdateError>') else if Pos('INTEG_67',E.OriginalException.Message) <> 0 then E.Message := ('Order Date cannot be after Ship Date. ' + '<OnUpdateError>') else E.Message := (E.OriginalException.Message + '<OnUpdateError>'); end; end;
The OriginalExcpetion property of the E parameter is first checked to determine if an EDatabaseError occurred. If this is true, the Message property is searched for a unique text string matching each of the database errors where a different error message is to be displayed. Using the Interbase integrity identifiers (INTEG_65 and INTEG_67) a distinction can be made. If any other database error occurs that database error message is displayed. Replacing the contents of the Message property of the EUpdateError parameter fully supports the reconcile error dialog box from the Object Repository.
Contents Some application requirements dictate the need to have fields available for input or assignment on the client but the data in the fields do not update the database. These fields can be created on the client side as internal calculated fields or they can be created as calculated fields in the SQL statement retrieving the data. If these fields are the only data that is modified in the CDS, executing ApplyUpdates results in a datapacket sent to the database but there are no fields to place in the UPDATE statement which results in the following error reconcile dialog message using SQL Server: Incorrect syntax near 'se' Running the InterBase DSP_Demo project generates the following error message in a reconcile error dialog: Token unknown - line 2, char -1 where The real issue is the SQL that is generated by the DSP. The first part of the statement is as follows: update Session se where The DataSetProvider attempts to create an UPDATE statement but the SQL statement generated is incorrect and fails to execute. This empty field list generates an exception and causes the update to fail. Even if you exclude the fields using ProviderFlags, the error still persists. The solution to this situation is to use the BeforeUpdateRecord event and cycle through all the fields in the datapacket for each record and exclude the record from generating an update when no change is made to the actual data fields in the table. Additional tables were added to the DBDEMOS database to be used for this and the next example. These tables represent basic data for sessions at BorCon. Figure 15 shows the tables and data types. Table 5 lists each table and how they are used.
Figure 15: Data tables used for final two topics Table 5 Table name and description for example Table Attendee Speaker AttendeeSession TrackType Session SessionRoom RoomList Description Attendee names and id Conference speaker list Sessions selected for an attendee Tracks offered (e.g. Delphi, JBuilder, StarTeam, CalilberRM, etc) Available sessions, title, speaker, room, time, session type Rooms used by each session List of available rooms at the Convention Center
NOTE: The design of these tables was for demonstration purposes only and do not represent the exact relationships required to fully support all possible session, speaker and attendee relationships. For example, each session only supports a single speaker and is assigned to a single track. Only a subset of the data from the Conference was entered, just enough to use in demonstrating the technique being demonstrated. The code for this example is found on the Empty Update tab of the project shown in Figure 16. The requirement here is to display all the sessions and which room or rooms they are going to use in the Convention Center. The RoomList table has RoomNo and RoomName fields. The actual names used are A1, B1, etc. For BorCon, this is a straightforward naming convention. In some hotels/convention centers the names of the rooms are names like Rainer, Whidbey, or other non-numerical names. The intent of the example is to show how you would support the saving of the RoomNo in the data but display the room name in the form.
Figure 16: Empty Update tab The SQL to retrieve the data from the Sessions table and create the calculated field is as follows: SELECT S.*, CAST(NULL as VARCHAR(100)) as RoomNames FROM Session S Once the data is retrieved, the AfterOpen event on the ClientDataSet is used to translate the room numbers into the full room description. The data is placed into the RoomNames field.
Modifications to the rooms used by a session are done by clicking the ellipses button for the row in the grid. This displays a dialog box where the rooms are selected and room name list is placed into the RoomNames field and the room number list is assigned to the RoomIds field. Clicking the Open button on the Empty Update tab retrieves the data. If you immediately click the ApplyUpdates the reconcile error dialog is displayed with the error message Line 1: Incorrect syntax near 'se'. This is caused by the changes created in the CDS AfterOpen. To prevent this error, code needs to be added to the DSP BeforeUpdateRecord. Clicking the Enable BUR checkbox assigns the following code to the DSP.
procedure TdmEmptyUpdate.dspCustomerBeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean); var bAllowApply: Boolean; i: Integer; begin if UpdateKind = ukModify then begin bAllowApply := False; for i := 0 to DeltaDS.FieldCount - 1 do bAllowApply := bAllowApply or ((not VarIsClear(DeltaDS.Fields[i].NewValue)) and (pfInUpdate in DeltaDS.Fields[i].ProviderFlags)); Applied := not bAllowApply; end; {NOTE: This is from an example by Jeff Overcash on TeamB } end;
It should be first noted that this technique is based on an example from the Delphi forums by Jeff Overcash. Jeff supplied this idea to a question raised on how to create a numeric field that allows entry in the CDS. The basic technique is to cycle through all the fields in the datapacket (DeltaDS) and determines if the field is to be updated (pfInUpdate in the ProviderFlags property) and the NewValue is assigned. If all fields are checked and there are no fields updated, then there is no need to create an update and the Applied parameter is set to True. An alternative to this method is to turn logging off before setting the RoomNames values, modify the data, then turn logging back on. This is done by setting the CDS LogChanges property to False, make the changes, then set LogChanges to True. This disables all logging while RoomNames is assigned. This technique can also be used when the SQL statement used to retrieve data in a one-toone relationship or a many-to-one relationship. It is possible that fields from the one side of the relationship are updated thus cause the same condition as described above. This same technique can be used to determine the base table has no updates but the joined
data has to be updated with an INSERT, DELETE, or UPDATE statement generated by your code.
Summary
Contents dbExpress and dbExpress.NET allow both Win32 and .NET Borland developers to easily connect to many of the most popular databases used today. The power of both the DataSetProvider and ClientDataSet enable custom control over the data retrieval and data modification to support the needs of todays application requirement.