Nedávno zde Daniel popisoval ORM z praxe a já jsem se rozhodl, že se sám podívám na EntityDAC od DevArt s LINQ podporou. Považuji DevArt za hodně kvalitní vývojáře a proto jsem byl zvědav s čím mne překvapí. A není to špatné.
Úplně první co mne zaujalo, je že EntityDac podporuje 4 způsoby mapování dat na objekty DB a to vše ve vlastním designeru, který umí načíst model podle DB (nebo naopak vytvořit model a pak z něho vytvořit DB). EntityDac je dostupný v několika edicích včetně free express edice.
Na začátek ještě upozorním, že z hlediska přístupu k DB EntityDac podporuje přístup přes UniDAC, FireDac, ADO, DbExpress a další.
Takže když si vybereme šablonu (template) mapování (DevArt doporučuje pro pochopení první - na bázi TObject, mně se naopak zdá mnohem lepší Attribute mapped Entities), nástroj se připojí do DB, dá Vám vybrat tabulky, view, SP atd. a podle toho vygeneruje model.
Model se dá v nástroji upravovat a přizpůsobovat.
SQL ukázka
V demo programu jsou dvě tabulky:
CREATE TABLE DEPT (
DEPTNO INTEGER NOT NULL PRIMARY KEY,
DNAME VARCHAR(14),
LOC VARCHAR(13)
);
--
CREATE TABLE EMP (
EMPNO INTEGER NOT NULL PRIMARY KEY,
ENAME VARCHAR(10),
JOB VARCHAR(9),
MGR INTEGER,
HIREDATE TIMESTAMP,
SAL INTEGER,
COMM INTEGER,
DEPTNO INTEGER REFERENCES DEPT (DEPTNO)
);
Nyní můžeme přistoupit ke generování kódu. Podle vybrané šablony se vygeneruje několik tříd (jejich názvy se dají volit v dialogu na obrázku - viz. output name for Linq).
EntityDac v mé případě (zvolená šablona) vygeneruje popis tabulky ve stylu:
[Table('EMP')]
[Model('DemoEntity')]
[Key('FEmpno')]
TEmp = class(TMappedEntity)
private
[Column('EMPNO')]
FEmpno: Integer;
[Column('ENAME', 10, [CanBeNull])]
FEname: String;
[Column('JOB', 9, [CanBeNull])]
FJob: String;
[Column('MGR', [CanBeNull])]
FMgr: IntegerNullable;
[Column('HIREDATE', [CanBeNull])]
FHiredate: TDateTimeNullable;
[Column('SAL', [CanBeNull])]
FSal: DoubleNullable;
[Column('COMM', [CanBeNull])]
FComm: DoubleNullable;
[Column('DEPTNO')]
FDeptno: Integer;
[Column]
[Reference('TDept', 'FEmps', 'FDeptno', 'FDeptno', srCascade, drCascade)]
FDept: TReferenceData;
function GetEmpno: Integer; virtual;
procedure SetEmpno(const Value: Integer); virtual;
function GetEname: String; virtual;
procedure SetEname(const Value: String); virtual;
function GetJob: String; virtual;
procedure SetJob(const Value: String); virtual;
function GetMgr: IntegerNullable; virtual;
procedure SetMgr(const Value: IntegerNullable); virtual;
function GetHiredate: TDateTimeNullable; virtual;
procedure SetHiredate(const Value: TDateTimeNullable); virtual;
function GetSal: DoubleNullable; virtual;
procedure SetSal(const Value: DoubleNullable); virtual;
function GetComm: DoubleNullable; virtual;
procedure SetComm(const Value: DoubleNullable); virtual;
function GetDeptno: Integer; virtual;
procedure SetDeptno(const Value: Integer); virtual;
function GetDept: TDept;
procedure SetDept(const Value: TDept);
protected
procedure Register; override;
constructor Create(AMetaType: TMetaType); overload; override;
public
constructor Create; overload; override;
property Empno: Integer read GetEmpno write SetEmpno;
property Ename: String read GetEname write SetEname;
property Job: String read GetJob write SetJob;
property Mgr: IntegerNullable read GetMgr write SetMgr;
property Hiredate: TDateTimeNullable read GetHiredate write SetHiredate;
property Sal: DoubleNullable read GetSal write SetSal;
property Comm: DoubleNullable read GetComm write SetComm;
property Deptno: Integer read GetDeptno write SetDeptno;
property Dept: TDept read GetDept write SetDept;
end;
Všimněte si toho GetDept - TDept. To je odkaz na druhou třídu (tabulka). Tím, že jsem zvolil atributy, jsou jednotlivé pole tagovány RTTI atributy. Přijde mi to takové správné moderní řešení :-).
Framework má podporu pro zobrazení v db komponentách přes TEntityDataset, TEntityTable, takže výsledky operací se dají normálně zobrazovat (nevím zda i ve free edici).
Linq
Ale mnohem zajímavější je LINQ:
var
E: IEmpExpression;
D: IDeptExpression;
Query: ILinqQueryable;
begin
E := Context.Emp;
D := Context.Dept;
Query := Linq.From(E)
.LeftJoin(D).On(D.DeptNo = E.DeptNo)
.Select([E.EName, D.DName]);
Result := Context.GetEntities(Query);
Context je instance modelu. Emp a Dept jsou ty tabulky v modelu (viz. třída výše), takže pak je Linq jednoduše pochopitelné. Celkem maso je ale to On(D.DeptNo = E.DeptNo), to není string! (ale může být).
Vygeneruje
SELECT t1.ENAME AS Ename, t2.DNAME AS Dname
FROM EMP AS t1
LEFT JOIN DEPT AS t2 ON t2.DEPTNO = t1.DEPTNO
IEmpExpression, IDeptExpression je vygenerováno programem
IDeptExpression = interface(IMetaType)
['{4D873A5E-7D30-48FF-9B02-7697397C6A8E}']
function Deptno: TExpression;
function Dname: TExpression;
function Loc: TExpression;
function Emps: IDeptEmpsExpression;
function Unique: IDeptExpression;
end;
IEmpExpression = interface(IMetaType)
['{D066A14A-4CE1-4EE0-8776-3977678D9810}']
function Empno: TExpression;
function Ename: TExpression;
function Job: TExpression;
function Mgr: TExpression;
function Hiredate: TExpression;
function Sal: TExpression;
function Comm: TExpression;
function Deptno: TExpression;
function Dept: IDeptExpression;
function Unique: IEmpExpression;
end;
Další příklad:
Query := Linq.From(E)
.OrderBy(E.EName)
.Select([E.EName, E.Job, E.Dept.DName]);
Všimněte si toho E.Dept.DName:
SELECT t1.ENAME AS Ename, t1.JOB AS Job, t2.DNAME AS Dname
FROM EMP AS t1
LEFT OUTER JOIN DEPT AS t2 ON t1.DEPTNO = t2.DEPTNO
ORDER BY t1.ENAME
Samozřejmě podpora generik (změna názvu u posledního zaměstance):
var
Emp: TEmp;
begin
Emp := DemoContext.GetEntities<TEmp>.Last;
// Modify entity attributes
Emp.Ename := 'Name ' + IntToStr(Round(Random(99)));
// Save changes
Emp.Save;
end;
Upřímně: je to mazec a je to dobře navržené, ale ještě v tom pořád plavu. Stáhněte si kompilované demo, je to za tisíce slov.