DA01 - Data Abstract 总览

来源:互联网 发布:自动排班软件 编辑:程序博客网 时间:2024/05/22 00:09
 
DA01 - Data Abstract 总览       
数据接口的艰难选择
数据接口框架(DAF)是一组在常见的数据库中查询数据源,执行命令的组件(通常由供应商提供).常见的Delphi数据接口框架包含BorlanddbExpress, 微软的ADO, Jason WhartonIBObjects, CoreLabs SDAC/ODAC等等.
数据接口框架通常包括两个类型:
  1. 多驱动: 而可以使用特定的驱动操作不同数据库(ADO, BDE, dbExpress).
  2. 专用的:为目标数据库提供高效操作,支持数据库所有特性 (i IBObjects, IBExpress, CoreLabs SDAC/ODAC).
如果需要使用多种数据库 (Oracle和微软的SQL Server),应该选择多驱动的DAF.当只用一种数据库时,很明显最好选择专用的DAF.选择是否很简单明了?当然不是.
使用常用数据接口架构的问题
当多驱动DAF实现操作多数据库的同时,他们仍然要面对处理多种SQL方言的负担. 当在Oracle7MS SQL Server中查询Orders表并Join Customers表时,你应该可以意识到这对于你的项目组生产力意味着什么. 另外,多驱动DAF操作数据库时要比专用驱动效率底下; 这意味着很多驱动都能运行良好,但有些却缺乏灵活和可靠性. 依赖于你选择的通用DAF,可能会发现特殊的驱动不存在,甚至你的系统已经开始开发了.
专用数据接口架构的问题
专用DAF之间差距很大.当你安装了IBObjects,Delphi组件面板上将有不少于7个新标签,IBExpress则只有两个. 这意味着如果你真的使用了特殊架构的所有特性将会绑定的非常牢固,相互之间切换不重做几乎无法实现. 明显,你可能不需要转换而且当前的DAF可能也很完美,但是如果可以转换不是更好吗?
ODAC DAO是另外一个好的范例:假设你开始时使用DAO,后来客户要求你解决存取安装在客户端的OCI.DAO转换到ODAC需要多少时间?
这还没有考虑如果三方供应商停止提供产品支持时的情况.
业务讨论
如果一个系统能运行在多数据库是一个很明显的优势.
系统可能一开始很小使用Firebird,假设从来没有遇到速度问题,或需要运行在Oracle.突然,产品开始受到重视一个大公司要求它支持Oracle.在你及竞争者之间选择.你会放弃这个客户吗?如果你的最近的和最大的客户要求数据库可以容纳300G数据而你原来的数据库没有这个能力怎么办?
可运行于多数据库可以增加你的商用潜能. 直到如今问题是这种系统相对于其功能代价高昂.
 
Data Abstract 解决方案
Data Abstract设计用来解决上面列出的所有问题以及其他一些通常涉及多层应用设计的重点.
Data Abstract封装多驱动和专用DAF, 提供它们所有的优点而没有引入它们的缺点.
Data Abstract 驱动
Data Abstract 驱动通过封装专用驱动实现.这意味着我们不用在去发明车轮,而是使用现有的最好的方式.
如果你决定使用Interbase, 你可以使用安装在IBObjects, IBExpress dbExpress中的驱动.
如果你需要存取Microsoft SQL Server, 你需要选择ADOExpress, CoreLab's SDAC dbExpress.
如果你不喜欢使用它们,你可以自己写一个驱动!
产品对发展中的驱动提供了简单的接口.要求你去实现4个基类.一个驱动的包含在单元中的源代码大约有300.
例如,基于ADOExpress驱动单元,有一个方法必须要写:
procedure TDAEADOConnection.DoCommitTransaction;
begin
 ADOConnection.CommitTrans();
end;
你可以为所有的列表数据创建存取驱动,如逗号间隔的文件或硬盘数据.事实上, Data Abstract 同时也提供了一个简单的磁盘驱动允许查询计算机磁盘上的文件名.
Data Abstract驱动可以在运行时动态的从DLL文件中加载,或静态编译到你的模块. 一个特别的组件TDADriverManager,提供所有驱动管理方面的全面控制.
这是一个包含在Data Abstract 中的驱动管理的简单应用程序.静态连接到ADOExpress IBExpress 驱动并在运行时动态加载IBObjects, DiskDrive SDAC 驱动:
下面的对话框更详细的说明内部情况:
Data Abstract创建广泛的用户接口
接口非常强大同时又非常少的依赖Delphi特性. 因为Delphi的数据存取库定义在最初版本,那时语言还没有接口的定义. 重新设计VCL支持它们又会是很多程序无法运行.
Delphi.Net 也支持.NET框架中的基于接口的ADO.Net. 在那之前,我们只能使用组件或包含于MIDASIProviderSupport.
为了抽象DAF并简化应用程序代码,我们决定设计Data Abstract.连接,数据集,存储过程仍然存在, 但是不再像以前一样拖放到窗体;你需要在特殊的控件ConnectionManager Schema中声明它们.
让我们比较一下代码.本例我们打开一个数据集并将一个字段的值显示在TListBox.
标准的代码方式
var myds : TDataset;
    i : integer;
begin
 myds := TDataset.Create(NIL);
 try
    myds.Connection := MyConnection; // MyConnection was open elsewhere
    myds.SQL.Text := 'SELECT Name FROM Customers';
    myds.Open;
    while not myds.EOF do begin
      ListBox.Items.Add(myds.Fields[0].AsString);
      myds.Next;
    end;
 finally
    myds.Free;
 end;
end;
标准的RAD方式
var i : integer;
begin
 try
    myds.Open;
    while not myds.EOF do begin
      ListBox.Items.Add(myds.Fields[0].AsString);
      myds.Next;
    end;
 finally
    myds.Close;
 end;
end;
Data Abstract 方式
var myds : IDADataset;
    i : integer;
begin
 // The last boolean parameter instructs to opens it
 myds := DASchema.NewDataset('CustomerList', MyAbstractConnection, TRUE);
 while not myds.EOF do begin
    ListBox.Items.Add(myds.Fields[0].Value);
    myds.Next;
 end;
end;
可见,第一个范例代码多于另外两个,但是RAD方式和Data Abstract 方式代码量相同. 事实上,如果你注意最后一个片段, 你将看到操作数据集的关闭打开代码都省略掉了,这将减少错误倾向.
你封装过TDataSet使其更容易使用? 当然必须你自己做:避免购买三方组件.
现在我们做一个复杂一点的范例,让我们比较一下RAD 方式和Data Abstract 方式.假设我们想依靠方法传递过来的参数改变SelectWhere子句.
RAD 方式
procedure SelectCustomers(const aCity, anAddress : string);
var whre : string;
begin
 try
    whre := '';
    myds.SQL.Text := 'SELECT Name FROM Customers'
 
    if (aCity<>'') then whre := '(City='+aCity+')';
 
    if (anAddress<>'') then begin
      if (whre<>'') then whre := whre+' AND ';
      whre := '(Address='+anAddress+')';
    end;
 
    myds.Open;
    [..]
 finally
    myds.Close;
 end;
end;
Data Abstract 方式
procedure SelectCustomers(const aCity, anAddress : string);
var myds : IDADataset;
begin
 try
    myds := DASchema.NewDataset('Customers', MyAbstractConnection, FALSE);
    // If a City is an empty string, the condition is not added
    myds.Where.AndIfNotEmpty(aCity, 'City');
    myds.Where.AndIfNotEmpty(anAddress, 'Address'); // Same...
    myds.Open;
    [..]
 finally
    myds.Close;
 end;
end;
明显,也支持参数.
现在我们看看最后的范例.
最后的方法是如果你同时支持三种数据库(Oracle, SQL Server Interbase,)应该怎么修改?
无论是你的数据库要做JOIN或其他操作都不只是需要修改一行代码. 为了理解如何实现我们讨论两个Data Abstract的新控件ConnectionManager Schema.
Data AbstractConnectionManager
Data Abstract假设同时只有一个数据模块用于连接到多个数据库.业务逻辑只能写一次并要尽量独立于具体的数据库(例如.表和字段名).
第一件事情是要抽取数据库连接的定义.
TDAConnectionManager正是做这个的:保存Data Abstract 连接字符串指向不同数据库.每个连接字符串都有一个标识名称和可选的描述:
记住你不需要在Data Abstract中实现多数据库运行的目标.但是如果你需要时可以更有弹性的修改.
Data Abstract的连接字符串使用基于架构和数据库的标准格式. 看如下范例:
使用ADOExpress 连接Microsoft SQL ServerNorthwind 数据库:
ADO?AuxDriver=SQLOLEDB.1;Server=localhost;Database=Northwind;UserID=sa
使用IBExpress 连接Interbase Employee数据库:
IBX?Server=localhost;UserID=sysdba;Password=masterkey;Database=C:/Program Files/Borland/InterBase/examples/Database/Employee.gdb
使用IBObjects 连接Interbase Employee数据库:
IBO?Server=localhost;UserID=sysdba;Password=masterkey;Database=C:/Program Files/Borland/InterBase/examples/Database/Employee.gdb
 
定义好连接后就可以如下方式使用了:
begin
 connection := DAConnectionManager.NewConnection('EmployeeIBO', TRUE);
end;
可见为了抽象并隔离它们,连接使用其名称获取.
连接属性
方法TDAConnectionManager.NewConnection 的声明:
function NewConnection(const aConnectionName :string;
 OpenIt : boolean = FALSE) : IDAConnection;
可见,返回类型是一个接口,自动实现引用计数而不用去手动释放.同时IDAConnection接口已经为所有的数据库开放了一个最小操作集合.
接口IDAConnection的定义:
IDAConnection = interface
['{6D9C806F-65A5-43B3-8F07-4ED782A13A0A}']
 // Properties readers/writers
 function GetConnectionString : string;
 procedure SetConnectionString(Value : string);
 function GetConnected : boolean;
 procedure SetConnected(Value : boolean);
 function GetName : string;
 
 // Transaction support
 function BeginTransaction : integer;
 procedure CommitTransaction;
 procedure RollbackTransaction;
 
 // Connection
 procedure Open;
 procedure Close;
 
 // Metadata
 procedure GetTableNames(out List : IROStrings);
 procedure GetStoredProcedureNames(out List : IROStrings);
 procedure GetTableFields(const aTableName : string; out Fields :
 TDAFieldCollection);
 procedure GetStoredProcedureParams(const aStoredProcedureName :
 string; out Params : TDAParamCollection);
 
 // Commands
 function NewStoredProcedure(const StoredProcedureName : string) :
 IDAStoredProcedure;
 function NewDataset(const SQL : string) : IDADataset;
 
 // Properties
 property ConnectionString : string read GetConnectionString write
 SetConnectionString;
 property Connected : boolean read GetConnected write SetConnected;
 property Name : string read GetName;
end;
现在问题是:难道框架只支持部分功能集而没有提供对IBObjects IBExpress的弹性?可以想象,答案是否定的.
Data Abstract为你连接到的特定数据库定义了附加接口. , Interbase 连接支持如下附加接口:
IIBConnectionProperties = interface
['{5F001B6F-4FB6-46B7-BC27-3326C4658F75}']
 function GetRole : string;
 procedure SetRole(const Value : string);
 function GetSQLDialect : integer;
 procedure SetSQLDialect(Value : integer);
 
 procedure Commit;
 procedure CommitRetaining;
 procedure Rollback;
 procedure RollbackRetaining;
 
 property Role : string read GetRole write SetRole;
 property SQLDialect : integer read GetSQLDialect write SetSQLDialect;
end;
所以,为了设置连接的SQLRole SQLDialect需要如下代码:
var ibprops : IIBConnectionProperties;
begin
 connection := DAConnectionManager.NewConnection('EmployeeIBO', TRUE);
 if Supports(connection, IIBCOnnection, ibprops) then begin
    ibprops.Role := 'ADMIN';
    ibprops.SQLDialect := 3;
 end;
end;
附加接口是不断发展的,你可以轻松的为一个已存在的驱动增加附加接口. 你可以完全控制你的驱动支持什么.
其他被所有驱动开放的接口是IDAConnectionObjectAccess:
IDAConnectionObjectAccess = interface
['{FF8F2319-4EAE-4A2B-8713-A6E6B3F5E48A}']
 // Properties readers/writers
 function GetConnectionObject : TObject;
 function GetConnectionProperties(const aPropertyName : string) :
 Variant;
 procedure SetConnectionProperties(const aPropertyName : string;
 const aValue : Variant);
 
 // Properties
 property ConnectionObject : TObject read GetConnectionObject;
 property ConnectionProperties[const aPropertyName : string] :Variant
           read GetConnectionProperties write SetConnectionProperties;
end;
IDAConnectionObjectAccess 提供了建立连接的VCL控件的存取接口( TIBDatabase, TADOCOnnection ).
Data AbstractSchema
Data Module很容易由于存在过多的控件而变得混乱,尤其是TDatasets.
当你需要支持多种数据库时,你可能会采用如下方式:
  1. 动态创建TDataSet并通过代码调整不同数据库的SQL方言.
  2. 为每个要连接的数据库复制一份DataModule.
  3. 使用像ADO一样的DAF和一些TDatasets, 但是仍然要在.pas文件中写SQL代码.
这些情形都不是最好的方案.
围绕着这个问题, Data Abstract 引入了Schemas概念.
Schema是一组指向你系统中特定领域的逻辑业务DataSet和业务命令(可能连接到一个业务对象).
在下图展示的Data Abstract Schema Modeler 中你可以可视化的建立业务Schema.
左上部的Datasets列表是一系列返回表格数据集的查询. 左下部的Commands列表, 是一系列无数据集返回的INSERTs, DELETEs, UPDATEs 或调用存储过程的操作.
每个数据集和命令都可能在你的每个目标数据库中执行特定的SQL命令. 注意,下面的截图中SQL属性与上面的不同:
业务数据集不必从相关的数据库中精确复制;它们可以独立定义. 在本例中Customer7个列,而在对应数据库中的表要多得多. 基于逻辑数据集的JOIN也是合法的.
如果你注意两个截图的列名,你可能会猜到: 我们不只使用不同的数据库引擎(通过ADOExpress 使用MSSQL 通过IBExpress使用Interbase)同时我们也使用两个完全不同的数据库结构 (Northwind Employees.gdb)!!
看到Schema的优点我们现在马上返回到代码中.
让我们再次注意SelectCustomers 方法:
procedure SelectCustomers(const aCity, anAddress : string);
var myds : IDADataset;
begin
 try
    myds := DASchema.NewDataset('Customers', MyAbstractConnection, FALSE);
 
    myds.Where.AndIfNotEmpty(aCity, 'City');
    // If a City is an empty string, the condition is not added
    myds.Where.AndIfNotEmpty(anAddress, 'Address'); // Same...
 
    myds.Open;
    [..]
 finally
    myds.Close;
 end;
end;
第一行代码自动抽取出匹配MyAbstractConnection的正确SQL表达式.因此,如果MyAbstactConnection初始化为"IBEmployees" 连接, myds.SQL 将如下:
SELECT CUST_NO, CUSTOMER, PHONE_NO [..] FROM CUSTOMER
如果连接到"MSSQL" 则如下:
SELECT CustomerID, CompanyName, Phone [..] FROM Customers
下面两行代码在Select语句中加入WHERE子句(如果aCity anAddress不是空字符串).
myds.Where.AndIfNotEmpty(aCity, 'City');
// If a City is an empty string, the condition is not added
myds.Where.AndIfNotEmpty(anAddress, 'Address'); // Same...
'City' 'Address'将通过ColumnMappings映射为适当的列名组合在SQL命令中.
如上面所提到的,不管你使用什么数据库或DAF都使用同样的代码工作!
连接池
到现在为止,上面的范例已经通过ConnectionManager组件获取连接. ConnectionManager 只能创建活动连接不能提供池支持.通常情况下(没有用-ADO)将需要你自己去实现连接缓冲并在请求线程之间共享(例如ISAPI RemObjects服务模块).
Data Abstract引入叫做TDAConnectionPool特殊组件实现这个功能.
为了受益于连接缓冲池你需要设置它的属性并替换代码
connection := DAConnectionManager.NewConnection('EmployeeIBO', TRUE);
connection := DAConnectionPool.NewConnection('EmployeeIBO', TRUE);
主要是就是取代ConnectionManager而从ConnectionPool组件中获取连接.