- What Is a Client Dataset?
- Advantages and Disadvantages of Client Datasets
- Creating Client Datasets
- Populating and Manipulating Client Datasets
- Navigating Client Datasets
- Client Dataset Indexes
- Filters and Ranges
- Searching
- Summary
Populating and Manipulating Client Datasets
After you create a client dataset (either at design-time or at runtime), you want to populate it with data. There are several ways to populate a client dataset: You can populate it manually through code, you can load the dataset's records from another dataset, or you can load the dataset from a file or a stream. The following sections discuss these methods, as well as how to modify and delete records.
Populating Manually
The most basic way to enter data into a client dataset is through the Append and Insert methods, which are supported by all datasets. The difference between them is that Append adds the new record at the end of the dataset, but Insert places the new record immediately before the current record.
I always use Append to insert new records because it's slightly faster than Insert. If the dataset is indexed, the new record is automatically sorted in the correct order anyway.
The following code snippet shows how to add a record to a client dataset:
cdsEmployee.Append; // You could use cdsEmployee.Insert; here as well cdsEmployee.FieldByName('ID').AsInteger := 5; cdsEmployee.FieldByName('FirstName').AsString := 'Eric'; cdsEmployee.Post;
Modifying Records
Modifying an existing record is almost identical to adding a new record. Rather than calling Append or Insert to create the new record, you call Edit to put the dataset into edit mode. The following code changes the first name of the current record to Fred.
cdsEmployee.Edit; // Edit the current record cdsEmployee.FieldByName('FirstName').AsString := 'Fred'; cdsEmployee.Post;
Deleting Records
To delete the current record, simply call the Delete method, like this:
cdsEmployee.Delete;
If you want to delete all records in the dataset, you can use EmptyDataSet instead, like this:
cdsEmployee.EmptyDataSet;
Populating from Another Dataset
dbExpress datasets are unidirectional and you can't scroll backward through them. This makes them incompatible with bidirectional, data-aware controls such as TDBGrid. However, TClientDataSet can load its data from another dataset (including dbExpress datasets, BDE datasets, or other client datasets) through a provider. Using this feature, you can load a client dataset from a unidirectional dbExpress dataset, and then connect a TDBGrid to the client dataset, providing bidirectional support.
Indeed, this capability is so powerful and important that it forms the basis for Delphi's multitier database support.
Populating from a File or Stream: Persisting Client Datasets
Though client datasets are located in RAM, you can save them to a file or a stream and reload them at a later point in time, making them persistent. This is the third method of populating a client dataset.
To save the dataset to a file, use the SaveToFile method, which is defined like this:
procedure SaveToFile(const FileName: string = ''; Format: TDataPacketFormat = dfBinary);
Similarly, to save the dataset to a stream, you call SaveToStream, which is defined as follows:
procedure SaveToStream(Stream: TStream; Format: TDataPacketFormat = dfBinary);
SaveToFile accepts the name of the file that you're saving to. If the filename is blank, the data is saved using the FileName property of the client dataset.
Both SaveToFile and SaveToStream take a parameter that indicates the format to use when saving data. Client datasets can be stored in one of three file formats: binary, or either flavor of XML. Table 3.1 lists the possible formats.
Table 3.1 Data Packet Formats for Loading and Saving Client Datasets
Value |
Description |
dfBinary |
Data is stored using a proprietary, binary format. |
dfXML |
Data is stored in XML format. Extended characters are represented using an escape sequence. |
dfXMLUTF8 |
Data is stored in XML format. Extended characters are represented using UTF8. |
When client datasets are stored to disk, they are referred to as MyBase files. MyBase stores one dataset per file, or per stream, unless you use nested datasets.
NOTE
If you're familiar with Microsoft ADO, you recall that ADO enables you to persist datasets using XML format. The XML formats used by ADO and MyBase are not compatible. In other words, you cannot save an ADO dataset to disk in XML format, and then read it into a client dataset (or vice versa).
Sometimes, you need to determine how many bytes are required to store the data contained in the client dataset. For example, you might want to check to see if there is enough room on a floppy disk before saving the data there, or you might need to preallocate the memory for a stream. In these cases, you can check the DataSize property, like this:
if ClientDataSet1.DataSize > AvailableSpace then ShowMessage('Not enough room to store the data');
DataSize always returns the amount of space necessary to store the data in binary format (dfBinary). XML format usually requires more space, perhaps twice as much (or even more).
NOTE
One way to determine the amount of space that's required to save the dataset in XML format is to save the dataset to a memory stream, and then obtain the size of the resulting stream.
Example: Creating, Populating, and Manipulating a Client Dataset
The following example illustrates how to create, populate, and manipulate a client dataset at runtime. Code is also provided to save the dataset to disk and to load it.
Listing 3.1 shows the complete source code for the CDS (ClientDataset) application.
Listing 3.1 CDSMainForm.pas
unit MainForm; interface uses SysUtils, Types, IdGlobal, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls, DB, DBClient, QExtCtrls, QGrids, QDBGrids, QActnList; const MAX_RECS = 10000; type TfrmMain = class(TForm) DataSource1: TDataSource; pnlClient: TPanel; pnlBottom: TPanel; btnPopulate: TButton; btnSave: TButton; btnLoad: TButton; ActionList1: TActionList; btnStatistics: TButton; Populate1: TAction; Statistics1: TAction; Load1: TAction; Save1: TAction; DBGrid1: TDBGrid; lblFeedback: TLabel; procedure FormCreate(Sender: TObject); procedure Populate1Execute(Sender: TObject); procedure Statistics1Execute(Sender: TObject); procedure Save1Execute(Sender: TObject); procedure Load1Execute(Sender: TObject); private { Private declarations } FCDS: TClientDataSet; public { Public declarations } end; var frmMain: TfrmMain; implementation {$R *.xfm} procedure TfrmMain.FormCreate(Sender: TObject); begin FCDS := TClientDataSet.Create(Self); FCDS.FieldDefs.Add('ID', ftInteger, 0, True); FCDS.FieldDefs.Add('Name', ftString, 20, True); FCDS.FieldDefs.Add('Birthday', ftDateTime, 0, True); FCDS.FieldDefs.Add('Salary', ftCurrency, 0, True); FCDS.CreateDataSet; DataSource1.DataSet := FCDS; end; procedure TfrmMain.Populate1Execute(Sender: TObject); const FirstNames: array[0 .. 19] of string = ('John', 'Sarah', 'Fred', 'Beth', 'Eric', 'Tina', 'Thomas', 'Judy', 'Robert', 'Angela', 'Tim', 'Traci', 'David', 'Paula', 'Bruce', 'Jessica', 'Richard', 'Carla', 'James', 'Mary'); LastNames: array[0 .. 11] of string = ('Parker', 'Johnson', 'Jones', 'Thompson', 'Smith', 'Baker', 'Wallace', 'Harper', 'Parson', 'Edwards', 'Mandel', 'Stone'); var Index: Integer; t1, t2: DWord; begin RandSeed := 0; t1 := GetTickCount; FCDS.DisableControls; try FCDS.EmptyDataSet; for Index := 1 to MAX_RECS do begin FCDS.Append; FCDS.FieldByName('ID').AsInteger := Index; FCDS.FieldByName('Name').AsString := FirstNames[Random(20)] + ' ' + LastNames[Random(12)]; FCDS.FieldByName('Birthday').AsDateTime := StrToDate('1/1/1950') + Random(10000); FCDS.FieldByName('Salary').AsFloat := 20000.0 + Random(600) * 100; FCDS.Post; end; FCDS.First; finally FCDS.EnableControls; end; t2 := GetTickCount; lblFeedback.Caption := Format('%d ms to load %.0n records', [t2 - t1, MAX_RECS * 1.0]); end; procedure TfrmMain.Statistics1Execute(Sender: TObject); var t1, t2: DWord; msLocateID: DWord; msLocateName: DWord; begin FCDS.First; t1 := GetTickCount; FCDS.Locate('ID', 9763, []); t2 := GetTickCount; msLocateID := t2 - t1; FCDS.First; t1 := GetTickCount; FCDS.Locate('Name', 'Eric Wallace', []); t2 := GetTickCount; msLocateName := t2 - t1; ShowMessage(Format('%d ms to locate ID 9763' + #13'%d ms to locate Eric Wallace' + #13'%.0n bytes required to store %.0n records', [msLocateID, msLocateName, FCDS.DataSize * 1.0, MAX_RECS * 1.0])); end; procedure TfrmMain.Save1Execute(Sender: TObject); var t1, t2: DWord; begin t1 := GetTickCount; FCDS.SaveToFile('C:\Employee.cds'); t2 := GetTickCount; lblFeedback.Caption := Format('%d ms to save data', [t2 - t1]); end; procedure TfrmMain.Load1Execute(Sender: TObject); var t1, t2: DWord; begin try t1 := GetTickCount; FCDS.LoadFromFile('C:\Employee.cds'); t2 := GetTickCount; lblFeedback.Caption := Format('%d ms to load data', [t2 - t1]); except FCDS.Open; raise; end; end; end.
There are five methods in this application and each one is worth investigating:
FormCreate creates the client dataset and its schema at runtime. It would actually be easier to create the dataset at design-time, but I wanted to show you the code required to do this at runtime. The code creates four fields: Employee ID, Name, Birthday, and Salary.
Populate1Execute loads the client dataset with 10,000 employees made up of random data. At the beginning of the method, I manually set RandSeed to 0 to ensure that multiple executions of the application would generate the same data.
NOTE
The Delphi Randomizer normally seeds itself with the current date and time. By manually seeding the Randomizer with a constant value, we can ensure that the random numbers generated are consistent every time we run the program.
The method calculates approximately how long it takes to generate the 10,000 employees, which on my computer is about half of a second.
Statistics1Execute simply measures the length of time required to perform a couple of Locate operations and calculates the amount of space necessary to store the data on disk (again, in binary format). I'll be discussing the Locate method later in this chapter.
Save1Execute saves the data to disk under the filename C:\Employee.cds. The .cds extension is standard, although not mandatory, for client datasets that are saved in a binary format. Client datasets stored in XML format generally have the extension .xml.
NOTE
Please make sure that you click the Save button because the file created (C:\EMPLOYEE.CDS) is used in the rest of the example applications in this chapter, as well as some of the examples in the following chapter.
Load1Execute loads the data from a file into the client dataset. If LoadFromFile fails (presumably because the file doesn't exist or is not a valid file format), the client dataset is left in a closed state. For this reason, I reopen the client dataset when an exception is raised.
Figure 3.5 shows the CDS application running on my computer. Note the impressive times posted to locate a record. Even when searching through almost the entire dataset to find ID 9763, it only takes approximately 10 ms on my computer.
Figure 3.5 The CDS application at runtime.