delphi3000.com Article

Dynamic COM-Based Dataset Service
Undertitle:with Nested Datasets using Delphi 4
URL:http://www.delphi3000.com/article.asp?ID=749
Category:MIDAS
Uploader:Patrick Tardif
 
Question:

Ever since technologies such as COM - which allow structured information to be exchanged between two PCs - have been introduced, using them has become indispensable. In this article, I will explain how to create a server using COM and MIDAS technologies, which dynamically builds and sends data to the client. This approach is much more flexible than statically defining all the datasets with their Master-Detail relations using DataModules, where if you wished to create a new relationship between two Master-Detail tables, you had to recompile the application. The scope of this article is to show you how to create a dynamic COM-based dataset service. With this tool, there is no longer any need to recompile the data application server. In addition, this application, which is an automation server, sends the data as a TClientDataset (OleVariant that can be unpacked by the ClientDataset) which, in turn, can also contain other ClientDatasets. In other words, the server creates and sends a hierarchical dataset tree.

Answer:

How to create master-detail relations using the TQuery object

Beforehand, you absolutely need to know how to create a Master-Detail relation using two TQuery objects. The reason for this is simple: in this article, the server I created only uses TQuery objects to obtain data. It does not use TTable objects. I decided to simplify the implementation of this tool, which you can use for your future multi-tier projects. TQuery objects are much more flexible than TTable objects.




Figure 1: Master-detail relation between CUSTOMER and ORDERS tables.

Lets suppose we have two database tables: one called CUSTOMER and the other called ORDERS (see Database DDEMOS). The Master-Detail relation existing between these two database tables is based on the field called CUSTNO found in both the CUSTOMER table and in the ORDERS table (see the figure 1). The next step will be to create your SQL syntax containing the parameters, which define the Master-Detail relation between the two database tables. The SQL syntax we will use in this example is: SELECT * FROM ORDERS WHERE CUSTNO = :CUSTNO. The name of the parameter must be the same as the field, which determines the relation in the master database table. Please note that there must not be any space between the colon (:) and "CUSTNO". Once you have established the syntax, you must define the type of each parameter used. In this case, we only have one parameter. The last step will be to select the detail table's DataSource. In my example it is the ORDERS table, which contains a DataSource, connected to the CUSTOMER table. You must understand this concept to go any further, because this is what I will use for the example.

Defining the server

  • Create a new project and call it DynamicDatasetServiceServer.dpr
  • Once the project has been created, click on "File" and "New"; then click the multi-tier tab and select "RemoteDataModule"
  • A window will appear and you will be asked to enter information in three boxes. The first box asks for the ClassName. Enter the name of your server (in this case: “DynamicDatasetService”). In the second and third box it is best to keep the default values.
  • Drag and drop a TProvider and call it “ProviderDatasetService”

You have now created the server, which for the moment doesn't do anything. We need to define all the interfaces and all the CoClasses to be used. Before going any further, we must know exactly what kind of service our server needs to provide.

The client should be capable of creating master TQuery objects containing SQL syntax and defining connections to a database. The client should also be capable of defining all the detail TQuery objects, which themselves contain syntax and connections as well. As I explained earlier, you must be able to define the type of data for each parameter used in the SQL syntax.

We must then collect information created by the client on the server's side. Usually, we use TList objects to collect objects, but because we are using COM, we cannot directly stock objects. We must stock interfaces instead. These interfaces will allow the server to access information. We must therefore create object collection automation. To do this, we will derive our class from TAutoObject. This will call for a type library that you can see in figure 2.

In my example, I use five CoClasses using four different interfaces. Two of the CoClasses will be object collection automation servers; respectively DdsDtsDetails and DdsSQLParams.



Figure 2: Type library of the Dynamic COM-Based Dataset Service

Each of these CoClasses implement IDdsCollection which in turn contains the fundamental methods for stocking and accessing an object list, which are: Add, Delete, Count and Items. Note that the parameter used by Add is IDispatch. The CoClass DdsDtsDetails stocks the interface IDdsDtsDetail, which is implemented by the CoClass DdsDtsDetail. In the same fashion, DdsSQLParams stocks IDdsSQLParam implemented by DdsSQLParam. We therefore define two automation servers (services) whose purpose is to collect references to the interfaces. Notice that by convention, I use the suffix "s" to define an object, which stocks other objects. I use the same name for the object that we add into the collection, however I drop the "s". It is important to create a type library because each CoClass will be a child of TAutoObject.

TAutoObject requires a type library because it supports dual interfaces, and because it inherits the IProvideClassInfo interface support, which in turn requires a type library. The type library is not mandatory, but since we will derive our class automation from TAutoObject, we will need to create a type library.



Figure 3: Declaration of automation object TDdsDtsDetails

I implemented each CoClass in a different unit. Figure 3 shows the declaration of automation object TDdsDtsDetails. You can observe that the object contains a TInterfaceList object, which is a kind of TList in which interfaces can be directly stocked. To help you understand the advantage of TInterfaceList over TList, you can take a look at the CastExample.dpr application delivered with this article. If you use a TList, you have to use the following syntax to add an interface:

Flist.Add(Pointer(Interface));

Another problem with this approach, is that you must implement RefCount yourself, using _AddRef and _Release methods. By using TInterfaceList you eliminate this tedious process. Another important point to emphasize is that seeing we are using automation, the constructor is never directly called upon. It is rather the Initialize method which is called.



Figure 4: Implementation of TddsDtsDetails automation object.

When implementing the Add method in the Figure 4, you can see that I cast the IDispatch interface to be exactly the interface I want it to be. We therefore limit the use of this automation object to using only IddsDtsDetail. The Initialize method lets you create TInterfaceList, which will stock the interfaces that you can see in the Figure 4.

The relation, which exists between each of these CoClasses, is illustrated below in the Figure 5.



Figure 5: Objects automation servers relationship

The main service is DynamicDatasetService, which uses the DdsDtsDetail service. It uses DdsDtsDetails to stock information relating to all the detail datasets tied to the master dataset. If you take a closer look at the illustration in figure 5, you will notice the DdsDdsDetail service uses two services: DdsSQLParams and DdsDtsDetails. This design lets you create as many relations as you need. There are two important methods used to create the information to be sent to the client, which you can find in the DynamicDatasetService server where all the code is found: the ProcessCreationDataset function (which returns the master dataset in which all the relations with the other datasets are found). This method uses the CreateBranch method (which allows creation of the detail datasets). You can see those methods in the Figure 6.



Figure 6: Main method for the Dynamic COM-Based Dataset Service

Defining the client

Before you begin creating the client, you have to run the server once in order to stock the information from the services and the interfaces in the registry. Once you have done this, drag and drop a TDCOMConnection object on the form and select "DynamicDatasetServiceServer.DynamicDatasetService" (which is the server you have just created) for the ServerName property. Then drag and drop a TClientDataset on the form and choose your DCOMConnection Object for the RemoteServer property. As for the ProviderName property, choose ResultClientDatasetProvider (which is the only option given). If you select Active at True for the ClientDataset, Delphi will generate an exception. In this case the client will have to dynamically create all the datasets needed on the server in order for the server to be able to send data to the client. Once this is done, you can change the Active property to True at run time.

How to dynamically create datasets

Before dynamically creating the datasets, you must know what information you want to obtain from the server. In our client application, we have two options to create different result sets. The two options are illustrated in figures 7 and 8.



Figure 7: Relations Master-detail option 1






Figure 8: Relations Master-detail option 2

In Figure 7 (Option 1), we wish to obtain a TClientDataset which contains the data in the CUSTOMER database table, which in turn contains CUSTOLY and ORDERS nested ClientDatasets, which in turn again contains ITEMS and EMPLOYEE datasets. We would like all this information in only one ClientDataset.

The first step, in order to be able to receive data from the DynamicDatasetService, is to create all the services within the DynamicDatasetService in order for it to be able to build the hierarchical structure for the datasets you want to use. In my client application, there are two possible options as illustrated in Figures 7 and 8. If you take a closer look at the code in each of the options, you will see that we created a proxy object which contains the SelectSQLStatement, TitleColumnName, BDEAliasName, FieldMasterLink, FieldDetailLink and FieldMasterDataType parameters.



Figure 9: Code using DynamicDatasetService for Option 1 and Option 2

The difference between both options is that the object used in Option 1 is TProxyCUSTOMER, whereas Option 2 uses TProxyCUSTOMER2. You can see the differences in the Figure 9. The only method the proxies implement, is the LinksRule method that you can see in the Figure 10 and 11. If there is no detail table to be created by this method, then all you have to do is add "inherited". The TProxy object carries out the fundamental work.



Figure 10: Implementation of the method LinksRule for TProxyCUSTOMER





Figure 11: Implementation of the method LinksRule for TProxyCUSTOMER2

Conclusion

As you can see, COM technology is powerful and gives you the opportunity to create thin clients. With this tool, you can separate the direct data manipulation from the client and give the flexibility necessary without recompilation. The main goal for n-tier projects is to have the business rules on the server side. That way, when we have to change them, we don't have to recompile the client application. In this example, we didn’t show this part, but you still have this flexibility if you need it. The purpose of this example was just to show you one possibility. When using hierarchical data, having several generations tends to slow down the speed of your application. A better solution would be to send your object directly to the client. The object should be programmed to pack itself into an OleVariant and be able to regenerate itself from an OleVariant to the client side. Your objects can be also cached in memory on the server for even faster results and you can use new technology from COM+.



Download the Delphi5 Source-Code:

DynamicDatasetServiceDelphi5.zip (24 KB)

Download the Delphi4 Source-Code for this article:

Server.zip (10,3 KB)
Client.zip (6,48 KB)
CastingInterface.zip (2,33 KB)

© by 1999 Tier Solution Inc. (Author: Patrick Tardif, President)



Copyright 2000 delphi3000.com
Contact: delphi3000@bluestep.com'

Comments to this article
Write a new comment
Contract you're functions...
    Yoav Abrahami (Aug 8 2001 10:00AM)

Great article.

A comments:
In the TDdsDtsDetails.Add function, you have the line

Procedure TDdsDtsDetails.Add(Const aObject: IDispatch);
Begin
  FIList.Add(aObject as IDdsDtsDetail);
End;

You typecast the aObject variable to TDdsDtsDetail in order to limit the use of TDdsDtsDetails to only TDdsDtsDetail objects. This method is not clean, and will produce a typecase error if the user of the TDdsDtsDetails class violates the contract and tries to insert object of some other class to the TDdsDtsDetails object. The error message and the problem does not seem to have anything in common.

A better way is to use assertions. write a code like

Procedure TDdsDtsDetails.Add(Const aObject: IDispatch);
Begin
  Assert(aObject is IDdsDtsDetail, 'TDdsDtsDetails cannot add objects of class other then IDdsDtsDetail');
  FIList.Add(aObject);
End;

You get three advanteges for this:
1. You get an error message that tells you what the problem is.
2. You can compile the code without the assertion (One of the compiler options).
3. You're intention is clear - don't enter objects other then IDdsDtsDetail.



Respond

RE: Contract you're functions...
Patrick (Aug 8 2001 9:18PM)

This is a good point but Assert is a debugging tool and when the time comes for deploying your code to another user, the Assert is turned off, and the TDdsDtsDetails may contain unknown objects and will not work properly the way you're suggesting - unless you keep the assertion turned on.
Respond

<%If Session("sSecurityLevel") >= 2 Then%>Additional Admin-Function:
Delete This Article-Comment! Delete this Comment!<%End If%>
Dynamic Data
    Jan Porter (Jan 19 2001 12:41PM)

Dynamic Data architecture enhances an objects reuse across a broader spectrum. Our industry tends to hard code the relationships of data making it difficult for future maintenance and reuse. Where we place our structure is where we place our limititation. Data is the most changeable requirement from project to project and historically we place our structure at the most volatile point, hence we scrap and start all over again with each and every new project. Dynamic Data architectures, which encapsulate the processes, verb requirements and software patterns, yet still allows for dynamic data tagging and any additional specific business rules for a particular instance of the object, this is where the future of efficient software programming is going. For far too long we've been a data driven architectural industry. It is time to flip the tables 180 degrees and focus our architectural design on the few software patterns that exist and allow data to be flexible and dynamic in a particular instance or use.
Respond

<%If Session("sSecurityLevel") >= 2 Then%>Additional Admin-Function:
Delete This Article-Comment! Delete this Comment!<%End If%>
It is great
    Gavin Lin (Jul 2 2000 5:54PM)

This is the best I have erver seen.
Respond

<%If Session("sSecurityLevel") >= 2 Then%>Additional Admin-Function:
Delete This Article-Comment! Delete this Comment!<%End If%>
Midas database
    anonymus (May 26 2000 4:45AM)

Probably the most valuable article to appear here to date. The code snippets have been added to my library for future reference. Thanks
Respond

<%If Session("sSecurityLevel") >= 2 Then%>Additional Admin-Function:
Delete This Article-Comment! Delete this Comment!<%End If%>
Hope come more delphi elites like Mr. Tardif
    anonymus (May 19 2000 5:31PM)

Mr. Tardif,

Delphi experts, please come on, like Tardif, so as to outstrike VB, that I spent so much time ago, but get nothing beautiful!
Respond

<%If Session("sSecurityLevel") >= 2 Then%>Additional Admin-Function:
Delete This Article-Comment! Delete this Comment!<%End If%>
Auto Increment Fields
    anonymus (May 11 2000 5:04AM)

Hi,

   I have written an application server (and a client) where I'm trying to insert a new record on a Table that has many simple fields and one auto increment field (MS SQL) that I'm using for Primary Key.
   There is one problem with this application: the value assigned to this field is not viewed by client application after ApplyUpdates is used.
   I tryed to use RefreshRecord and nothing happend. If I use Refresh, or close and re-open then Table, the value shows assigned to this field, but I loose the record position that I inserted.
   Can you help-me with this problem?
Respond

RE: Auto Increment Fields
anonymus (May 11 2000 10:08PM)

The .NewValue property worked fine for me under DBase 7. I haven't tried it out with MIDAS, but give it a try.
Respond

RE: RE: Auto Increment Fields
Nos Doughty (Jun 24 2000 2:05PM)

I had a similar problem.

Basically, you have to open and close to guarantee the update and you save your position with a set of functions relating to "bookmarks"

just save and goto

the help files have all the info you need to do this

good luck.
Respond

<%If Session("sSecurityLevel") >= 2 Then%>Additional Admin-Function:
Delete This Article-Comment! Delete this Comment!<%End If%>
Excellent article!!!!
    Helmut Dollinger (Mar 27 2000 8:45PM)

This is the article I searched for ....

Thanks!!

H.
Respond

RE: Excellent article!!!!
Eva Scharf (Mar 27 2000 8:47PM)

Yes, this article is really excellent, I hope you will publish a further one like this in the near future !!!

Eva
Respond

<%If Session("sSecurityLevel") >= 2 Then%>Additional Admin-Function:
Delete This Article-Comment! Delete this Comment!<%End If%>