Chapter 15
Using provider components

Provider components (TDataSetProvider) supply the mechanism by which client datasets obtain their data (unless they are using flat files). Providers are responsible for packaging data into data packets that are then sent to client datasets and applying updates received from client datasets. Usually they reside on an application server as part of a multi-tiered application, but they can appear as part of the same application as the client dataset (or XML broker). Providers work in conjunction with resolver components that handle the details of resolving data to the database or dataset.

Most of the work of a provider component happens automatically. You need not write any code on the provider to create a fully functioning application server. However, provider components include a number of events and properties that allow your application more direct control over what information is packaged for clients and how your application responds to client requests.

This chapter describes how to use a provider component to control the interaction with client applications.

Determining the source of data

When you use a provider component, you must specify a dataset that it can use to get the data it assembles into data packets. To do this, set the DataSet property of the provider to the name of the dataset to use. At design time, select from available datasets in the DataSet property drop-down list in the Object Inspector.

TDataSetProvider can work with any dataset that supports the IProviderSupport interface. This interface is introduced by TDataSet, so it is available for all datasets. However, the IProviderSupport methods implemented in TDataSet are mostly stubs that don't do anything or that raise exceptions. Most of the dataset classes that ship with Delphi (BDE-enabled datasets, ADO-enabled datasets, Client datasets, and InterBase Express components) override these methods to implement the IProviderSupport interface in a more useful fashion.

Note: Because the provider relies on an interface belonging to the dataset, it has no specific dependencies on the data access mechanism (BDE, DBOLE, or some other mechanism). These dependencies all fall to the dataset that the provider uses.

Component writers that create their own custom descendants from TDataSet must override all appropriate IProviderSupport methods if their datasets are to work in an application server. If the provider only provides data packets on a read-only basis (that is, if it does not apply updates), the IProviderSupport methods implemented in TDataSet may be sufficient.

Choosing how to apply updates

By default, when TDataSetProvider components apply updates and resolve update errors, they communicate directly with the database server using dynamically generated SQL statements. This approach has the advantage that your server application does not need to merge updates twice (first to the dataset, and then to the remote server).

However, you may not always want to take this approach. For example, you may want to use some of the events on the dataset component. Alternately, the dataset you use may not support the use of SQL statements (for example if you are providing from a TClientDataSet component).

TDataSetProvider lets you decide whether to apply updates to the database server using SQL or to the source dataset by setting the ResolveToDataSet property. When this property is True, updates are applied to the dataset. When it is False, updates are applied directly to the underlying database server.

Controlling what information is included in data packets

There are a number of ways to control what information is included in data packets that are sent to and from the client. These include

Specifying what fields appear in data packets

To control what fields are included in data packets, create persistent fields on the dataset that the provider uses to build the packets. The provider then includes only these fields. Fields whose values are generated dynamically on the server (such as calculated fields or lookup fields) can be included, but appear to client datasets on the receiving end as static read-only fields. For information about creating persistent fields, see "Creating persistent fields".

If the client dataset will be editing the data and applying updates to the application server, you must include enough fields so that there are no duplicate records in the data packet. Otherwise, when the updates are applied, it is impossible to determine which record to update. If you do not want the client dataset to be able to see or use extra fields provided only to ensure uniqueness, set the ProviderFlags property for those fields to include pfHidden.

Note: Including enough fields to avoid duplicate records is also a consideration when using queries on the application server. The query should include enough fields so that records are unique, even if your application does not use all the fields.

Setting options that influence the data packets

The Options property of the provider component lets you specify when BLOBs or nested detail tables are sent, whether field display properties are included, what type of updates are allowed, and so on. The following table lists the possible values that can be included in Options.

Table 15.1   Provider options

Value

Meaning

poFetchBlobsOnDemand

BLOB field values are not included in the data packet. Instead, client applications must request these values on an as-needed basis. If the client dataset's FetchOnDemand property is True, the client requests these values automatically. Otherwise, the client application uses the client dataset's FetchBlobs method to retrieve BLOB data.

poFetchDetailsOnDemand

When the provider represents the master of a master/detail relationship, nested detail values are not included in the data packet. Instead, client applications request these on an as-needed basis. If the client dataset's FetchOnDemand property is True, the client requests these values automatically. Otherwise, the client application uses the client dataset's FetchDetails method to retrieve nested details.

poIncFieldProps

The data packet includes the following field properties (where applicable): Alignment, DisplayLabel, DisplayWidth, Visible, DisplayFormat, EditFormat, MaxValue, MinValue, Currency, EditMask, DisplayValues.

poCascadeDeletes

When the provider represents the master of a master/detail relationship, detail records are deleted by the server automatically when master records are deleted. To use this option, the database server must be set up to perform cascaded deletes as part of its referential integrity.

poCascadeUpdates

When the provider represents the master of a master/detail relationship, key values on detail tables are updated automatically when the corresponding values are changed in master records. To use this option, the database server must be set up to perform cascaded updates as part of its referential integrity.

poReadOnly

The client dataset can't apply updates to the provider.

poAllowMultiRecordUpdates

A single update can cause more than one record of the underlying database table to change. This can be the result of triggers, referential integrity, custom SQL statements, and so on. Note that if an error occurs, the event handlers provide access to the record that was updated, not the other records that change in consequence.

poDisableEdits

Clients can't modify existing data values. If the client tries to edit a field an exception is raised. (This does not affect the client's ability to insert or delete records).

poDisableInserts

Clients can't insert new records. If the client tries to insert a new record an exception is raised. (This does not affect the client's ability to delete records or modify existing data)

poDisableDeletes

Clients can't delete new records. If the client tries to delete a record an exception is raised. (This does not affect the client's ability to insert or modify records)

poNoReset

Clients can't specify that the provider should reposition the cursor on the first record before providing data.

poAutoRefresh

The provider refreshes the client dataset with current record values whenever it applies updates.

poPropogateChanges

Changes made by the server to updated records as part of the update process are sent back to the client and merged into the client dataset.

poAllowCommandText

The client can override the associated dataset's SQL text or the name of the table or stored procedure it represents.

Adding custom information to data packets

Providers can send application-defined information to the data packets using the OnGetDataSetProperties event. This information is encoded as an OleVariant, and stored under a name you specify. Client datasets in the client application can then retrieve the information using their GetOptionalParam method. You can also specify that the information be included in delta packets that the client dataset sends when updating records. In this case, the client application may never be aware of the information, but the server can send a round-trip message to itself.

When adding custom information in the OnGetDataSetProperties event, each individual attribute (sometimes called an "optional parameter") is specified using a variant array that contains three elements: the name (a string), the value (a Variant), and a boolean flag indicating whether the information should be included in delta packets when the client applies updates. Multiple attributes can be added by creating a variant array of variant arrays. For example, the following OnGetDataSetProperties event handler sends two values, the time the data was provided and the total number of records in the source dataset. Only information about the time the data was provided is returned when clients apply updates:

procedure TMyDataModule1.Provider1GetDataSetProperties(Sender: TObject; DataSet: TDataSet; 
out Properties: OleVariant);
begin
  Properties := VarArrayCreate([0,1], varVariant);
  Properties[0] := VarArrayOf(['TimeProvided', Now, True]);
  Properties[1] := VarArrayOf(['TableSize', DataSet.RecordCount, False]);
end;

When the client applies updates, the time the original records were provided can be read in the provider's OnUpdateData event:

procedure TMyDataModule1.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet);
var
  WhenProvided: TDateTime;
begin
  WhenProvided := DataSet.GetOptionalParam('TimeProvided');
  ...
end;

Responding to client data requests

In most multi-tiered applications, client requests for data are handled automatically. A client dataset requests a data packet by calling GetRecords (indirectly, through the IAppServer interface). The provider responds automatically by fetching data from the associated dataset, creating a data packet, and sending the packet to the client.

The provider has the option of editing data after it has been assembled into a data packet but before the packet is sent to the client. For example, the provider might want to encrypt sensitive data before it is sent on to the client, or to remove records from the packet based on some criterion (such as the user's role in an MTS application).

To edit the data packet before sending it on to the client, write an OnGetData event handler. The data packet is provided as a parameter in the form of a client dataset. Using the methods of this client dataset, data can be edited before it is sent to the client.

As with all method calls that are made through the IAppServer interface, the provider has an opportunity to communicate persistent state information with the client application before and after the call to GetRecords. This communication takes place using the BeforeGetRecords and AfterGetRecords event handlers. For a discussion of persistent state information in application servers, see "Supporting state information in remote data modules".

Responding to client update requests

A provider applies updates to database records based on a Delta data packet received from a client application. The client requests updates by calling the ApplyUpdates method (indirectly, through the IAppServer interface).

As with all method calls that are made through the IAppServer interface, the provider has an opportunity to communicate persistent state information with the client application before and after the call to ApplyUpdates. This communication takes place using the BeforeApplyUpdates and AfterApplyUpdates event handlers. For a discussion of persistent state information in application servers, see "Supporting state information in remote data modules".

When a provider receives an update request, it generates an OnUpdateData event, where you can edit the Delta packet before it is written to the dataset or influence how updates are applied. After the OnUpdateData event, the provider uses its associated resolver component to write the changes to the database.

The resolver component performs the update on a record-by-record basis. Before the resolver applies each record, it generates a BeforeUpdateRecord event on the provider, which you can use to screen updates before they are applied. If an error occurs when updating a record, the resolver calls the provider's OnUpdateError event handler to resolve the error. Usually errors occur because the change violates a server constraint or the database record was changed by a different application subsequent to its retrieval by this client application, but prior to the client's request to apply updates.

Update errors can be processed by either the application server or the client. Application servers should handle all update errors that do not require user interaction to resolve. When the application server can't resolve an error condition, it temporarily stores a copy of the offending record. When record processing is complete, the application server returns a count of the errors it encountered to the client dataset, and copies the unresolved records into a results data packet that it passes back to the client for further reconciliation.

The event handlers for all provider events are passed the set of updates as a client dataset. If your event handler is only dealing with certain types of updates, you can filter the dataset on the update status of records so that your event handler does not need to sort through records it won't be using. To do this, set the StatusFilter property of the client dataset.

Note: Applications must supply extra support when the updates are directed at a dataset that does not represent a single table. For details on how to do this, see "Applying updates to datasets that do not represent a single table".

Editing delta packets before updating the database

Before the provider applies updates to the database, it generates an OnUpdateData event. The OnUpdateData event handler receives a copy of the Delta packet as a parameter. This is a client dataset.

In the OnUpdateData event handler, you can use any of the properties and methods of the client dataset to edit the Delta packet before it is written to the dataset. One particularly useful property is the UpdateStatus property. UpdateStatus indicates what type of modification the current record in the delta packet represents. It can have any of the values in Table 15.2.

Table 15.2   UpdateStatus values

Value

Description

usUnmodified

Record contents have not been changed

usModified

Record contents have been changed

usInserted

Record has been inserted

usDeleted

Record has been deleted

For example, the following OnUpdateData event handler inserts the current date into every new record that is inserted into the database:

procedure TMyDataModule1.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet);
begin
  with DataSet do
  begin
    First;
    while not Eof do
    begin
      if UpdateStatus = usInserted then
      begin
        Edit;
        FieldByName('DateCreated').AsDateTime := Date;
        Post;
      end;
      Next;
    end;
end;

Influencing how updates are applied

The OnUpdateData event also gives your provider a chance to indicate how records in the delta packet are applied to the database.

By default, changes in the delta packet are written to the database using automatically generated SQL UPDATE, INSERT, or DELETE statements such as

UPDATE EMPLOYEES
  set EMPNO = 748, NAME = 'Smith', TITLE = 'Programmer 1', DEPT = 52
WHERE
  EMPNO = 748 and NAME = 'Smith' and TITLE = 'Programmer 1' and DEPT = 47

Unless you specify otherwise, all fields in the delta packet records are included in the UPDATE clause and in the WHERE clause. However, you may want to exclude some of these fields. One way to do this is to set the UpdateMode property of the provider. UpdateMode can be assigned any of the following values:

Table 15.3   UpdateMode values

Value

Meaning

upWhereAll

All fields are used to locate fields (the WHERE clause).

upWhereChanged

Only key fields and fields that are changed are used to locate records.

upWhereOnly

Only key fields are used to locate records.

You might, however, want even more control. For example, with the previous statement, you might want to prevent the EMPNO field from being modified by leaving it out of the UPDATE clause and leave the TITLE and DEPT fields out of the WHERE clause to avoid update conflicts when other applications have modified the data. To specify the clauses where a specific field appears, use the ProviderFlags property. ProviderFlags is a set that can include any of the values in Table 15.4

Table 15.4   ProviderFlags values

Value

Description

pfInWhere

The field does not appear in the WHERE clause of generated INSERT, DELETE, and UPDATE statements.

pfInUpdate

The field does not appear in the UPDATE clause of generated UPDATE statements.

pfInKey

The field is used in the WHERE clause of a generated SELECT statement that executes when update failures occur. This SELECT statement tries to locate the current value of modified or deleted records, or a record causing key violations when insertions fail.

pfHidden

The field is included in records to ensure uniqueness, but can't be seen or used on the client side.

Thus, the following OnUpdateData event handler excludes the EMPNO field from the UPDATE clause and the TITLE and DEPT fields from the WHERE clause:

procedure TMyDataModule1.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet);
begin
  with DataSet do
  begin
    FieldByName('EMPNO').UpdateFlags := [ufInUpdate];
    FieldByName('TITLE').UpdateFlags := [ufInWhere];
    FieldByName('DEPT').UpdateFlags := [ufInWhere];
  end;
end;

Note: You can use the UpdateFlags property to influence how updates are applied even if you are updating to a dataset and not using dynamically generated SQL. These flags still determine which fields are used to locate records and which fields get updated.

Screening individual updates

Immediately before each update is applied, the provider receives a BeforeUpdateRecord event. You can use this event to edit records before they are applied, similar to the way you can use the OnUpdateData event to edit entire delta packets. For example, the provider does not compare BLOB fields (such as memos) when checking for update conflicts. If you want to check for update errors involving BLOB fields, you can use the BeforeUpdateRecord event.

In addition, you can use this event to apply updates yourself or to screen and reject updates. The BeforeUpdateRecord event handler lets you signal to the resolver that an update has been handled already and should not be applied. The resolver then skips that record, but does not count it as an update error. For example, this event provides a mechanism for applying updates to a stored procedure (which can't be updated automatically), allowing the provider to skip any automatic processing once the record is updated from within the event handler.

Resolving update errors on the provider

When an error condition arises as the application server tries to post a record in the delta packet, an OnUpdateError event occurs. If the application server can't resolve an update error, it temporarily stores a copy of the offending record. When record processing is complete, the application server returns a count of the errors it encountered to the client dataset, and copies the unresolved records into a results data packet that it passes back to the client for further reconciliation.

This mechanism lets you handle any update errors you can resolve mechanically on the application server, while still allowing user interaction on the client application to correct error conditions.

The OnUpdateError handler gets a copy of the record that could not be changed, an error code from the database, and an indication of whether the resolver was trying to insert, delete, or update the record. The problem record is passed back in a client dataset. You should never use the data navigation methods on this dataset. However, for each field in the dataset, you can use the NewValue, OldValue, and CurValue properties to determine the cause of the problem and make any modifications to resolve the update error. If the OnUpdateError event handler can correct the problem, it sets the Response parameter so that the corrected record is applied.

Applying updates to datasets that do not represent a single table

When a resolver component generates SQL statements that apply updates directly to a database server, it needs the name of the database table that contains the records. This can be handled automatically for many datasets such as TTable or "live" TQuery components.

Automatic updates are a problem however, if the resolver must apply updates to the data underlying a stored procedure with a result set or a multi-table query. This is because there is no easy way to obtain the name of the table to which updates should be applied.

If the query or stored procedure is a BDE-enabled dataset (TQuery or TStoredProc) and it has an associated update object, the provider will use the update object. However, if there is no update object, you can supply the table name programmatically in an OnGetTableName event handler. Once an event handler supplies the table name, the resolver component can generate appropriate SQL statements to apply updates.

Note: Supplying a table name only works if the target of the updates is a single database table (that is, only the records in one table need to be updated). If the update requires making changes to multiple underlying database tables, you must explicitly apply the updates in code using the BeforeUpdateRecord event of the provider. Once this event handler has applied an update, you can set the event handler's Applied parameter to True so that the resolver does not generate an error.

Responding to client-generated events

Provider components implement a general-purpose event that lets you create your own calls from clients directly to the provider. This is the OnDataRequest event.

OnDataRequest is not part of the normal functioning of the provider. It is simply a hook to allow your clients to communicate directly with providers on the application server. The event handler takes an OleVariant as an input parameter and returns an OleVariant. By using OleVariants, the interface is sufficiently general to accommodate almost any information you want to pass to or from the provider.

To generate an OnDataRequest event, the client application calls the DataRequest method of the client dataset.

Handling server constraints

Most relational database management systems implement constraints on their tables to enforce data integrity. A constraint is a rule that governs data values in tables and columns, or that governs data relationships across columns in different tables. For example, most SQL-92 compliant relational databases support the following constraints:

Note: This list is not exclusive. Your database server may support some or all of these constraints in part or in whole, and may support additional constraints. For more information about supported constraints, see your server documentation.

Database server constraints obviously duplicate many kinds of data checks that traditional desktop database applications have managed in the past. You can take advantage of server constraints in your multi-tiered database applications without having to duplicate the constraints in application server or client application code.

The the provider is working with a BDE-enabled dataset, its Constraints property enables you to replicate and apply server constraints to data passed to and received from client applications. When Constraints is True (the default), your server's constraints are replicated to clients and affect client attempts to update data.

Important: Before the application server can pass constraint information on to the client application, it must retrieve the constraints from the database server. To import database constraints from the server, use SQL explorer to import the database server's constraints and default expressions into the Data Dictionary. Constraints and default expressions in the Data Dictionary are automatically made available to BDE-enabled datasets in the application server.

There may be times when you do not want to apply server constraints to data sent to a client application. For example, a client application that receives data in packets and permits local updating of records prior to fetching more records may need to disable some server constraints that might be triggered because of the temporarily incomplete set of data. To prevent constraint replication from the application server to a client dataset, set Constraints to False. Note that client datasets can disable and enable constraints using the DisableConstraints and EnableConstraints methods. For more information about enabling and disabling constraints from the client dataset, see "Handling constraints".