Sunday, December 9, 2007

Delphi's Database Architecture

Accessing a Database: dbExpress, Local Data,
and Other Alternatives
The early incarnations of Delphi—immediately adopted as a tool for building database-oriented applications—could access a database only by means of the Borland Database Engine (BDE). Starting with Delphi 3, the portion of VCL related to database access was restructured to open it up to multiple database access solutions, which currently include ADO, native InterBase components, the dbExpress library, and the BDE. Many third-party vendors are now able to offer alternative database access mechanisms to a variety of data formats (some not accessible through Borland components) and still provide a solution integrated with Delphi's VCL.

The dbExpress Library

One of the most relevant new features of Delphi in the recent years has been the introduction of the dbExpress database library (DBX), available both for the Linux and the Windows platforms. I say library and not database engine because, unlike other solutions, dbExpress uses a lightweight approach and requires basically no configuration on end-user machines.

Being light and portable are the two key characteristics of dbExpress; Borland introduced it for those reasons, along with the development of the Kylix project. Compared to other powerhouses, dbExpress is very limited in its capabilities. It can access only SQL servers (no local files); it has no caching capabilities and provides only unidirectional access to the data; and it can natively work only with SQL queries and is unable to generate the corresponding SQL update statements.

At first, you might think these limitations make the library useless. On the contrary: These are features that make it interesting. Unidirectional datasets with no direct update are the norm if you need to produce reports, including generating HTML pages showing the content of a database. If you want to build a user interface to edit the data, however, Delphi includes specific components (the ClientDataSet and Provider, in particular) that provide caching and query resolution. These components allow your dbExpress-based application much more control than a separate (monolithic) database engine, which does extra things for you but often does them the way it wants to, not the way you would like.

The Borland Database Engine

Delphi still ships with the BDE, which allows you to access local database formats (like Paradox and dBase) and SQL servers as well as anything accessible through ODBC drivers. This was the standard database technology in early versions of Delphi, but Borland now considers it obsolete. This is particularly true for the use of the BDE to access SQL servers through the SQL Links drivers. Using the BDE to access local tables is still officially supported, simply because Borland doesn't provide a direct migration path for this type of application.

In some cases, a local table can be replaced with the ClientDataSet component (MyBase) specifically for temporary and small lookup tables. However, this approach won't work for larger local tables, because MyBase requires the entire table to be loaded in memory to access even a single record. The suggestion is to move larger tables to a SQL server installed on the client computer. InterBase, with its small footprint, is ideal in this particular situation. This type of migration will also open to you the doors of Linux, where the BDE is not available.

InterBase Express

Borland has made available another set of database access components for Delphi: InterBase Express (IBX). These components are specifically tailored to Borland's own InterBase server. Unlike dbExpress, this is not a server-independent database engine, but a set of components for accessing a specific database server. If you plan to use only InterBase as your back-end database, using a specific set of components can give you more control over the server, provide the best performance, and allow you to configure and maintain the server from within a custom client application.

MyBase: Stand-alone ClientDataSet

If you want to write a single-user database application in Delphi, the simplest approach is to use the ClientDataSet component and map it to a local file. This local file mapping is different from the traditional data mapping to a local file. The traditional approach is to read from the file a record at a time, and possibly have a second file that stores indexes. The ClientDataSet maps an entire table (and possibly a master/detail structure) to the file in its entirety: When a program starts, the entire file is loaded in memory, and then everything is saved at once.

The DFM File of the MyBase1 Sample Program

  ActiveControl = DBGrid1
Caption = 'MyBase1'
OnCreate = FormCreate
object DBGrid1: TDBGrid
DataSource = DataSource1
end
object DataSource1: TDataSource
DataSet = cds
end
object cds: TClientDataSet
FileName = 'C:\Program Files\Common Files\Borland

Shared\Data\customer.cds'
end
end
object Form1: TForm1

Using Data-Aware Controls

Once you set up the proper data-access components, you can build a user interface to let a user view the data and eventually edit it. Delphi provides many components that resemble the usual controls but are data-aware. For example, the DBEdit component is similar to the Edit component, and the DBCheckBox component corresponds to the CheckBox component. You can find all these components in the Data Controls page of the Delphi Component Palette.

All these components are connected to a data source using the corresponding property, DataSource. Some of them relate to the entire dataset, such as the DBGrid and DBNavigator components, and the others refer to a specific field of the data source, as indicated by the DataField property. Once you select the DataSource property, the DataField property editor will contain a list of available values.

Data in a Grid

The DBGrid is a grid capable of displaying a whole table at once. It allows scrolling and navigation, and you can edit the grid's contents. It is an extension of the other Delphi grid controls.

You can customize the DBGrid by setting its Options property's various flags and modifying its Columns collection. The grid allows a user to navigate the data using the scrollbars and perform all the major actions. A user can edit the data directly, insert a new record in a given position by pressing the Insert key, append a new record at the end by going to the last record and pressing the Down arrow key, and delete the current record by pressing Ctrl+Del.

The DataSet Component

Instead of proceeding with the discussion of the capabilities of a specific dataset at this point, I prefer to devote some space to a generic introduction of the features of the TDataSet class, which are shared by all inherited data-access classes. The DataSet component is very complex, so I won't list all its capabilities—I will only discuss its core elements.

This component provides access to a series of records that are read from some source of data, kept in internal buffers (for performance reasons), and eventually modified by a user, with the possibility of writing back changes to the persistent storage. This approach is generic enough to be applied to different types of data (even non-database data), but it has a few rules:

  • There can be only one active record at a time, so if you need to access data in multiple records, you must move to each of them, read the data, then move again, and so on. You'll find an example of this and related techniques in the section "Navigating a Dataset."

  • You can edit only the active record: You cannot modify a set of records at the same time, as you can in a relational database.

  • You can modify data in the active buffer only after you explicitly declare you want to do so, by giving the Edit command to the dataset. You can also use the Insert command to create a new blank record and close both operations (insert or edit) by giving a Post command.

Other interesting elements of a dataset that I'll explore in the following sections are its status (and the status change events), navigation and record positions, and the role of field objects. As a summary of the capabilities of the DataSet component, I included the public methods of its class in Listing 13.2 (the code has been edited and commented for clarity). Not all of these methods are directly used everyday, but I kept them all in the listing.

Listing 13.2: The Public Interface of the TDataSet Class (Excerpted)
Start example
TDataSet = class(TComponent, IProviderSupport)
...
public
// create and destroy, open and close
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Open;
procedure Close;
property BeforeOpen: TDataSetNotifyEvent read FBeforeOpen write FBeforeOpen;
property AfterOpen: TDataSetNotifyEvent read FAfterOpen write FAfterOpen;
property BeforeClose: TDataSetNotifyEvent
read FBeforeClose write FBeforeClose;
property AfterClose: TDataSetNotifyEvent read FAfterClose write FAfterClose;

// status information
function IsEmpty: Boolean;
property Active: Boolean read GetActive write SetActive default False;
property State: TDataSetState read FState;
function ActiveBuffer: PChar;
property IsUniDirectional: Boolean
read FIsUniDirectional write FIsUniDirectional default False;
function UpdateStatus: TUpdateStatus; virtual;
property RecordSize: Word read GetRecordSize;
property ObjectView: Boolean read FObjectView write SetObjectView;
property RecordCount: Integer read GetRecordCount;
function IsSequenced: Boolean; virtual;
function IsLinkedTo(DataSource: TDataSource): Boolean;

// datasource
property DataSource: TDataSource read GetDataSource;
procedure DisableControls;
procedure EnableControls;
function ControlsDisabled: Boolean;

// fields, including blobs, details, calculated, and more
function FieldByName(const FieldName: string): TField;
function FindField(const FieldName: string): TField;
procedure GetFieldList(List: TList; const FieldNames: string);
procedure GetFieldNames(List: TStrings); virtual; // virtual since Delphi 7
property FieldCount: Integer read GetFieldCount;
property FieldDefs: TFieldDefs read FFieldDefs write SetFieldDefs;
property FieldDefList: TFieldDefList read FFieldDefList;
property Fields: TFields read FFields;
property FieldList: TFieldList read FFieldList;
property FieldValues[const FieldName: string]: Variant
read GetFieldValue write SetFieldValue; default;
property AggFields: TFields read FAggFields;
property DataSetField: TDataSetField
read FDataSetField write SetDataSetField;
property DefaultFields: Boolean read FDefaultFields;
procedure ClearFields;
function GetBlobFieldData(FieldNo: Integer;
var Buffer: TBlobByteData): Integer; virtual;
function CreateBlobStream(Field: TField;
Mode: TBlobStreamMode): TStream; virtual;
function GetFieldData(Field: TField;
Buffer: Pointer): Boolean; overload; virtual;
procedure GetDetailDataSets(List: TList); virtual;
procedure GetDetailLinkFields(MasterFields, DetailFields: TList); virtual;
function GetFieldData(FieldNo: Integer;
Buffer: Pointer): Boolean; overload; virtual;
function GetFieldData(Field: TField; Buffer: Pointer; NativeFormat: Boolean):
Boolean; overload; virtual;
property AutoCalcFields: Boolean
read FAutoCalcFields write FAutoCalcFields default True;
property OnCalcFields: TDataSetNotifyEvent
read FOnCalcFields write FOnCalcFields;

// position, movement
procedure CheckBrowseMode;
procedure First;
procedure Last;
procedure Next;
procedure Prior;
function MoveBy(Distance: Integer): Integer;
property RecNo: Integer read GetRecNo write SetRecNo;
property Bof: Boolean read FBOF;
property Eof: Boolean read FEOF;
procedure CursorPosChanged;
property BeforeScroll: TDataSetNotifyEvent
read FBeforeScroll write FBeforeScroll;
property AfterScroll: TDataSetNotifyEvent
read FAfterScroll write FAfterScroll;

// bookmarks
procedure FreeBookmark(Bookmark: TBookmark); virtual;
function GetBookmark: TBookmark; virtual;
function BookmarkValid(Bookmark: TBookmark): Boolean; virtual;
procedure GotoBookmark(Bookmark: TBookmark);
function CompareBookmarks(Bookmark1, Bookmark2: TBookmark): Integer; virtual;
property Bookmark: TBookmarkStr read GetBookmarkStr write SetBookmarkStr;

// find, locate
function FindFirst: Boolean;
function FindLast: Boolean;
function FindNext: Boolean;
function FindPrior: Boolean;
property Found: Boolean read GetFound;
function Locate(const KeyFields: string; const KeyValues: Variant;
Options: TLocateOptions): Boolean; virtual;
function Lookup(const KeyFields: string; const KeyValues: Variant;
const ResultFields: string): Variant; virtual;

// filtering
property Filter: string read FFilterText write SetFilterText;
property Filtered: Boolean read FFiltered write SetFiltered default False;
property FilterOptions: TFilterOptions
read FFilterOptions write SetFilterOptions default [];
property OnFilterRecord: TFilterRecordEvent
read FOnFilterRecord write SetOnFilterRecord;

// refreshing, updating
procedure Refresh;
property BeforeRefresh: TDataSetNotifyEvent
read FBeforeRefresh write FBeforeRefresh;
property AfterRefresh: TDataSetNotifyEvent
read FAfterRefresh write FAfterRefresh;
procedure UpdateCursorPos;
procedure UpdateRecord;
function GetCurrentRecord(Buffer: PChar): Boolean; virtual;
procedure Resync(Mode: TResyncMode); virtual;

// editing, inserting, posting, and deleting
property CanModify: Boolean read GetCanModify;
property Modified: Boolean read FModified;
procedure Append;
procedure Edit;
procedure Insert;
procedure Cancel; virtual;
procedure Delete;
procedure Post; virtual;
procedure AppendRecord(const Values: array of const);
procedure InsertRecord(const Values: array of const);
procedure SetFields(const Values: array of const);

// events related to editing, inserting, posting, and deleting
property BeforeInsert: TDataSetNotifyEvent
read FBeforeInsert write FBeforeInsert;
property AfterInsert: TDataSetNotifyEvent
read FAfterInsert write FAfterInsert;
property BeforeEdit: TDataSetNotifyEvent read FBeforeEdit write FBeforeEdit;
property AfterEdit: TDataSetNotifyEvent read FAfterEdit write FAfterEdit;
property BeforePost: TDataSetNotifyEvent read FBeforePost write FBeforePost;
property AfterPost: TDataSetNotifyEvent read FAfterPost write FAfterPost;
property BeforeCancel: TDataSetNotifyEvent
read FBeforeCancel write FBeforeCancel;
property AfterCancel: TDataSetNotifyEvent
read FAfterCancel write FAfterCancel;
property BeforeDelete: TDataSetNotifyEvent
read FBeforeDelete write FBeforeDelete;
property AfterDelete: TDataSetNotifyEvent
read FAfterDelete write FAfterDelete;
property OnDeleteError: TDataSetErrorEvent
read FOnDeleteError write FOnDeleteError;
property OnEditError: TDataSetErrorEvent
read FOnEditError write FOnEditError;
property OnNewRecord: TDataSetNotifyEvent
read FOnNewRecord write FOnNewRecord;
property OnPostError: TDataSetErrorEvent
read FOnPostError write FOnPostError;

// support utilities
function Translate(Src, Dest: PChar;
ToOem: Boolean): Integer; virtual;
property Designer: TDataSetDesigner read FDesigner;
property BlockReadSize: Integer read FBlockReadSize write SetBlockReadSize;
property SparseArrays: Boolean read FSparseArrays write SetSparseArrays;
end;

No comments: