754 on the server. To accomplish this, you’ll need to add some more code to the client. However, before we do that, let’s add some features to the server. Adding Constraints to the Server When you write a traditional data module in Delphi, you can easily add some of the applica- tion logic, or business rules, by handling the dataset events, and by setting field object prop- erties and handling their events. You should avoid doing this work on the client application; instead, write your business rules on the middle tier. In the DataSnap architecture, you can send some constraints from the server to the client and let the client program impose those constraints during the user input. You can also send field properties (such as minimum and maximum values and the display and edit masks) to the client and (using some of the data access technologies) process updates through the dataset used to access the data (or a companion UpdateSql object). Field and Table Constraints When the provider interface creates data packets to send to the client, it includes the field definitions, the table and field constraints, and one or more records (as requested by the ClientDataSet component). This implies that you can customize the middle tier and build distributed application logic by using SQL-based constraints. The constraints you create using SQL expressions can be assigned to an entire dataset or to specific fields. The provider sends the constraints to the client along with the data, and the client applies them before sending updates back to the server. This reduces network traffic, compared to having the client send updates back to the application server and eventually up to the SQL server, only to find that the data is invalid. Another advantage of coding the con- straints on the server side is that if the business rules change, you need to update the single server application and not the many clients on multiple computers. But how do you write constraints? There are several properties you can use: • BDE datasets have a Constraints property, which is a collection of TCheckConstraint objects. Every object has a few properties, including the expression and the error message. • Each field object defines the CustomConstraint and ConstraintErroMessage proper- ties. There is also an ImportedConstraint property for constraints imported from the SQL server. • Each field object has also a DefaultExpression property, which can be used locally or passed to the ClientDataSet. This is not an actual constraint, only a suggestion to the end user. Chapter 17 • Multitier Database Applications with DataSnap Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 755 Our next example, AppServ2, adds a few constraints to a remote data module connected to the sample EMPLOYEE InterBase database. After connecting the table to the database and creating the field objects for it, you can set the following special properties: object SQLDataSet1: TSQLDataSet object SQLDataSet1EMP_NO: TSmallintField CustomConstraint = ‘x > 0 and x < 10000’ ConstraintErrorMessage = ‘Employee number must be a positive integer below 10000’ FieldName = ‘EMP_NO’ end object SQLDataSet1FIRST_NAME: TStringField CustomConstraint = ‘x <> ‘#39#39 ConstraintErrorMessage = ‘The first name is required’ FieldName = ‘FIRST_NAME’ Size = 15 end object SQLDataSet1LAST_NAME: TStringField CustomConstraint = ‘not x is null’ ConstraintErrorMessage = ‘The last name is required’ FieldName = ‘LAST_NAME’ end end NOTE The expression ‘x <> ‘#39#39 is the DFM transposition of the string x <> ‘’, indicating that we don’t want to have an empty string. The final constraint, not x is null, instead allows empty strings but not null values. Including Field Properties You can control whether the properties of the field objects on the middle tier are sent to the ClientDataSet (and copied into the corresponding field objects of the client side), by using the poIncFieldProps value of the Options property of the DataSetProvider. This flag con- trols the download of the field properties Alignment, DisplayLabel, DisplayWidth, Visible, DisplayFormat, EditFormat, MaxValue, MinValue, Currency, EditMask, and DisplayValues, if they are available in the field. Here is an example of another field of the AppServ2 example with some custom properties: object SQLDataSet1SALARY: TBCDField DefaultExpression = ‘10000’ FieldName = ‘SALARY’ DisplayFormat = ‘#,###’ EditFormat = ‘####’ Precision = 15 Size = 2 end Adding Constraints to the Server Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 756 With this setting, you can simply write your middle tier the way you usually set the fields of a standard client/server application. This approach also makes it faster to move existing applications from a client/server to a multitier architecture. The main drawback of sending fields to the client is that transmitting all the extra information takes time. Turning off poIncFieldProps can dramatically improve network performance of datasets with many columns. A server can generally filter the fields returned to the client; it does this by declaring persis- tent field objects with the Fields editor and omitting some of the fields. Because a field you’re filtering out might be required to identify the record for future updates (if the field is part of the primary key), you can also use the field’s ProviderFlags property on the server to send the field value to the client but make it unavailable to the ClientDataSet component (this provides some extra security, compared to sending the field to the client and hiding it there). Field and Table Events You can write middle-tier dataset and field event handlers as usual and let the dataset process the updates received by the client in the traditional way. This means that updates are consid- ered to be operations on the dataset, exactly as when a user is directly editing, inserting, or deleting fields locally. This is accomplished by setting the ResolveToDataSet property of the TDatasetProvider component, again connecting either the dataset used for input or a second one used for the updates. This approach is possible with datasets supporting editing operations. These includes BDE, ADO, and InterBase Express datasets, but not those of the new dbExpress architecture. With this technique, the updates are performed by the dataset, which implies a lot of con- trol (the standard events are being triggered) but generally slower performance. Flexibility is much greater, as you can use standard coding practices. Also, porting existing local or client/ server database applications, which use dataset and field events, is much more straightforward with this model. However, keep in mind that the user of the client program will receive your error messages only when the local cache (the delta) is sent back to the middle tier. Saying to the user that some data prepared half an hour ago is not valid might be a little awkward. If you follow this approach, you’ll probably need to apply the updates in the cache at every AfterPost event on the client side. Finally, if you decide to let the dataset and not the provider do the updates, Delphi helps you a lot in handling possible exceptions. Any exceptions raised by the middle-tier update events (for example, OnBeforePost) are automatically transformed by Delphi into update errors, which activate the OnReconcileError event on the client side (more on this event later in this chapter). No exception is shown on the middle tier, but the error travels back to the client. Chapter 17 • Multitier Database Applications with DataSnap Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 757 Adding Features to the Client After adding some constraints and field properties to the server, we can now return our attention to the client application. The first version was very simple, but now there are sev- eral features we can add to it to make it work well. In the ThinCli2 example, I’ve embedded support for checking the record status and accessing the delta information (the updates to be sent back to the server), using some of the ClientDataSet techniques already discussed in Chapter 13. The program also handles reconcile errors and supports the briefcase model. Keep in mind that while you’re using this client to edit the data locally, you’ll be reminded of any failure to match the business rules of the application, set up on the server side using constraints. The server will also provide us with a default value for the Salary field of a new record and pass along the value of its DisplayFormat property. In Figure 17.3 you can see one of the error messages this client application can display, which it receives from the server. This message is displayed while editing the data locally, not when you send it back to the server. The Update Sequence This client program also includes a button to Apply the updates to the server and a standard reconcile dialog. Here is a summary of the complete sequence of operations related to an update request and the possible error events: 1. The client program calls the ApplyUpdates method of a ClientDataSet. 2. The delta is sent to the provider on the middle tier. The provider fires the OnUpdateData event, where you have a chance to look at the requested changes before they reach the FIGURE 17.3: The error message displayed by the ThinCli2 example when the employee ID is too large Adding Features to the Client Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 758 database server. At this point you can modify the delta, which is passed in a format compatible with the data of a ClientDataSet. 3. The provider (technically, a part of the provider called the “resolver”) applies each row of the delta to the database server. Before applying each update, the provider receives a BeforeUpdateRecord event. If you’ve set the ResolveToDataSet flag, this update will eventually fire local events of the dataset in the middle tier. 4. In case of a server error, the provider fires the OnUpdateError event (on the middle tier) and the program has a chance of fixing the error at that level. 5. If the middle-tier program doesn’t fix the error, the corresponding update request remains in the delta. The error is returned to the client side at this point or after a given number of errors have been collected, depending on the value of the MaxErrors parameter of the ApplyUpdates call. 6. Finally, the delta packet with the remaining updates is sent back to the client, firing the OnReconcileError event of the ClientDataSet for each remaining update. In this event handler, the client program can try to fix the problem (possibly prompting the user for help), modifying the update in the delta, and later reissuing it. Refreshing Data You can obtain an updated version of the data, which other users might have modified, by calling the Refresh method of the ClientDataSet. However, this operation can be done only if there are no pending update operations in the cache, as calling Refresh raises an exception when the change log is not empty: if cds.ChangeCount = 0 then cds.Refresh; If only some records have been changed, you can refresh the others by calling RefreshRecords. This method refreshes only the current record, but it should be used only if the user hasn’t mod- ified the current record. In this case, in fact, RefreshRecords leaves the unapplied changes in the change log. As an example, you can refresh a record every time it becomes the active one, unless it has been modified and the changes have not yet been posted to the server: procedure TForm1.cdsAfterScroll(DataSet: TDataSet); begin if cds.UpdateStatus = usUnModified then cds.RefreshRecord; end; When the data is subject to frequent changes by many users and each user should see changes right away, you should generally apply any change immediately in the AfterPost Chapter 17 • Multitier Database Applications with DataSnap Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 759 and AfterDelete methods, and call RefreshRecords for the active record (as shown above) or each of the records visible inside a grid. This code is actually part of the ClientRefresh example, connected to the AppServ2 server. For debugging purposes, the program also logs the EMP_NO field for each record it refreshes, as you can see in Figure 17.4. I’ve done this by adding a button to the ClientRefresh example. The handler of this button moves from the current record to the first visible record of the grid and then to the last visi- ble record. This is accomplished by noting that there are RowCount - 1 rows visible, assum- ing that the first row is the fixed one hosting the field names. The program doesn’t call RefreshRecord every time, as each movement will trigger an AfterScroll event with the code shown above. This is the code to refresh the visible rows, which might even be trig- gered by a timer: var i: Integer; bm: TBookmarkStr; begin // refresh visible rows cds.DisableControls; // start with the current row i := TMyGrid(DbGrid1).Row; bm := cds.Bookmark; try // get back to the first visible record while i > 1 do begin cds.Prior; Dec (i); end; // return to the current record i := TMyGrid(DbGrid1).Row; cds.Bookmark := bm; // go ahead until the grid is complete FIGURE 17.4: The form of the ClientRefresh example, which automatically refreshes the active record and allows more extensive updates by pressing the buttons Adding Features to the Client Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 760 while i < TMyGrid(DbGrid1).RowCount do begin cds.Next; Inc (i); end; finally // set back everything and refresh cds.Bookmark := bm; cds.EnableControls; end; This approach generates a huge amount of network traffic, so you might want to trigger updates only when there are actual changes. This can be implemented by adding a callback technology to the server, so that it can inform all connected clients that a given record has changed. The client can determine whether it is interested in the change and eventually trig- ger the update request. Advanced DataSnap Features There are many more features in DataSnap than I’ve covered up to now. Here is a quick tour of some of the more advanced features of the architecture, partially demonstrated by the AppSPlus and ThinPlus examples. Unfortunately, demonstrating every single idea would turn this chapter into an entire book (and not every Delphi programmer is interested in and can afford DataSnap), so I’ll limit myself to an overview. Besides the features discussed in the following sections, the AppSPlus and ThinPlus examples demonstrate the use of a socket connection, limited logging of events and updates on the server side, and direct fetching of a record on the client side. The last feature is accomplished with this call: procedure TClientForm.ButtonFetchClick(Sender: TObject); begin ButtonFetch.Caption := IntToStr (cds.GetNextPacket); end; This allows you to get more records than are actually required by the client user interface (the DBGrid). In other words, you can fetch records directly, without waiting for the user to scroll down in the grid. I suggest you study the details of these complex examples after read- ing the rest of this section. Chapter 17 • Multitier Database Applications with DataSnap Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 761 Parametric Queries If you want to use parameters in a query or stored procedure, then instead of building a cus- tom solution (with a custom method call to the server), you can let Delphi help you. First define the query on the middle tier with a parameter, such as: select * from customer where Country = :Country Use the Params property to set the type and default value of the parameter. On the client side, you can use the Fetch Params command of the ClientDataSet’s shortcut menu, after connecting it to the proper provider. At run time, you can call the equivalent FetchParams method of the ClientDataSet component. Now you can provide a local default value to the parameter by acting on the Params prop- erty. This will be sent to the middle tier when you fetch the data. The ThinPlus example refreshes the parameter with the following code: procedure TFormQuery.btnParamClick(Sender: TObject); begin cdsQuery.Close; cdsQuery.Params[0].AsString := EditParam.Text; cdsQuery.Open; end; You can see the secondary form of this example, which shows the result of the parametric query in a grid, in Figure 17.5. In the figure you can also see some custom data sent by the server, as explained in the section “Customizing the Data Packets.” FIGURE 17.5: The secondary form of the ThinPlus example, showing the data of a parametric query Advanced DataSnap Features Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 762 Custom Method Calls Since the server has a normal COM interface, we can add more methods or properties to it and call them from the client. Simply open the type library editor of the server and use it as with any other COM server. In the AppSPlus example, I’ve added a custom Login method with the following implementation: procedure TAppServerPlus.Login(const Name, Password: WideString); begin // TODO: add actual login code if Password <> Name then raise Exception.Create (‘Wrong name/password combination received’) else Query.Active := True; ServerForm.Add (‘Login:’ + Name + ‘/’ + Password); end; The program makes a simple test, instead of checking the name/password combination against a list of authorizations as a real application should do. Also, disabling the Query doesn’t really work, as it can be activated by the provider. Disabling the DataSetProvider is actually a more robust approach. The client has a simple way to access the server, the AppServer prop- erty of the remote connection component. Here is a sample call from the ThinPlus example, which takes place in the AfterConnect event of the connection component: procedure TClientForm.ConnectionAfterConnect(Sender: TObject); begin Connection.AppServer.Login (Edit2.Text, Edit3.Text); end; Note that you can call extra methods of the COM interface through DCOM and also using a socket-based or HTTP connection. Because the program uses the safecall calling conven- tion, the exception raised on the server is automatically forwarded and displayed on the client side. This way, when a user selects the Connect check box, the event handler used to enable the client datasets is interrupted, and a user with the wrong password won’t be able to see the data. NOTE Besides direct method calls from the client to the server, you can also implement callbacks from the server to the client. This can be used, for example, to notify every client of specific events. COM events are one way to do this. As an alternative, you can add a new interface, implemented by the client, which passes the implementation object to the server. This way, the server can call the method on the client computer. Callbacks are not possible with HTTP connections, though. Chapter 17 • Multitier Database Applications with DataSnap Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 763 Master/Detail Relations If your middle-tier application exports multiple datasets, you can retrieve them using multiple ClientDataSet components on the client side and connect them locally to form a master/detail structure. This will create quite a few problems for the detail dataset unless you retrieve all of the records locally. This solution also makes it quite complex to apply the updates; you cannot usually cancel a master record until all related detail records have been removed, and you cannot add detail records until the new master record is properly in place. (Actually, different servers handle this differently, but in most cases where a foreign key is used, this is the standard behavior.) What you can do to solve this problem is to write complex code on the client side to update the records of the two tables according to the specific rules. A completely different approach is to retrieve a single dataset that already includes the detail as a dataset field, a field of type TDatasetField. To accomplish this, you need to set up the master/detail relation on the server application: object TableCustomer: TTable DatabaseName = ‘DBDEMOS’ TableName = ‘customer.db’ end object TableOrders: TTable DatabaseName = ‘DBDEMOS’ MasterFields = ‘CustNo’ MasterSource = DataSourceCust TableName = ‘ORDERS.DB’ end object DataSourceCust: TDataSource DataSet = TableCustomer end object ProviderCustomer: TDataSetProvider DataSet = TableCustomer end On the client side, the detail table will show up as an extra field of the ClientDataSet, and the DBGrid control will display it as an extra column with an ellipsis button. Clicking the but- ton will display a secondary form with a grid presenting the detail table (see Figure 17.6). If you need to build a flexible user interface on the client, you can then add a secondary Client- DataSet connected to the dataset field of the master dataset, using the DataSetField property. Simply create persistent fields for the main ClientDataSet and then hook up the property: object cdsDet: TClientDataSet DataSetField = cdsTableOrders end Advanced DataSnap Features Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com [...]... records Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 7 68 Chapter 17 • Multitier Database Applications with DataSnap What’s Next? Borland originally introduced its multitier technology in Delphi 3 and has kept extending it from version to version In addition to further updates and the change of the MIDAS name to DataSnap, Delphi 6 sees the introduction of XML and SOAP support, introducing an... reasonable amount of text and big enough for graphics You can see an example of this grid at design time in Figure 18. 4 FIGURE 18. 4: An example of the MdDbGrid component at design time Notice the output of the graphics and memo fields Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 7 86 Chapter 18 • Writing Database Components While creating the output was a simple matter of adapting the code used in... controls ● A record viewer ● Building custom datasets ● Saving a dataset to a local stream Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 18 770 Chapter 18 • Writing Database Components I n Chapter 11, “Creating Components,” we explored the development of Delphi components in depth Now that I’ve discussed database programming, we can get back to the earlier topic and focus on the development of... area Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 784 Chapter 18 • Writing Database Components between the cell border and the output text The actual output is produced by calling another Windows API function, DrawText, which centers the text vertically in its cell This drawing code works both at run time, as you can see in Figure 18. 3, and at design time The output may not be perfect, but... 764 Chapter 17 • Multitier Database Applications with DataSnap With this setting you can show the detail dataset in a separate DBGrid placed as usual in the form (the bottom grid of Figure 17 .6) or in any other way you like Note that with this structure, the updates relate only to the master table, and the server should handle the proper update sequence even in complex situations FIGURE 17 .6: The... specify the number of rows (in order to avoid having rows hidden beneath the lower edge of the grid), the base class keeps recomputing them Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 788 Chapter 18 • Writing Database Components Finally, here’s the code used to draw the data, ported from the RecordView component and adapted slightly for the grid: procedure TMdDbGrid.DrawColumnCell (const Rect:... families of dataset components available in Delphi, in Chapter 13, Delphi s Database Architecture,” I mentioned the possibility of writing a custom dataset class Now it’s time to have a look at an actual example The reasons for writing a custom dataset relate to the fact that you won’t need to deploy a database engine but you’ll still be able to take full advantage of Delphi s database architecture, including... TMdDbTrack.CNHScroll(var Message: TWMHScroll); begin // enter edit mode FDataLink.Edit; // update data inherited; // let the system know FDataLink.Modified; end; Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 7 78 Chapter 18 • Writing Database Components procedure TMdDbTrack.CNVScroll(var Message: TWMVScroll); begin // enter edit mode FDataLink.Edit; // update data inherited; // let the system know FDataLink.Modified;... in Figure 18. 2 The DbTrack program contains a check box to enable and disable the table, the visual components, and a couple of buttons you can use to detach the vertical TrackBar component from the field it relates to Again, I placed these on the form to test enabling and disabling the track bar Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com Creating Custom Data Links 779 FIGURE 18. 2: The DbTrack... as a whole, a simple record viewer Delphi s database grid shows the value of several fields and several records simultaneously In my record viewer component, I want to list all the fields of the current record, using a customized grid This example will show you how to build a customized grid control, and a custom data link to go with it A Record Viewer Component In Delphi there are no data-aware components . Features Copyright ©2001 SYBEX, Inc., Alameda, CA www.sybex.com 7 68 What’s Next? Borland originally introduced its multitier technology in Delphi 3 and has kept extending it from version to version CA www.sybex.com 761 Parametric Queries If you want to use parameters in a query or stored procedure, then instead of building a cus- tom solution (with a custom method call to the server), you can let Delphi. Inc., Alameda, CA www.sybex.com 764 With this setting you can show the detail dataset in a separate DBGrid placed as usual in the form (the bottom grid of Figure 17 .6) or in any other way you like.