DA01 - Data Abstract
总览
数据接口的艰难选择
数据接口框架
(DAF)
是一组在常见的数据库中查询数据源
,
执行命令的组件
(
通常由供应商提供
).
常见的
Delphi
数据接口框架包含
Borland
的
dbExpress,
微软的
ADO, Jason Wharton
的
IBObjects, CoreLabs SDAC/ODAC
等等
.
数据接口框架通常包括两个类型
:
- 多驱动: 而可以使用特定的驱动操作不同数据库(如ADO, BDE, dbExpress).
- 专用的:为目标数据库提供高效操作,支持数据库所有特性 (i如 IBObjects, IBExpress, CoreLabs SDAC/ODAC).
如果需要使用多种数据库
(
如
Oracle
和微软的
SQL Server),
应该选择多驱动的
DAF.
当只用一种数据库时
,
很明显最好选择专用的
DAF.
选择是否很简单明了
?
当然不是
.
使用常用数据接口架构的问题
当多驱动
DAF
实现操作多数据库的同时
,
他们仍然要面对处理多种
SQL
方言的负担
.
当在
Oracle7
和
MS 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.
在那之前
,
我们只能使用组件或包含于
MIDAS
的
IProviderSupport.
为了抽象
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
方式
.
假设我们想依靠方法传递过来的参数改变
Select
的
Where
子句
.
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 Abstract
的
ConnectionManager
Data Abstract
假设同时只有一个数据模块用于连接到多个数据库
.
业务逻辑只能写一次并要尽量独立于具体的数据库
(
例如
.
表和字段名
).
第一件事情是要抽取数据库连接的定义
.
TDAConnectionManager
正是做这个的
:
保存
Data Abstract
连接字符串指向不同数据库
.
每个连接字符串都有一个标识名称和可选的描述
:
记住你不需要在
Data Abstract
中实现多数据库运行的目标
.
但是如果你需要时可以更有弹性的修改
.
Data Abstract
的连接字符串使用基于架构和数据库的标准格式
.
看如下范例
:
使用
ADOExpress
连接
Microsoft SQL Server
的
Northwind
数据库
:
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 Abstract
的
Schema
Data Module
很容易由于存在过多的控件而变得混乱
,
尤其是
TDatasets.
当你需要支持多种数据库时
,
你可能会采用如下方式
:
- 动态创建TDataSet并通过代码调整不同数据库的SQL方言.
- 为每个要连接的数据库复制一份DataModule.
- 使用像ADO一样的DAF和一些TDatasets, 但是仍然要在.pas文件中写SQL代码.
这些情形都不是最好的方案
.
围绕着这个问题
, Data Abstract
引入了
Schemas
概念
.
Schema
是一组指向你系统中特定领域的逻辑业务
DataSet
和业务命令
(
可能连接到一个业务对象
)
.
在下图展示的
Data Abstract Schema Modeler
中你可以可视化的建立业务
Schema.
左上部的
Datasets
列表是一系列返回表格数据集的查询
.
左下部的
Commands
列表
,
是一系列无数据集返回的
INSERTs, DELETEs, UPDATEs
或调用存储过程的操作
.
每个数据集和命令都可能在你的每个目标数据库中执行特定的
SQL
命令
.
注意
,
下面的截图中
SQL
属性与上面的不同
:
业务数据集不必从相关的数据库中精确复制
;
它们可以独立定义
.
在本例中
Customer
有
7
个列
,
而在对应数据库中的表要多得多
.
基于逻辑数据集的
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
组件中获取连接
.