DataSnap memory management is very powerful and a key DataSnap feature to implement cache solution. Imagine the scenario where the client application requests the same data thousands of times every day. You will have to touch the database every time. Let’s assume this data doesn’t change very often, like list of countries, states or cities.
When caching is implemented for this scenario, the first request to get the data from the database keeps it in memory. On the second and following requests the server will get the data from memory and return to the client. In other words, you just touch the database one time.
The combination between DataSnap memory management, DBXReader and ClientDataSet are what you need to implement a caching solution.
The DataSnap memory management is defined on the DSServerClass component using the LifeCycle property, which you can define as:
- Server → One class instance is used per server, all clients get the same class instance from the server (Singleton)
- Session → One class instance is used per DataSnap Session, each client get your own instance from the server (Statefull).
- Invocation → One class instance is used per invocation method (Stateless), here you can decide when create and destroy the class.
Looking the LifeCycle property, you see that you can implement a cache solution per client (Session) or per server (Server) just by changing this property.
Let’s see how we can implement a caching solution using the scenario where I need to cache the list of states; this data is in the table STATE in this case.
My Server Class is a DSServerModule (class name TDMDataSet5) and contains 2 private and 1 public methods, which are:
private function GetRecords(Fields, Table: String): TDBXReader; function GetData(Cds : TClientDataSet; Fields, Table: String) : TDBXReader; public function GetState: TDBXReader;
GetRecords will execute the query against the database and return the records as DBXReader, this method doesn’t implement any logic to check if the cache is already filled.
function TDMDataSet5.GetRecords(Fields, Table: String): TDBXReader; var cmd: TDBXCommand; begin cmd := DMServerContainer.GetConnection.DBXConnection.CreateCommand; try cmd.Text := 'Select ' + Fields + ' from ' + Table; Result := cmd.ExecuteQuery; except raise; end; end;
Since DBXReader is unidirectional we can’t keep the data in memory, the solution is to copy and maintain the data in a ClientDataSet.
GetData is an internal method responsible to create and maintain the data in a cache, and return the data as DBXReader.
Looking at the implementation below, the method will get the data from the database (GetRecords) only if the ClientDataSet is not active, in other words we never had the data in the cache and it will be executed only one time. After the IF statement, the TDBXDataSetReader class will copy the data from ClientDataSet to DBXReader and return that.
function TDMDataSet5.GetData(Cds: TClientDataSet; Fields, Table: String): TDBXReader; var Reader : TDBXReader; begin if not Cds.Active then // Not active means, never move the data to ClientDataSet – no cache begin Reader := GetRecords(Fields, Table); TDBXDataSetReader.CopyReaderToClientDataSet( Reader, Cds ); Reader.Free; Cds.Open; end; Result := TDBXDataSetReader.Create(Cds, False (* InstanceOwner *) ); end;
You may ask why I’m copying the data from ClientDataSet to DBXReader and not return the ClientDataSet directly. Two reasons:
- I can’t marshal/unmarshal ClientDataSet as a JSON object
- DataSnap converts DBXReader into JSON when the server methods are invocated through REST interfaces.
An important point here, the DSServerModule TDMDataSet5 will manage the cache. If I define the LifeCycle for this class as server it means only one cache instance for all clients “global cache”, defining it as Session means that I’m creating a cache for each client connected to the server.
On the client side using a native client as example we will get a DBXReader, it is up to your application to decide what to do with the data, but if you need to connect to this data with data-aware components, you just need to copy the DBXReader data into ClientDataSet, the TDBXDataSetReader.CopyReaderToClientDataSet method is the solution for that.
If your data related with the State table change you have to implement a server method to refresh the data. Also using LifeCycle as Server the cache will be destroyed at the moment you stop the server, but if you use LifeCycle as Session the cache will be destroyed when the client disconnect to the server.
Using this technique you garanty the cache on the server side independent of the client implementation, also I would like to remember this solution works for DataSnap Servers, if you are using DataSnap REST interface you won’t have cache because every server request works as invocation lifecycle.
This is one realistic example to explain how to implement cache on DataSnap Server using data from database as example, also you learned how to move data from/to ClientDataSet to/from DBXReader.
You can download the source code sample here, look at the unit DataSetDM5.pas (Server) and FormDataSet5 (client).