Hallo...
Das ist mein erstes Tutorial. Bitte seid gnädig... Es soll als Anregung dienen wie man auch ohne datensensitive Controls auskommt.
Ich möchte Euch zeigen, wie man programmintern mit Objekten arbeitet und diese Objekte in einer normalisierten Datenbank speichert. Im Prinzip ist es
ein Mini-ORM ohne externes Framework. Der Kreativität sind keine Grenzen gesetzt.
Ich habe versucht das einfach zu halten. Mancher Code könnte mit unterschiedlichen Methoden realisiert werden. (JOIN statt separater procedure) Welche Variante
man nimmt, ist jedem selbst überlassen.
Hinweise:
* Weil die Units zu lang sind... siehe Projekt.
* Weil nicht alle das compilieren können ist die EXE zum Ausprobieren dabei... siehe Projekt
* Die Zwischenvariablen (wie var Customer: TCustomer; in Main) sind nicht immer notwendig aber es macht es für den Anfang
imho übersichtlicher.
Anhänge:
* Projekt mit Datenbank ausführbar.
* kompletter Quelltext D10.1
Voraussetzungen:
Delphi XE und höher wegen Generics im Beispiel
Aufbau 3 Schicht Anwendung:
(
Unit: FormMain) -> (
Unit: Logic) -> (
Unit: Database) -> (Database: z.B.Firebird)
<- (Event) <- (Event)
Database (beliebiebiges DBMS)
Code:
CREATE TABLE T_CUSTOMER (
ID INTEGER NOT NULL,
F_NAME VARCHAR(50),
F_FIRST_NAME VARCHAR(50),
F_ADDRESS_ID INTEGER
);
CREATE TABLE T_ADDRESS (
ID INTEGER NOT NULL,
F_POSTCODE VARCHAR(30),
F_TOWN VARCHAR(50),
F_STREET VARCHAR(50),
F_HOUSE_NUMBER VARCHAR(10)
);
Unit: Database
In dieser
Unit ist die Schnittstelle zur Datenbank definiert. In diesem Falle als Interface. Die Logik kennt nur das Interface welches in der Logik instanziert wird. (siehe
Unit: Logik)
Die Querys werden nicht mehr auf die Form "geklatscht" und dort die
SQL eingetragen. Das Interface kennt alleinig die
SQL Statements.
Das macht es einfacher mehrere Datenbanken anzubinden. Für jede Datenbank gibt es dann ein eigenes Interface wegen der Unterschiede der möglichen Datenbanken.
Als Datenbankkomponenten kommen hier die UniDAC mit der Schnittstelle zu Firebird zum Einsatz. Andere Komponenten sind natürlich auch möglich.
Diese eine
Unit kennt allein die Datenbank. Die Kommunikation mit der Logik, welche die
Unit nicht kennt, kann sowohl über Events oder als Rückgabe der Funktion
aus der Logik erfolgen.
Interface:
Delphi-Quellcode:
unit Database.Interfaces;
interface
uses
Logic.DataClasses,
Database.Events;
type
IDatabaseCommon =
interface(IInterface)
// ggf. in seperate Unit bei mehreren DBMS
['
{E41ADEE8-56F9-4223-8238-61B6C033DFF1}']
function GetAfterConnect: TOnAfterConnectEvent;
procedure SetAfterConnect(
const Value: TOnAfterConnectEvent);
function GetDatabaseError: TOnDatabaseErrorEvent;
procedure SetDatabaseError(
const Value: TOnDatabaseErrorEvent);
function GetAfterDisconnect: TOnAfterDisconnectEvent;
procedure SetAfterDisconnect(
const Value: TOnAfterDisconnectEvent);
property OnAfterConnect: TOnAfterConnectEvent
read GetAfterConnect
write SetAfterConnect;
property OnAfterDisconnect: TOnAfterDisconnectEvent
read GetAfterDisconnect
write SetAfterDisconnect;
property OnDatabaseError: TOnDatabaseErrorEvent
read GetDatabaseError
write SetDatabaseError;
function Connect: Boolean;
procedure Disconnect;
procedure StartTransaction;
procedure Commit;
procedure Rollback;
function GetSQLByName(SQLName:
string):
string;
// ggf. bei Laden des SQL Statements aus Ressource
end;
IDatabase =
interface(IDatabaseCommon)
['
{C1BC6FE3-9586-4D92-8221-A3DD030E80B5}']
// Entweder overload oder als einzelne Prozeduren, der Creativität sind keine Grenzen gesetzt.
procedure FillList(List: TCustomerList);
overload;
function Save(Customer: TCustomer): Integer;
overload;
function Save(Address: TAddress): Integer;
overload;
procedure Get(Customer: TCustomer; ID: Integer);
overload;
procedure Get(Address: TAddress; ID: Integer);
overload;
// kann auch separat genutzt werden...oder auch nicht
end;
implementation
end.
Database gekürzt:
Delphi-Quellcode:
unit Database.Firebird;
interface
uses
System.Classes, System.SysUtils, System.Variants, System.Generics.Collections, System.Generics.Defaults, System.DateUtils,
Uni, DBAccess, InterBaseUniProvider,
Database.Interfaces, Database.Events,
Logic.DataClasses;
type
TDatabaseFirebird =
class(TInterfacedObject, IDatabase)
strict private
// Properties Connection
FConnection: TUniConnection;
FOnAfterConnect: TOnAfterConnectEvent;
FOnAfterDisconnect: TOnAfterDisconnectEvent;
FOnDatabaseError: TOnDatabaseErrorEvent;
// Getter / Setter
function GetAfterConnect: TOnAfterConnectEvent;
procedure SetAfterConnect(
const Value: TOnAfterConnectEvent);
function GetDatabaseError: TOnDatabaseErrorEvent;
procedure SetDatabaseError(
const Value: TOnDatabaseErrorEvent);
function GetAfterDisconnect: TOnAfterDisconnectEvent;
procedure SetAfterDisconnect(
const Value: TOnAfterDisconnectEvent);
// Eventhandler
procedure DoAfterConnect(Sender: TObject);
procedure DoAfterDisconnect(Sender: TObject);
procedure DoError(Sender: TObject; E: EDAError;
var Fail: Boolean);
// Funktionen
function CreateQuery: TUniQuery;
public
constructor Create;
destructor Destroy;
override;
// Events
property OnAfterConnect: TOnAfterConnectEvent
read GetAfterConnect
write SetAfterConnect;
property OnAfterDisconnect: TOnAfterDisconnectEvent
read GetAfterDisconnect
write SetAfterDisconnect;
property OnDatabaseError: TOnDatabaseErrorEvent
read GetDatabaseError
write SetDatabaseError;
// Funktionen aus Interface
function Connect: Boolean;
procedure Disconnect;
procedure StartTransaction;
procedure Commit;
procedure Rollback;
function GetSQLByName(SQLName:
string):
string;
// ggf. bei Laden des SQL Statements aus Ressource
procedure FillList(List: TCustomerList);
overload;
function Save(Customer: TCustomer): Integer;
overload;
function Save(Address: TAddress): Integer;
overload;
procedure Get(Customer: TCustomer; ID: Integer);
overload;
procedure Get(Address: TAddress; ID: Integer);
overload;
// kann auch separat genutzt werden...oder auch nicht
end;
...
Unit: Logic
In dieser
Unit ist die Logik definiert. Die Logik nimmt die Befehle der Form entgegen und führt diese aus. Desweiteren hällt die Logik die Daten der Anwendung. In diesem
Falle die CustomerList. Die Kommunikation mit der Form, welche die
Unit nicht kennt, kann sowohl über Events oder als Rückgabe der Function aus der Logik erfolgen.
Klassendefinition gekürzt:
Delphi-Quellcode:
unit Logic.DataClasses;
interface
uses
System.Generics.Collections, System.Generics.Defaults;
type
TDataState = (ddsNormal, ddsNew, ddsModified, ddsDeleted);
TBaseClass =
class
strict protected
FID: Integer;
FState: TDataState;
// jedes Objekt kennt seinen Status
public
property ID: Integer
read FID
write FID;
property State: TDataState
read FState
write FState;
end;
TAddress =
class(TBaseClass)
strict private
FTown:
string;
FStreet:
string;
FPostCode:
string;
FHouseNumber:
string;
public
constructor Create;
destructor Destroy;
override;
property PostCode:
string read FPostCode
write FPostCode;
property Town:
string read FTown
write FTown;
property Street:
string read FStreet
write FStreet;
property HouseNumber:
string read FHouseNumber
write FHouseNumber;
end;
TCustomer =
class(TBaseClass)
strict private
FName:
string;
FAddress: TAddress;
FFirstName:
string;
public
constructor Create;
destructor Destroy;
override;
property Name:
string read FName
write FName;
property FirstName:
string read FFirstName
write FFirstName;
property Address: TAddress
read FAddress
write FAddress;
end;
TCustomerList = TObjectList<TCustomer>;
...
Logic gekürzt:
Delphi-Quellcode:
unit Logic.Base;
interface
uses
Database.Interfaces, Database.Firebird, Database.Events,
Logic.DataClasses;
type
TOnFillCustomerListEvent =
procedure(Sender: TObject; List: TCustomerList)
of object;
TOnGetCustomerEvent =
procedure(Sender: TObject; Customer: TCustomer)
of object;
TOnDataChangedEvent =
procedure(Sender: TObject; State: Boolean)
of object;
TLogic =
class
strict private
FDatabase: IDatabase;
FCustomerList: TCustomerList;
FOnConnectDatabase: TOnAfterConnectEvent;
FOnDisconnectDatabase: TOnAfterDisconnectEvent;
FOnDatabaseError: TOnDatabaseErrorEvent;
FOnFillCustomerList: TOnFillCustomerListEvent;
FOnGetCustomer: TOnGetCustomerEvent;
procedure DoOnDatabaseError(Sender: TObject; ErrorNumber: Integer; ErrorMessage:
string);
procedure DoOnAfterConnect(Sender: TObject);
procedure DoOnAfterDisconnect(Sender: TObject);
private
FDataChanged: Boolean;
FOnDataChanged: TOnDataChangedEvent;
procedure SetDataChanged(
const Value: Boolean);
public
constructor Create;
destructor Destroy;
override;
property OnConnectDatabase: TOnAfterConnectEvent
read FOnConnectDatabase
write FOnConnectDatabase;
property OnDisconnectDatabase: TOnAfterDisconnectEvent
read FOnDisconnectDatabase
write FOnDisconnectDatabase;
property OnDatabaseError: TOnDatabaseErrorEvent
read FOnDatabaseError
write FOnDatabaseError;
property OnFillCustomerList: TOnFillCustomerListEvent
read FOnFillCustomerList
write FOnFillCustomerList;
property OnGetCustomer: TOnGetCustomerEvent
read FOnGetCustomer
write FOnGetCustomer;
property OnDataChanged: TOnDataChangedEvent
read FOnDataChanged
write FOnDataChanged;
property DataChanged: Boolean
read FDataChanged
write SetDataChanged;
property CustomerList: TCustomerList
read FCustomerList;
procedure GetCustomerList;
procedure GetCustomer(ID: Integer);
procedure SaveCustomer(Customer: TCustomer);
procedure RefreshCustomerList;
end;
...
Unit: FormMain gekürzt
In dieser
Unit ist die Form mit den Controls definiert. Die Form gibt der Logic Befehle was sie an Informationen haben möchte. Über Events werden die Information aus der Logik
zurückgeliefert und verarbeitet.
Delphi-Quellcode:
unit FormMain;
interface
uses
Winapi.Windows,
Winapi.Messages,
System.SysUtils, System.Variants, System.Actions, System.Classes,
Vcl.Graphics,
Vcl.Controls,
Vcl.Forms,
Vcl.Dialogs,
Vcl.ExtCtrls,
Vcl.StdCtrls,
Vcl.ActnList,
Vcl.ComCtrls,
ImageList.Small,
Logic.Base, Logic.DataClasses;
const
conTextGroupboxNormal = '
Details (Normal)';
conTextGroupboxEdit = '
Details (Editmodus)';
type
TfoMain =
class(TForm)
pnlTop: TPanel;
pnlContent: TPanel;
lvCustomers: TListView;
btnNew: TButton;
btnCopy: TButton;
btnDelete: TButton;
grpDetails: TGroupBox;
btnSave: TButton;
edtName: TEdit;
lblName: TLabel;
edtFirstName: TEdit;
lblFirstName: TLabel;
edtPostCode: TEdit;
lblPostCode: TLabel;
edtTown: TEdit;
lblTown: TLabel;
edtStreet: TEdit;
lblStreet: TLabel;
edtHouseNumber: TEdit;
lblHouseNumber: TLabel;
actlstMain: TActionList;
actNew: TAction;
btnInfo: TButton;
actCopy: TAction;
actDelete: TAction;
actInfo: TAction;
actSave: TAction;
actCancel: TAction;
btnCancel: TButton;
btnMessage: TButton;
actMessage: TAction;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormShow(Sender: TObject);
procedure actNewExecute(Sender: TObject);
procedure actCopyExecute(Sender: TObject);
procedure actDeleteExecute(Sender: TObject);
procedure actInfoExecute(Sender: TObject);
procedure actSaveExecute(Sender: TObject);
procedure actCancelExecute(Sender: TObject);
procedure lvCustomersChange(Sender: TObject; Item: TListItem; Change: TItemChange);
procedure actMessageExecute(Sender: TObject);
private
FLogic: TLogic;
procedure DoFillCustomerList(Sender: TObject; List: TCustomerList);
procedure DoGetCustomer(Sender: TObject; Customer: TCustomer);
procedure DoDataChanged(Sender: TObject; State: Boolean);
procedure ShowCustomerList(List: TCustomerList);
procedure ShowCustomer(Customer: TCustomer);
procedure SetCustomerToEdit(Active: Boolean);
procedure SetButtons(Active: Boolean);
public
end;
var
foMain: TfoMain;
...
Prinzip des Datenholens:
Delphi-Quellcode:
procedure TDatabaseFirebird.Get(Customer: TCustomer; ID: Integer);
// nur einen Customer holen
var
Qry: TUniQuery;
begin
Qry := CreateQuery;
// Query incl. der Connection erzeugen
try
// SQL wie gehabt
Qry.SQL.Text := '
SELECT * FROM T_CUSTOMER WHERE ID = :ID';
// Alternativ über Ressource: Qry.SQL.Text := GetSQLByName('xxx'); // SQL Name ergänzen
Qry.ParamByName('
ID').AsInteger := ID;
Qry.Open;
// das Objekt füllen
Customer.ID := Qry.FieldByName('
ID').AsInteger;
Customer.
Name := Qry.FieldByName('
F_NAME').AsString;
Customer.FirstName := Qry.FieldByName('
F_FIRST_NAME').AsString;
// Alternative für GET wäre ein JOIN im Statement und die Adresse hier zusammenbauen.
// Der Vorteil der Trennung: Man kann auch die Adresse seperat lesen. Wie man es braucht... :-)
Get(Customer.Address, Qry.FieldByName('
F_ADDRESS_ID').AsInteger);
Customer.State := ddsNormal;
// Wichtig: Status setzen
finally
Qry.Free;
end;
end;
...
procedure TDatabaseFirebird.FillList(List: TCustomerList);
// komplette Liste füllen
var
Qry: TUniQuery;
Customer: TCustomer;
begin
List.Clear;
Qry := CreateQuery;
// Query incl. der Connection erzeugen
try
Qry.SQL.Text := '
SELECT * FROM T_CUSTOMER';
// Alternativ über Ressource: Qry.SQL.Text := GetSQLByName('xxx'); // SQL Name ergänzen
Qry.Open;
while not Qry.Eof
do
begin
Customer := TCustomer.Create;
// Objekt erzeuggen
Get(Customer, Qry.FieldByName('
ID').AsInteger);
// Objekt füllen
List.Add(Customer);
// Objekt in Liste legen
Qry.Next;
end;
finally
Qry.Free;
end;
end;
Prinzip der Speicherung
Delphi-Quellcode:
function TDatabaseFirebird.Save(Customer: TCustomer): Integer;
// Speichern
var
Qry: TUniQuery;
begin
Result := -1;
Qry := CreateQuery;
try
StartTransaction;
try
case Customer.State
of // entsprechend dem Status des Objektes
ddsNew:
// insert
begin
Customer.Address.ID := Save(Customer.Address);
// Rückgabe der ID als Erstes wegen ID
Qry.SQL.Text := '
INSERT INTO T_CUSTOMER (F_NAME, F_FIRST_NAME, F_ADDRESS_ID) VALUES (:NAM, :FIN, :ADD) returning ID';
// Alternativ über Ressource: Qry.SQL.Text := GetSQLByName('xxx'); // SQL Name ergänzen
// das Objekt dem SQL übergeben
Qry.ParamByName('
NAM').AsString := Customer.
Name;
Qry.ParamByName('
FIN').AsString := Customer.FirstName;
Qry.ParamByName('
ADD').AsInteger := Customer.Address.ID;
Qry.ExecSQL;
Customer.ID := Qry.ParamByName('
RET_ID').AsInteger;
Customer.State := ddsNormal;
Result := Customer.ID;
end;
ddsModified:
// update
begin
Qry.SQL.Text := '
UPDATE T_CUSTOMER SET F_NAME = :NAM, F_FIRST_NAME = :FIN, F_ADDRESS_ID = :ADD WHERE ID = :ID';
// Alternativ über Ressource: Qry.SQL.Text := GetSQLByName('xxx'); // SQL Name ergänzen
// das Objekt dem SQL übergeben
Qry.ParamByName('
ID').AsInteger := Customer.ID;
Qry.ParamByName('
NAM').AsString := Customer.
Name;
Qry.ParamByName('
FIN').AsString := Customer.FirstName;
Qry.ParamByName('
ADD').AsInteger := Customer.Address.ID;
Qry.ExecSQL;
Save(Customer.Address);
Customer.State := ddsNormal;
Customer.Address.State := ddsNormal;
Result := Customer.ID;
end;
ddsDeleted:
// deleted
begin
Qry.SQL.Text := '
DELETE FROM T_CUSTOMER WHERE ID = :ID';
// Alternativ über Ressource: Qry.SQL.Text := GetSQLByName('xxx'); // SQL Name ergänzen
Qry.ParamByName('
ID').AsInteger := Customer.ID;
Qry.ExecSQL;
Customer.Address.State := ddsDeleted;
Save(Customer.Address);
Result := Customer.ID;
end;
end;
Commit;
except
Rollback;
end;
finally
Qry.Free;
end;
end;
Prinzip der Datenanzeige
Delphi-Quellcode:
procedure TfoMain.DoFillCustomerList(Sender: TObject; List: TCustomerList); // Event nach dem Datenholen
begin
ShowCustomerList(List);
SetCustomerToEdit(False);
end;
...
procedure TfoMain.ShowCustomerList(List: TCustomerList);
var
Item: TListItem;
Customer: TCustomer;
begin
lvCustomers.Items.Clear;
for Customer in List do
begin
Item:= lvCustomers.Items.Add;
Item.Data:= Customer; // Das Objekt (Pointer) hängt an dem Eintrag
Item.SubItems.Add(Customer.Name);
Item.SubItems.Add(Customer.FirstName);
Item.SubItems.Add(Customer.Address.Town);
Item.ImageIndex:= dmSmall.GetIconIndexDataState(Customer.State);
end;
lvCustomers.Items.Item[0].Selected := True; // ersten Eintrag markieren...oder so
end;
Erweiterungen:
Nach Bedarf können u.a. Funktionen hinzugefügt oder mit einander kombiniert werden. (DRY)
Vorteile:
* Keine datensensitiven Controls. Das bedeutet Unabhängigkeit von der Optik der
DB sensitiven Controls.
* Eine Property des Objektes kann z.B. in einem TEdit einem TMemo oder mit einem TRotMitGelbenPunktenControl dargestellt werden.
* Alle
SQL Statements auf einem Fleck. Das erleichtert das Suchen nach einem Statement. Die
SQL sind nicht mehr auf den gesamten QT verteilt.
* Die Umbauten bei Datenbankwechsel beziehen sich nur auf das Interface.
* Durch die Objekte kann man sich die Informationen beliebig zustammenstellen. Auch wenn sie auch verschieden Tabelle stammen.
* Speichern mit einem Einzeiler
Delphi-Quellcode:
FDatabase.Save(Customer);
...
* Objekte sind besser debugbar.
* Objekte können wiederum Listen mit Objekten enthalten.
* Objekte sind mit einem Rutsch, über das Database Interface, speicherbar.
Bei diesem Tutorial geht es ums Prinzip bei der Arbeit mit Objekten ohne die üblichen Verdächtigen der großen Frameworks für Datenbanken. Meistens lohnt der Aufwand nicht
ein großes Framework zu installieren. Auch der Einarbeitungsaufwand bei diesen ist nicht ohne... Manchmal ist weniger mehr.
Diskussion eröffnet.
PS: Alle Fehler sind urheberrechtlich geschützt weil Unikate.