我们的ORM RESTful框架能够通过一组通用单元和类访问许多数据库引擎。可以访问SQL和NoSQL引擎,这在ORM环境中是一个非常独特的特性。
请记住介绍mORMot时的这张数据库层的图:
框架将SQLite3作为其在服务器上的SQL核心,但是特有的机制允许访问任何远程数据库,并将这些表内容与框架的本地ORM表混合。由于SQLite3独特神奇的虚拟表机制,这些外部表可以在SQL语句中视为本地SQLite3表访问,甚至对于NoSQL引擎也是如此。
模式 | 引擎 |
---|---|
SQL | SQLite3, Oracle, NexusDB, MS SQL, Jet/MSAccess, FireBird, MySQL, PostgreSQL, IBM DB2, IBM Informix |
NoSQL | MongoDB, JSON或二进制持久性TObjectList 参见访问外部NoSQL数据库和内存静态过程 |
您可以混合使用数据库,如同样的mORMot ORM支持同时对分布在多个数据库的数据持久化,一些TSQLRecord
作为快速内部SQLite3表或TObjectList
,其它的在PostgreSQL数据库(与外部报告/ SAP引擎绑定),并整合MongoDB实例数据。
可以通过SynDB.pas
单元访问外部关系数据库管理系统(RDBMS)。之后,框架ORM能够通过mORMotDB.pas
单元访问它们。您也可以直接使用SynDB.pas
单元,而与我们的ORM没有任何链接。
当前处理数据访问的库列表如下:
驱动 | SynDB单元 | RDBMS引擎 |
---|---|---|
SQLite3 | SynDBSQLite3.pas |
直接SQLite3访问(dll或链接到exe) |
Oracle | SynDBOracle.pas |
直接Oracle访问(通过OCI) |
OleDB | SynOleDB.pas |
MS SQL, Jet/MSAccess或其他 |
ODBC | SynDBODBC.pas |
MS SQL、FireBird、MySQL、PostgreSQL、IBM DB2、Informix或其他 |
ZeosLib | SynDBZeos.pas |
MS SQL, SQLite3, FireBird, MySQL, PostgreSQL |
DB.pas /TDataset |
SynDBDataset.pas |
NexusDB and databases supported by DBExpress, FireDAC, AnyDac, UniDAC, BDE... |
HTTP | SynDBRemote.pas |
通过HTTP远程访问任何SynDB数据库 |
这个列表还没完,可能在不久的将来继续完善。欢迎任何形式的帮助,因为按照现有模式实现一个新单元并不困难,您可以从现有的驱动程序(例如Zeos或Alcinoe库)开始,开源贡献总是受欢迎的!
感谢我们的SynDB.pas
类设计,实现SQLite3直接访问非常简单方便。它甚至被用于我们的回归测试,以实现独立的统一测试。
添加了针对Oracle的专用的直接访问,因为Oracle所有可用的OleDB驱动程序(微软的和Oracle的)在处理BLOB方面都有问题,我们希望我们的客户端能轻量快速访问这个伟大的数据库。
事实上,OleDB是一个很好的数据库访问选择,它具有良好的性能、本地Unicode以及许多可用的驱动程序。由于OleDB,我们能够访问几乎任何现有的数据库。与添加其他第三方Delphi库相比,服务端可执行文件中的代码开销也要小得多。我们将使用Microsoft或其它OleDB对每个驱动程序执行所有的测试和调试。
自版本1.17以来,对ODBC层的直接访问已经包含在框架数据库单元中。它有更广泛的免费驱动(包括MySQL或FireBird),是OleDB的正式替代产品(下一个版本的MS SQL Server将只提供ODBC驱动,就像微软警告其客户的那样)。
自1.18版以来,可以使用任何ZeosLib/ZDBC驱动快速直接访问RDBMS底层客户端库。因为ZDBC库不依赖于DB.pas
,绕过了低效的TDataSet
组件,其性能非常高。ZDBC维护者做了很多优化,特别是与mORMot一起工作,这个库是与我们的框架一起工作的一流库。
同样自1.18版本,DB.pas
可以与我们的SynDB.pas
一起使用。当然,使用TDataset
作为中间层会比SynDB.pas
直接访问模式慢。但是它允许您复用任何现有(第三方)数据库连接驱动程序,这对存量应用程序的演化是有意义的,或者用于尚未支持的数据库引擎。
最后但最重要的是,SynDBRemote.pas
单元允许您创建在远程SynDB
HTTP服务器上执行SQL操作的数据库应用,而不是在数据库服务器上。您可以像其他SynDB数据库一样创建连接,但是传输将通过HTTP进行,不需要在应用程序中安装数据库客户端,参见通过HTTP远程访问。
因此,下列连接是可行的:
这个图在最新的层次上有点难理解,但是我想您已经得到了通用的分层设计,稍后它将被分割成更小的聚焦图。
SynDB.pas单元有如下特点:
DB.pas
/ TDataset
的组件的轻量包装器(如NexusDB、DBExpress、FireDAC、AnyDAC、UniDAC、BDE…)DB.pas
标准单位元及其所有依赖单元);TDynArrayHashed
);ISQLDBRows
接口,避免键入try...finally Query.Free end;
并允许单条SQL语句;TDataSet
结果集:一种是基于TClientDataSet
的读写,另一种是更快的只读TSynSQLStatementDataSet
。TQuery
的包装器;TDataSet
组件);SynDB
引擎进行快速和安全的远程访问,而不需要将RDBMS客户端库与应用程序一起部署;SynDB
客户端通过HTTP远程访问,它也是独立使用这些库的一个很好的示例程序。 我们的ORM不需要完整的特性集(不要指望将这个数据库类用于您的VCL DB RAD组件),仅处理我们需要的基本的SQL列类型(派生自SQLite的内部列类型): NULL, Int64, Double, Currency, DateTime, RawUTF8
和BLOB
。
它们在SyncDB .pas
中定义如下:
TSQLDBFieldType =
(ftUnknown, ftNull, ftInt64, ftDouble, ftCurrency, ftDate, ftUTF8, ftBlob);
TSQLDBFieldType | 内容 |
---|---|
ftNull |
对应SQL的NULL值 |
ftInt64 |
64位整数 |
ftDouble |
64位浮点数 |
ftCurrency |
财务类型,最多4位小数(货币) |
ftDate |
日期和时间,对应Delphi的TDateTime 类型 |
ftUTF8 |
Unicode文本,UTF-8编码,有或没有大小限制 |
ftBlob |
二进制内容,存储为RawByteString |
这些类型将映射为底层数据库级访问类型,而不是像在mORMot.pas
中定义的TSQLFieldType
那样的高级Delphi类型,也不是标准VCLDB.pas
单元中定义的泛型大型TFieldType
。事实上,它更依赖于标准的SQLite3泛型类型,即NULL、INTEGER、REAL、TEXT、BLOB(添加了ftCurrency
和ftDate
类型,以更好地支持大多数DB引擎),请参见http://www.sqlite.org/datatype3.html。
您可能注意到这里唯一处理的字符串类型使用UTF-8编码(使用我们的RawUTF8类型实现),用于跨delphi版本实现真正Unicode处理。代码可以通过variant
、string
或widestring
变量和参数访问文本数据,但是我们的单元将在内部使用UTF-8编码,请参阅Unicode和UTF-8,因此,它将与使用同样编码的ORM直接接口。当然,如果某列在数据库中没有定义为Unicode文本,则需要在数据驱动级实现与对应字符集的相互转换;但是在用户相关代码中,您应该始终使用Unicode内容。
BLOB列或参数作为RawByteString
变量访问,这些变量通过TRawByteStringStream
映射为标准TStream
。
除了原始数据访问,SynDB.pas
单元还处理一些SQL级语句生成,并用于我们的ORM内核使用。
以下RDBMS数据库引擎在SynDB.pas
中的定义:
TSQLDBDefinition = (dUnknown, dDefault, dOracle, dMSSQL, dJet,
dMySQL, dSQLite, dFirebird, dNexusDB, dPostgreSQL, dDB2, dInformix);
TSQLDBDefinition | 已测试的RDBMS |
---|---|
dDefault |
遵循SQL-92标准的任何数据库 |
dOracle |
Oracle 11g |
dMSSQL |
MS SQL Server 2008/2012 |
dJet |
Jet/MSAccess (仅支持Win32) |
dMySQL |
MySQL 5.6 |
dSQLite |
SQLite3 3.7.11及以上版本(我们为静态链接提供了最新版本) |
dFirebird |
Firebird 2.5.1 |
dNexusDB |
NexusDB 3.11 |
dPostgreSQL |
PostgreSQL 9.2/9.3 |
dDB2 |
IBM DB2 10.5 |
dInformix |
IBM Informix 11.70 |
以上版本已经经过测试,但是更新或更老的版本也可以工作。欢迎您的反馈,我们无法完成对数据库和客户端所有可能组合的测试!
SynDB.pas
单元能够生成上述引擎的SQL语句,CREATE TABLE
/ CREATE INDEX
命令,检索元数据(如表和字段信息),正确计算SELECT
的limit/offset语法、计算多条INSERT语句,检查SQL关键字,定义特定的schema/owner命名约定,处理日期和时间值,处理错误和异常,甚至创建一个数据库。
以下是实现外部数据库无关特性的单元:
文件 | 描述 |
---|---|
SynDB.pas |
抽象的数据库直接访问类 |
SynOleDB.pas |
OleDB直接访问类 |
SynDBODBC.pas |
ODBC直接访问类 |
SynDBZeos.pas |
ZDBC直接访问类 |
SynDBOracle.pas |
Oracle DB直接访问类(通过OCI) |
SynDBSQLite3.pas |
SQLite3直接访问类 |
SynDBDataset.pas |
TDataset (DB.pas )访问类 |
SynDBFireDAC.pas |
TDataset (DB.pas )访问类 |
SynDBUniDAC.pas |
TDataset (DB.pas )访问类 |
SynDBNexusDB.pas |
TDataset (DB.pas )访问类 |
SynDBBDE.pas |
TDataset (DB.pas )访问类 |
SynDBRemote.pas |
HTTP远程访问 |
SynDBVCL |
只读的TSynSQLStatementDataSet 结果集 |
SynDBMidasVCL |
可读写的TClientDataSet 结果集 |
值得注意的是,这些单元只依赖于SynCommons.pas
,它们独立于框架的ORM部分(甚至远程访问)。它们可以单独使用,使用普通SQL代码访问所有这些外部数据库。它们的所有类都继承自SynDB.pas
中定义的抽象类,所以从一个数据库引擎切换到另一个数据库引擎(即便是远程HTTP访问)只是更改一个类型的问题。
数据通过三大类访问:
在实践中,您定义一个TSQLDBConnectionProperties
实例,然后使用专用的NewConnection
/ ThreadSafeConnection
/ NewStatement
方法生成TSQLDBConnection
and TSQLDBStatement
实例。
以下是所有可用远程连接属性的通用类层次结构:
这些类是SynDB.pas
的根类,大部分数据库处理将通过该单元实现。mORMot框架的ORM只需要给定的TSQLDBConnectionProperties
实例就可以访问任何外部数据库。
然后定义了以下连接类:
每个连接可以创建一个对应的语句实例:
在上面的层次结构中,TSQLDBDatasetStatementAbstract
用于自定义类参数处理,例如FireDAC的TADParams
(它以数组DML为特征)。
还定义了一些专用的异常类:
查看TestOleDB.dpr
示例程序,位于SQlite3文件夹中,使用我们的SynOleDB
单元连接到本地的MS SQL Server 2008 R2 Express edition,该版本将使用Person.Address
表的JSON表示写入AdventureWorks2008R2示例数据库文件。
最简单的是保持在TSQLDBConnectionProperties级别,使用这个实例的Execute()
方法,并通过ISQLDBRows
接口访问所有返回的数据。它将以抽象的方式自动建立到数据库的线程安全的连接。
SynDB.pas
的典型用法包括:
TSQLDBConnectionProperties
实例;Execute*()
方法执行语句。定义数据库连接非常简单:
var Props: TSQLDBConnectionProperties;
...
Props := TOleDBMSSQLConnectionProperties.Create('.\\SQLEXPRESS','AdventureWorks2008R2','','');
try
UseProps(Props);
finally
Props.Free;
end;
根据TSQLDBConnectionProperties
子类的不同,输入参数也有所不同。请参阅文档中每个类的Create() constructor
,以便按照要求设置所有参数。
然后,任何子代码都可以执行任何SQL请求,具有可选的绑定参数,如下所示:
procedure UseProps(Props: TSQLDBConnectionProperties);
var I: ISQLDBRows;
begin
I := Props.Execute('select * from Sales.Customer where AccountNumber like ?',['AW000001%']);
while I.Step do
assert(Copy(I['AccountNumber'],1,8)='AW000001');
end;
在这个过程中,没有定义TSQLDBStatement
,也不需要添加try ... finally Query.Free; end;
语句块。
事实上,MyConnProps.Execute
方法将以ISQLDBRows
的形式返回一个TSQLDBStatement
实例,可以使用这些方法遍历结果行,并检索各个列值。在上面的代码中,I['FirstName']
实际上会调用I.Column[]
默认属性,该属性将以变体形式返回列值。您还有其他专用的方法,如ColumnUTF8
或ColumnInt
,可以直接检索需要的数据。
注意,当使用我们的TSynLog
类记录日志时,所有绑定参数都将出现在SQL语句中。
您可能已经注意到,在前面的代码示例中,我们使用了UseProps()
子过程。这是有目的的。
我们可以这样写一个小的测试:
var Props: TSQLDBConnectionProperties;
I: ISQLDBRows;
...
Props := TOleDBMSSQLConnectionProperties.Create('.\\SQLEXPRESS','AdventureWorks2008R2','','');
try
I := Props.Execute('select * from Sales.Customer where AccountNumber like ?',['AW000001%']);
while I.Step do
assert(Copy(I['AccountNumber'],1,8)='AW000001');
finally
Props.Free;
end;
end;
事实上,您不应该使用这种模式。这段代码将在运行时导致意外的访问冲突。
在后台,编译器会生成一些隐含的代码来释放I: ISQLDBRows
局部变量,如下所示:
...
finally
Props.Free;
end;
I := nil; // this is generated by the compiler, just before the final "end;"
end;
因此,在Props
实例之后释放ISQLDBRows
,将导致访问冲突。
正确的写法是使用一个子函数(函数离开时释放局部ISQLDBRows
),或者显式地释放接口变量:
while I.Step do
assert(Copy(I['AccountNumber'],1,8)='AW000001');
finally
I := nil; // release local variable
Props.Free;
end;
当然,大多数情况下,您将全局初始化进程的TSQLDBConnectionProperties
,然后在进程结束时释放它。每个请求都将在它自己的子方法中处理,因此将在TSQLDBConnectionProperties
主实例之前释放。
最后但最重要的是,您不应该在每次需要访问数据库时才创建TSQLDBConnectionProperties
实例,这样您可能会丢失很多SynDB
特性,比如每个线程连接池或语句缓存。
我们通过自定义变体时间实现了列值的后期绑定访问。它使用了Ole自动化的内部机制,访问列内容,就像访问本地对象属性所在的列名一样。
查看Delphi代码就很清楚和明显:
procedure UseProps(Props: TSQLDBConnectionProperties);
var Row: Variant;
begin
with Props.Execute('select * from Sales.Customer where AccountNumber like ?',
['AW000001%'],@Row) do
while Step do
assert(Copy(Row.AccountNumber,1,8)='AW000001');
end;
Props.Execute
返回ISQLDBRows
接口,因此上面的代码将初始化(或复用已有的)线程安全连接(OleDB使用每个线程模型),初始化一条语句,执行它,通过Step
方法和Row
变体访问行,通过Row.AccountNumber
语句直接行检索列值。
上述代码是完全安全的,所有内存都将通过ISQLDBRows
接口的引用计数垃圾回收特性释放。您不需要添加任何try..finally Free; end
代码。
这就是神奇的Delphi后期绑定。我们的SynBigTable
单元也提供了类似的特性。
实际上,这段代码比使用标准属性的访问要慢,如下所示:
while Step do
assert(Copy(ColumnUTF8('AccountNumber'),1,8)='AW000001');
但是前一个版本使用了列名的后期绑定,这看起来更自然。
当然,由于是后期绑定,编译器在编译时无法检查列名,如果源代码中的列名是错误的,则运行时才会触发错误。
首先,让我们看看访问行内容的最快方式。
在所有情况下,使用列名的文本版本('AccountNumber'
)比直接使用列索引要慢,即使我们的SynDB.pas
库使用散列快速查找,下面的代码总是更快:
var Customer: Integer;
begin
with Props.Execute(
'select * from Sales.Customer where AccountNumber like ?',
['AW000001%'],@Customer) do begin
Customer := ColumnIndex('AccountNumber');
while Step do
assert(Copy(ColumnString(Customer),1,8)='AW000001');
end;
end;
但是,老实说,在分析之后,发现大部分时间都花在了Step
方法上,尤其是fRowSet.GetData
。实际上,使用上面的代码,我没有发现任何值得一提的速度增长。
我们通过散列函数(即TDynArrayHashed
)查找名称的目标非常好。
相反,在分析之后发现基于Ole自动化的后期绑定要慢一些。事实上,Row.AccountNumber
表达式调用了一个隐含的DispInvoke
函数,该函数在多次调用时很慢。我们的SynCommons.pas
单元能够深入VCL,并通过修补内存中的VCL代码,调用该函数的优化版本,其结果是速度非常接近直接的Column['AccountNumber']
调用。
因为我们的SynDB.pas
单元不依赖于Delphi的DB.pas
单元,其结果集不从TDataset
继承。
这样做的好处是,当从对象代码访问这些结果集时,它们会快得多。
但缺点是,您不能在通常的VCL应用中使用它们。
为了便于SynDB.pas
与VCL组件的使用,可以从任何SynDB
查询创建TDataSet
结果集。
您可以使用两种优化的TDataSet结果集:
TDataSet类 | 操作 | 单元 | 备注 |
---|---|---|---|
TClientDataSet |
读写 | SynDBMidasVCL.pas |
由于内存复制而变慢 |
TSynSQLStatementDataSet |
只读 | SynDBVCL.pas |
直接映射,速度很快 |
因此,您可以将SynDB
请求的结果赋给任何TDataSource,例如我们的TSynSQLStatementDataSet
快速只读存储:
ds1.DataSet.Free; // release previous TDataSet
ds1.DataSet := ToDataSet(ds1,aProps.Execute('select * from people',[]));
或TClientDataSet
类型的内存存储:
ds1.DataSet.Free; // release previous TDataSet
ds1.DataSet := ToClientDataSet(ds1,aProps.Execute('select * from people',[]));
参见示例“17 - TClientDataset use
”了解更多关于使用此类TDataSet
的信息,包括一些速度信息。您需要先运行TestSQL3.dpr
回归测试,获得所需的SQlite3数据文件。
SynDB.pas
单元提供了一个类似TQuery
的类。这个类模拟通常的TQuery
类,没有继承DB.pas
及其缓慢的TDataSet
。
它模仿了基本的TQuery VCL方法,具有以下优点:
SynDB.pas
ISQLDBStatement
结果集上有自己的轻量级实现,因此通常要快得多;WideString
的形式返回数据;SynDBVCL.pas
中定义的ToDataSet()
函数从SynDB
的TQuery
创建TDataSet
。 当然,由于它不是TDataSet
组件,所以不能直接使用它作为RAD代码的简单替代。
但是,如果您的应用是以数据为中心,并且计划用一些类封装其业务逻辑,例如,如果它准备正确地实现OOP,而不是RAD,您仍然可以直接用TQuery仿真器替换现有代码:
Q := TQuery.Create(aSQLDBConnection);
try
Q.SQL.Clear; // optional
Q.SQL.Add('select * from DOMAIN.TABLE');
Q.SQL.Add(' WHERE ID_DETAIL=:detail;');
Q.ParamByName('DETAIL').AsString := '123420020100000430015';
Q.Open;
Q.First; // optional
while not Q.Eof do begin
assert(Q.FieldByName('id_detail').AsString='123420020100000430015');
Q.Next;
end;
Q.Close; // optional
finally
Q.Free;
end;
您应该使用TSQLDBStatement而不是这个包装器,但是使用这种TQuery兼容代码可以简化一些现有代码的升级,特别是对于遗留代码和现有项目。
它将有助于避免使用弃用过时的BDE,生成更小的可执行文件,访问各种数据库而无需支付大笔费用,避免对一个很大的遗留应用重写大量的现有代码行,或者让你的旧应用与数据库通过普通的HTTP通信,而不需要安装任何RDBMS客户端。
您可以使用TSynConnectionDefinition
将连接属性作为JSON内容保存在内存或文件中。
典型的存储内容可能是这样的:
{
"Kind": "TSQLDBSQLite3ConnectionProperties",
"ServerName": "database.db3",
"DatabaseName": "",
"UserID": "",
"Password": "PtvlPA=="
}
“Kind”
参数将用于存储实际的TSQLDBConnectionProperties
类名。因此,通过修改存储在JSON文本文件中的配置(无需重新编译应用程序),就可以在运行时轻松地从一个数据库切换到另一个数据库。注意,SynDB*的实现类单元应该编译到可执行文件中,如SynDBSQLite3.pas
用于TSQLDBSQLite3ConnectionProperties
或SynDBZeos.pas
用于TSQLDBZeosConnectionProperties
。
要从本地JSON文件创建一个新的TSQLDBConnectionProperties
实例,只需编写:
var Props: TSQLDBConnectionProperties;
...
Props := TSQLDBConnectionProperties.CreateFromFile('localDBsettings.json');
为安全起见,密码被加密并编码为Base64,您可以使用TSynConnectionDefinition
的Password
和PasswordPlain
属性来计算写到磁盘上的值。
因为TSynConnectionDefinition
是一个TSynPersistent
类,所以可以将它嵌套到应用程序中包含所有设置的TSynAutoCreateFields
实例中。
然后mORMot.pas
的ObjectToJSON
/ObjectToJSONFile
和JSONToObject
/JSONFileToObject
函数就可以将这些全局设置持久化到文件或数据库中。
有关ORM/REST级上的类似特性,请参见TSQLRest.CreateFrom()
,以及mORMotDB.pas
中定义的函数function TSQLRestCreateFrom( aDefinition: TSynConnectionDefinition)
,它创建一个普通的本地ORM的,如果aDefinition.Kind
是一个TSQLRest
类名;也可使用外部数据库存储的ORM,如果aDefinition.Kind
是TSQLDBConnectionProperties
类名。
从SynDB.pas
的逻辑视角来看,以下是数据库是如何访问的:
当然,正如SynDB
架构中所述,其实物理实现很复杂。
现在我们将详细介绍这些数据库连接如何实现SynDB.pas
接口。
OleDB(对象链接和嵌入数据库,也写成OLE DB或OLE-DB)是一种由Microsoft设计的API,用于统一访问来自各种数据源的数据。
当然,您可以使用Microsoft SQL Native客户端访问MS SQL Server 2005/2008/2012,Oracle也提供了一个本地OleDB驱动程序(但我们发现无论Oracle驱动程序,还是Microsoft版本,在blob方面都有问题)。不要忘记还有Advantage Sybase OleDB驱动程序等等…
如果你想连接到MS SQL Server,我们强烈推荐使用TOleDBMSSQL2012ConnectionProperties
类,对应SQLNCLI11
,Microsoft®SQL Server®2012本地客户端的一部分,它可以连接到各种MS SQL Server修订版(包括MS SQL Server 2008),这样更稳定。你可以从http://www.microsoft.com/en-us/download/details.aspx?下载操作系统对应的sqlncli.msi
。大多数情况下,您应该下载X64的sqlncli.msi
,该安装包也支持安装32位版本的SQL Server Native Client,因此也适用于32位的Delphi可执行文件,X86包只适用于32位Windows系统。
ODBC (Open DataBase Connectivity)是一种用于访问数据库管理系统(DBMS)的标准C语言中间件API。ODBC最初是在20世纪90年代初由Microsoft开发的,后来被OleDB取代。最近,微软正式宣布不支持OleDB,并敦促所有开发人员改用开放的、跨平台的ODBC API进行本地连接。又一次来自Micro$oft的糟糕的回退战略!
http://blogs.msdn.com/b/sqlnativeclient/archive/2011/08/29/microsoft-is-aligning-with-odbc-for-native-relational-data-access.aspx
通过使用我们自己的OleDB和ODBC实现,我们将能够直接将OleDB或ODBC二进制行转换为JSON,而不需要临时转换为Delphi高级类型(如临时字符串或变体分配)。由于我们绕过了BDE/dbExpress/FireDAC/AnyDAC组件集引入的许多层,因此性能要比使用标准TDataSet
或其他组件高得多。
大多数OleDB / ODBC驱动是免费的(包括由数据库所有者维护的),少数驱动提供商需要付费许可证。
值得指出的是,用于mORMot客户端-服务端架构时,OleDB或ODBC远程访问的对象持久性更希望实现服务器端的数据库实例访问。客户端可以通过标准HTTP进行通信,因此不需要任何特定的端口转发或其他IT配置就可以正常工作。
ZeosLib,又名Zeos,是一个开放源码库,它为Delphi、Kylix和Lazarus / FreePascal开发,提供许多数据库系统的本地访问。
它是完全面向对象的,具有完全模块化的设计。它通过包装本地客户端库来连接数据库,并通过抽象层ZDBC来访问它们。最初,ZDBC是JDBC 2.0(Java数据库连接API)到对象Pascal的一个端口。从那时起,API略有扩展,但主要思想没有改变,所以正式的JDBC 2.0规范是ZDBC API的主要切入点。
最新的7.x分支进行了深度重构,引入了新的方法和性能优化。事实上,我们与Michael (ZeosLib的主要贡献者)一起工作,以确保达到最优性能,所以mORMot和ZeosLib在读取或写入数据方面产生了令人印象深刻的协同作用。
自框架的1.18版本以来,我们将ZeosLib直接集成到mORMot持久层,并直接访问ZDBC层。也就是说,我们的SynDBZeos单元不引用DB.pas
单元,但可以直接访问ZDBC接口。
这种直接访问,绕过了VCL的DB.pas
层及TDataSet
瓶颈,非常接近我们的SynDB.pas
设计。因此,ZeosLib是mORMot的首选类库。SynDBZeos
单元也成为外部SQL数据库访问的优选。
我们建议您下载Zeos/ZDBC的7.2分支,它是撰写本文时的trunck分支。
Zeos/ZDBC作者已经进行了深入的代码重构(非常感谢Michael,又名EgonHugeist!),他甚至考虑到了mORMot的需要,以提供最佳的性能和集成,如UTF-8内容处理。
与之前的7.1版本相比,速度可以提高10倍以上,具体取决于数据库的后端和用例!
在写入数据(即Add/Update/Delete操作)时,已经向Zeos/ZDBC 7.2分支添加了数组绑定支持,我们的SynDBZeos单元将通过检测IZDatabaseInfo.SupportsArrayBindings
属性是否为true来使用它,目前Oracle和FireBird驱动程序就是这样。当按批处理模式处理时,我们的ORM从中受益,包括允许ZDBC创建优化的SQL。
在检索单条记录情况下,读取性能非常高,远远高于其它基于DB.pas
的数据库。如TSQLDBZEOSStatement.ColumnsToJSON()
将避免很多临时内存分配,并能够直接从底层ZDBC二进制缓冲区创建JSON。
如果你需要停留在7.2版本前,并希望使用SQlite3后端(但是你没有理由这样做,因为Zeos与SynDBSQlite3相比会很慢),对于Zeos<7.2的TZSQLiteCAPIPreparedStatement.ExecuteQueryPrepared()
和TZSQLiteResultSet.FreeHandle
方法,您需要应用一些补丁,正如SynDBZeos.pas
开头的注释所述。
如果你想通过Zeos/ZDBC连接MySQL,请遵循以下步骤:
libmysql.dll
,将其放入可执行文件夹中,或者放在系统路径中; fConnection := TSQLDBZEOSConnectionProperties.Create(
'zdbc:mysql://192.168.2.60:3306/world?username=root;password=dev', '', '', '');
URI()
方法: fConnection := TSQLDBZEOSConnectionProperties.Create(
TSQLDBZEOSConnectionProperties.URI(dMySQL,'192.168.2.60:3306'),'root','dev');
对于PostgreSQL,Zeos驱动程序只需要libpq.dll
和libintl.dll
,如从http://www.enterprisedb.com/products.services-training/pgbindownload下载。
PropsPostgreSQL := TSQLDBZEOSConnectionProperties.Create(
TSQLDBZEOSConnectionProperties.URI(dPostgreSQL,'localhost:5432'),
'dbname','username','password');
之后您可以使用TSQLDBZEOSConnectionProperties.URI()
方法来生成需要的ZDBC连接字符串:
PropsOracle := TSQLDBZEOSConnectionProperties.Create(
TSQLDBZEOSConnectionProperties.URI(dOracle,'','oci64\oci.dll'),
'tnsname','user','pass');
PropsFirebirdEmbedded := TSQLDBZEOSConnectionProperties.Create(
TSQLDBZEOSConnectionProperties.URI(dFirebird,'','Firebird\fbembed.dll')
'databasefilename','',');
PropsFirebirdRemote := TSQLDBZEOSConnectionProperties.Create(
TSQLDBZEOSConnectionProperties.URI(dFirebird,'192.168.1.10:3055',
'c:\Firebird_2_5\bin\fbclient.dll',false),
'3camadas', 'sysdba', 'masterkey');
请参阅TSQLDBZEOSConnectionProperties
文档,了解这些语法的更多信息,以及这个伟大的开源库的可用功能。
对于我们的框架,在实现SynDBZeos
、SynOleDB
、SynDBODBC
单元的同时,也实现了SynDBOracle
单元。它允许使用Oracle Call Interface直接访问任何远程Oracle服务器。
Oracle Call Interface (OCI)是面向Oracle数据库的最全面、高性能、本机非托管的接口,它公开了Oracle数据库的全部功能。直接接入oci.dll
库的接口是使用我们在SynDB.pas
中引入的DB抽象类编写的。
我们努力实现Oracle参考文档中关于Building High Performance Drivers for Oracle详细介绍的所有最佳实践模式。
得到的速度相当惊人:对于所有请求,SynDBOracle
的速度是使用Oracle提供的本地OleDB驱动程序的SynOleDB
连接的3到5倍。与Oracle的官方ODBC驱动程序(基于SynDBODBC
的连接)相比时,也发现了类似(甚至更糟糕)的速度损失。有关更详细的数字,请参见数据访问基准测试。
值得指出的是,使用mORMot客户端-服务端架构时,Oracle数据库对象持久性只需要在服务器端访问Oracle实例,就像使用OleDB或ODBC一样。
以下是SynDBOracle
单元的主要特点:
Int64
检索不带小数的NUMBER字段);'//host[:port]/[service_name]'
这样的连接字符串,避免使用TNSNAME.ORA
文件;TInt64DynArray
或TRawUTF8DynArray
作为参数,如用于SELECT .. IN
where子句;当然,该单元与外部SQL数据库访问过程完美集成。如具有本地导出JSON方法的特性,这是ORM框架的主要输入,直接在批处理序列处理数组绑定。
您可以使用Oracle提供的Oracle Instant Client (OIC)的最新版本,参见http://www.oracle.com/technetwork/database/features/instant-client,它允许在不安装标准巨大的Oracle客户机和配置ORACLE_HOME的情况下运行客户端应用程序。
只需在应用程序(可能是一个mORMot服务器)目录放入几个dll
文件,它就可以以惊人的速度工作,并具有Oracle的所有特性(其他独立的直接Oracle访问库依赖于已废弃的Oracle 8协议)。
译者注:Oracle Wallet,Oracle从10gR2开始提供了wallet,用于解决用户认证信息(用户名和密码)的存放问题,通过使用wallet可以实现无密码登录数据库,这样一来就不需要在应用程序中嵌入数据库密码(或者在配置文件中明文存放密码),同时更加方便维护大量服务器的环境的数据库密码维护工作,因为可以直接分发wallet文件,实现批量修改密码。
连接数据库的密码凭据现在可以存储在客户端Oracle Wallet中,这是一个用于存储身份验证和签名凭据的安全软件容器。
这种wallet的使用可以简化依赖密码凭据连接数据库的大规模部署。配置此功能后,应用程序代码、批处理作业和脚本不再需要嵌入用户名和密码。风险降低了,因为这样的密码不再显示地公开,而且在用户名或密码更改时无需更改应用程序代码,密码管理策略更容易执行。
为了使用这个特性,在连接数据库之前设置TSQLDBOracleConnectionProperties.UseWallet
为true
。
Wallet配置在服务端运行的计算机上执行。您必须执行一个完整的Oracle客户端安装:OIC,无需安装客户端的直接连接是不允许访问wallet身份验证的。
创建Wallet的步骤:
1) 为wallet创建一个文件夹:
> mkdir c:\OraWallets
2) 在命令行使用以下语法在客户端创建wallet:
> mkstore -wrl c:\OraWallets -create
Oracle会询问设置wallet主密码,一定要记住!
3) 在命令行使用以下语法在wallet中创建数据库连接凭证:
mkstore -wrl c:\OraWallets -createCredential TNS_alias_name_from_tnsnames_ora username password
其中password
是数据库用户的密码。Oracle将要求您输入wallet密码,使用前面步骤中的主密码。
4) 在客户端sqlnet.ora
文件中,添加WALLET_LOCATION
参数并将其设置为wallet的目录,并设置SQLNET.WALLET_OVERRIDE
参数为TRUE
:
SQLNET.WALLET_OVERRIDE = TRUE
WALLET_LOCATION =
(SOURCE =
(METHOD = FILE)
(METHOD_DATA =
(DIRECTORY = c:\OraWallets)
)
)
您不能在数据库有wallet时删除它,您需要通过以下命令删除wallet凭证:
mkstore -wrl wallet_location -deleteCredential db_alias
Oracle将要求输入wallet密码,使用与创建wallet时相同的密码。
请注意,如果您更喜欢使用GUI工具进行数据库管理,那么在数据库发行版中还可以使用Oracle Wallet Manager工具。
参见https://docs.oracle.com/cd/B28359_01/network.111/b28530/asowalet.htm
我们的ORM框架封装了一个高效的SQLite3,可以静态(即在exe
中)连接SQLite3引擎,也可以从外部SQLite3.dll
连接SQLite3引擎。
从我们的SynDB.pas
数据库抽象类调用SynSQLite3.pas
单元很容易。添加另一个这样的数据库只是一个非常轻量的层,它通过SynDBSQLite3.pas
单元实现。
如果希望将SQLite3引擎链接到项目可执行文件,请确保您的uses
子句引用了SynSQLite3Static.pas
单元。否则,定义一个TSQLite3LibraryDynamic
实例来加载外部sqlite3.dll
库:
FreeAndNil(sqlite3); // release any previous instance (e.g. static)
sqlite3 := TSQLite3LibraryDynamic.Create;
要创建到已存在的SQLite3数据库文件的连接属性,请调用TSQLDBSQLite3ConnectionProperties.Create
构造函数,将实际的SQLite3数据库文件作为ServerName参数,可以在Password
中选择使用加密密码(自1.16之后版本可用),而忽略其它参数(DataBaseName
,UserID
)。
这些类将实现一个内部语句缓存,就像用于TSQLRestServerDB
的那样。在实践中,使用缓存可以使处理速度提高两倍(在处理小的请求时)。
在mORMot ORM中您有两种访问SQLite3引擎的方法:
如果您基于mORMot的应用仅是使用一个集中的SQLite3数据库,那么使用SynDBSQLite3外部表是没有意义的。但是,如果您希望将来能够连接到任何外部数据库,或者将数据分割到几个数据库文件中,那么使用外部SQLite3表就很有意义。当然,SQlite3引擎库本身将与内部和外部进程共享。
自框架的1.18修订版,引入了一个新的SynDBDataset.pas
单元,支持各种基于DB.pas
的数据库与我们的SynDB.pas
类对接,并使用TDataset
获取结果。由于TDataset
的设计,相对于直接d的SynDB.pas
连接(例如SQLite3或Oracle的结果),性能有所下降,但它也打开了各种可能的数据库访问。
在mORMot源代码存储库的SynDBDataset
子文件夹中已经发布了一些专用的驱动程序。到目前为止,已对接FireDAC(以前是AnyDAC)、UniDAC和BDE库,并且可以直接连接到NexusDB引擎。
由于这里有许多可能的组合(请参阅[SynDB架构),所以欢迎反馈。由于我们的敏捷过程,我们将首先关注于我们需要和使用的驱动,这取决于mORMot用户要求的额外特性,并在可能的情况下提供包装器,或者至少提供测试功能,包括Embarcadero刚刚收购了AnyDAC并对其进行了改进并重新命名为FireDAC,以使其成为新的官方平台,DBExpress也将受益于该整合。
NexusDB是一个“royalty-free、兼容SQL:2003核心、客户端/服务端和嵌入式数据库系统,具有与其他大量许可产品相竞争的特性”(供应商说的)。
译者注:royalty-free,免版税。版税制是国际出版业通行的使用作品的支付方式,近年来我国也在逐渐实行。具体地讲,它是指著作权人因他人使用其作品而获得的一定货币份额。版税率一般多在6%到10%之间。以出版版税为例。出版版税的计算方法是,图书单价×图书印数或销量×版税率。RF是Royalty-Free的缩写,直接翻译成中文是“免版税”。用户在网上购买下载的图片,不需要按照其使用图片取得的收入的一定比例来向作者支付“版税”。而是一次性支付小额的使用费,便可以获得作者的授权使用。所以我们成为“RF授权”,具有“一次购买,多次使用”的特点。
我们使用并测试了免费的嵌入式版本,它非常适合像mORMot这样的客户端-服务端ORM框架,请参见http://www.nexusdb.com/support/index.php
FireDAC是一组独特的通用数据访问组件,用于在Delphi上开发跨平台数据库应用程序。这实际上是一个第三方组件集,由Embarcadero从DA-SOFT Technologies收购(以前称为AnyDAC),并包含在Delphi XE3自后版本中。这是Delphi中用于高速数据库开发的新官方平台,支持现在已被弃用的DBExpress。
我们已经优化了SynDB.pas
集成单元和mORMot持久层。如您可以通过ORM批处理过程,通过所谓的数组绑定,直接访问高速的FireDAC数组DML特性。
通用数据访问组件(UniDAC)是一个跨平台的组件库,提供Delphi对多个数据库的直接访问。参见http://www.devart.com/unidac
例如,要访问MySQL远程数据库,您应该使用:
PropsMySQL := TSQLDBUniDACConnectionProperties.Create(
TSQLDBUniDACConnectionProperties.URI(dMySQL,'192.168.2.60:3306'),
'world', 'root', 'dev');
与FireDAC相比,这个库提供了相当稳定的结果,但是缺少数组绑定特性。
Borland Database Engine (BDE)是Delphi早期版本附带的基于windows核心的数据库引擎和连接软件。即便它被弃用,自2000年以来被DBExpress所取代,但它仍然是一个工作解决方案,易于与SynDB.pas
驱动进行接口。
请不要在任何新项目上使用BDE !
您最好切换到另一个访问库。
SynDBRemote.pas
单元允许您创建在远程HTTP服务器上执行SQL操作的数据库应用程序,而不用在数据库服务器上。您可以像任何其他SynDB.pas
一样创建连接数据库,但传输将通过HTTP进行。因此,不需要在最终用户应用程序上部署数据库客户端:它只会使用HTTP请求,包括在Internet上也是如此。您可以使用SynDB.pas
类的所有特性,可以轻松地实现一个优化的HTTP连接。
这个特性不是RESTful ORM的一部分,因此不使用mORMot.pas
单元,它有自己的优化协议,使用增强的安全性(传输加密与用户身份验证和可选的HTTPS)和自动数据压缩。仅使用了SynCrtSock.pas
单元的HTTP客户端和服务端类。
由于您的应用程序可以同时使用TDataSet
(请参阅TDataSet和SynDB)和TQuery
(请参阅TQuery仿真类),这种新的传输方式可以轻松地将现有Delphi客户端-服务端应用程序转换为多层体系结构,而只需对源代码进行很小的改动。而对于您的新代码,您可以使用mORMot的RESTful功能切换到SOA/ORM设计。
传输协议采用优化的二进制格式,同时在两端进行压缩、加密和数字签名,远程用户身份验证将通过挑战验证方案执行。如果需要,还可使用http.sys`内核模式发布HTTPS服务。
为发布您的SynDB.pas
连接,您需要初始化一个在SynDBRemote.pas
中定义的TSQLDBServer*
类:
您可以基于TSQLDBServerSockets
套接字API 定义HTTP服务器,也可以基于TSQLDBServerHttpApi
类(仅Windows)定义更快、更稳定的HTTP服务,是基于Windows XP版本之后的http.sys
内核模式的HTTP服务。
对于客户端,您可以使用SynDBRemote.pas
中定义下列类:
注意,TSQLDBHttpRequestConnectionProperties
是一个抽象的父类,因此不应该直接实例化它,而应该实例化它的派生实现。
如您所见,您可以在普通套接字API客户端,WinINet
或WinHTTP
(在Windows下)客户端,libcurl
API(主要在Linux上)之间进行选择。在Windows上,TSQLDBWinHTTPConnectionProperties
类在Internet上更稳定,即使普通套接字在localhost
上的数据访问基准测试给出的数据更好。
可以这样定义HTTP服务:
uses SynDB, // RDBMS core
SynDBSQLite3, SynSQLite3Static, // static SQLite3 engine
SynDBRemote; // for HTTP server
...
var Props: TSQLDBConnectionProperties;
HttpServer: TSQLDBServerAbstract;
...
Props := TSQLDBSQLite3ConnectionProperties.Create('data.db3','','','');
HttpServer := TSQLDBServerHttpApi.Create(Props,'syndbremote','8092','user','pass');
上面的代码将初始化到本地data.db3
的SQlite3数据库连接(在Props变量中),然后通过http.sys
的HTTP内核模式将服务发布到http://1.2.3.4:8092/syndbremote URI,假定服务器的IP是1.2.3.4。
首先,使用'user'
/'pass'
要素定义了用户。注意,在我们的远程访问中,用户管理与RDBMS用户权限不一致:您最好在应用程序级别拥有自己的一组用户,以获得更高的安全性,并更好地与业务逻辑集成。如果想在RDBMS上创建新用户可能会很痛苦,但在SynDBRemote.pas
侧,通过Protocol.Authenticate
属性,管理远程用户身份验证则非常容易:
HttpServer.Protocol.Authenticate.AuthenticateUser('toto','pipo');
HttpServer.Protocol.Authenticate.AuthenticateUser('toto2','pipo2');
...
您还可以依照用户逐一认证所述的方法共享mORMot的REST身份验证用户,方法是将缺省的TSynAuthentication
类实例替换为TSynAuthenticationRest
,就像在mORMot.pas
中定义的那样。注意,同时使用SynDBRemote
和mORMot的ORM/SOA听起来像是一种弱设计,但是在处理遗留代码和许多现有SQL语句时可能会有它的优点。
URI应该进行注册,就像http.sys
API所期望的那样。您可以使用系统管理员权限运行服务端,或者在安装应用程序中调用以下方法(就像我们在TestSQL3Register.dpr中所做的那样):
THttpApiServer.AddUrlAuthorize('syndbremote','8092',false,'+');
在客户端,你可以这样写:
uses SynDB, // RDBMS core
SynDBRemote; // for HTTP client
...
var Props: TSQLDBConnectionProperties;
...
Props := TSQLDBWinHTTPConnectionProperties.Create('1.2.3.4:8092','syndbremote','user','pass');
如您所见,在客户端无需连接到SynDBSQLite3.pas
、SynSQLite3Static.pas
,只需要HTTP链接。不需要在应用程序中部署RDBMS客户端库,也不需要设置本地网络防火墙。
我们在这里定义了一个具有'user' / 'pass'凭据的单个用户,但是您可以使用TSQLDBServerAbstract
的Protocol.Authenticate
属性在服务器端管理更多的用户。。
然后,像往常一样使用连接执行您最喜欢的SQL:
procedure Test(Props: TSQLDBConnectionProperties);
var Stmt: ISQLDBRows;
begin
Stmt := Props.Execute('select * from People where YearOfDeath=?',[1519]);
while Stmt.Step do begin
assert(Stmt.ColumnInt('ID')>0);
assert(Stmt.ColumnInt('YearOfDeath')=1519);
end;
end;
或者可以通过SynDBVCL.pas
单元与VCL组件一起使用:
ds1.DataSet.Free; // release previous TDataSet
ds1.DataSet := ToDataSet(ds1,Props.Execute('select * from people',[]));
TSynSQLStatementDataSet
结果集将直接映射TSQLDBServer*
类返回的原始二进制数据,从而避免客户端应用程序中各种缓慢的数据编组,即使是大型内容。所有的数据都是由服务端计算和发送的:即使只在TDBGrid中显示一行,数据也是服务端传输的。事实上,只检索部分数据在本地网络上工作得很好,但在Internet上大量传输却不是一个好主意,因为太多的ping
操作。因此,可以考虑添加一些筛选字段或一些应用程序级分页,以减少需要从SynDBRemote
服务端检索的行数。
如果服务端类定义了自己的TSynAuthentication
类(如通过TSynAuthenticationRest
使用REST用户和组),您应该创建自己的类,并覆盖以下方法:
procedure TSQLDBWinHTTPConnectionPropertiesRest.SetInternalProperties;
begin
if fProtocol=nil then
fProtocol := TSQLDBRemoteConnectionProtocol.Create(
TSynAuthenticationRest.Create(nil,[]));
inherited;
end;
该重载方法从TSQLDBWinHTTPConnectionProperties
继承其所有行为,但使用ORM/SOA身份验证方案在服务端验证其用户。
您可以使用这个远程连接特性,如将一个独立的共享SQLite3数据库升级为高性能、低维护的客户端-服务器数据库引擎。您可以在服务器端创建它:
var props: TSQLDBSQLite3ConnectionProperties;
server: TSQLDBServerHttpApi;
...
props := TSQLDBSQLite3ConnectionProperties.Create('database.db3','','','');
props.MainSQLite3DB.Synchronous := smOff;
props.MainSQLite3DB.LockingMode := lmExclusive; // tune the performance
server := TSQLDBServerHttpApi.Create(props,'syndbremote','8092','user','password');
...
您可以通过创建如下属性来共享现有的SQlite3数据库实例(如创建用于RESTful ORM的TSQLRestServerDB,请参阅数据库层):
props := TSQLDBSQLite3ConnectionProperties.Create(aRestServerDB.DB);
server := TSQLDBServerHttpApi.Create(props,'syndbremote','8092','user','password');
当使用http.sys
内核模式服务,如果数据库名称(即这里的'syndbremote'
)与ORM表或方法服务不冲突,那么在普通ORM/SOA操作(对于纯粹的HTTP服务可能是80)和远程SynDB
访问之间可以共享相同的IP端口。
您还可以在服务器和服务端设置自己的TSQLDBProxyConnectionProtocol
类来定制传输协议。
我们的SynDBExplorer
工具能够将任何SynDB
连接作为HTTP服务发布,或者通过HTTP连接它。这样可能非常方便,包括调试来说也是。
要为现有数据库提供服务,只需像往常一样连接它,然后单击列表左下方的"HTTP Server
"按钮。您可以调整服务端属性(HTTP端口、用于URI的数据库名称、用户凭据),然后单击"Start
" 按钮。
要连接到这个远程连接,请再运行一个SynDBExplorer实例。,使用"Remote HTTP
"作为连接类型创建一个新的连接,并根据服务端设置相应的选项值,替换默认的"localhost:8092
"(替换localhost
为服务器IP以通过网络访问)为服务器名称、"syndbremote
" 为数据库名称、"synopse
"为用户名和密码。
这样就能远程访问主服务器实例,就像通过常规客户端访问数据库一样。
如果服务端数据库是SQLite3,那么您只需将这个本地引擎升级为一个真正的客户端-服务端数据库,您可能会对这样做的性能感到惊讶。
即使你可以使用这样的远程访问来实现一个n层架构,但您还是应该使用mORMot的客户端-服务端ORM,这样会提供更好的客户端-服务端集成,领域驱动设计模式的无感持久性,更好的OOP和SOLID模型设计,比原始SQL操作更高的性能。我们轻量化的mORMot不是简单在上面添加了数据传输层的ORM:它是一个完整的RESTful系统,是真正的SOA设计。
但是为了将一些遗留的SQL代码集成到一个新的架构,SynDBRemote.pas
有它的优点,可以与mORMot的高级特性结合使用。
请注意,对于跨平台客户端,mORMot的ORM/SOA模式是一种更好的方法:不要在移动应用程序中使用SQL,而是使用服务,这样您就不需要对业务逻辑进行各种小的修改之后重新验证并将应用程序重新发布到商店!
在使用任何对象关系映射(ORM)时,您主要有两种可能:
我们的mORMot框架两条路径都支持,即使像其他ORM一样,代码优先听起来更直接。
外部记录可以按照mORMot的ORM所要求的那样定义:
type
TSQLRecordPeopleExt = class(TSQLRecord)
private
fData: TSQLRawBlob;
fFirstName: RawUTF8;
fLastName: RawUTF8;
fYearOfBirth: integer;
fYearOfDeath: word;
fLastChange: TModTime;
fCreatedAt: TCreateTime;
published
property FirstName: RawUTF8 index 40 read fFirstName write fFirstName;
property LastName: RawUTF8 index 40 read fLastName write fLastName;
property Data: TSQLRawBlob read fData write fData;
property YearOfBirth: integer read fYearOfBirth write fYearOfBirth;
property YearOfDeath: word read fYearOfDeath write fYearOfDeath;
property LastChange: TModTime read fLastChange write fLastChange;
property CreatedAt: TCreateTime read fCreatedAt write fCreatedAt;
end;
可见,这与内部ORM类没有区别:它继承了TSQLRecord
,您也可以从TSQLRecordMany
继承,以便使用基于数据透视表的ORM实现。
唯一的区别是index 40
属性定义了FirstName
和LastName
的发布属性:定义了创建文本列外部字段时使用的长度(UTF-16 WideChar
或UTF-8编码):
property FirstName: RawUTF8
index 40
read fFirstName write fFirstName;
事实上,SQLite3并不关心文本字段的长度,但是几乎所有其他数据库引擎都希望在表中定义VARCHAR
列时指定最大长度。如果您没有在字段定义中指定任何长度(如没有定义index ???
属性), ORM将创建一个长度不受限制的列(如MS SQL Server的varchar(max)
)。在这种情况下,代码可以工作,但是性能和磁盘效率可能会大大降低,通过CLOB访问的速度明显变慢。这种性能损失的唯一例外是SQlite3和PostgreSQL,对于它们,无限制的文本列的处理速度与varchar(#)
一样快。
默认情况下,ORM不会执行任何检查,来确保字段长度符合外部数据库中的列大小要求。您可以使用TSQLRecordProperties
的SetMaxLengthValidatorForTextFields()
或SetMaxLengthFilterForTextFields()
方法在发送数据到外部数据库之前先执行验证和筛选规则,请参阅筛选和验证。
以下是对于外部数据库的回归测试摘要:
var RExt: TSQLRecordPeopleExt;
(...)
fProperties := TSQLDBSQLite3ConnectionProperties.Create(SQLITE_MEMORY_DATABASE_NAME,'','','');
VirtualTableExternalRegister(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal');
aExternalClient := TSQLRestClientDB.Create(fExternalModel,nil,'testExternal.db3',TSQLRestServerDB);
try
aExternalClient.Server.StaticVirtualTableDirect := StaticVirtualTableDirect;
aExternalClient.Server.CreateMissingTables;
Check(aExternalClient.Server.CreateSQLMultiIndex(
TSQLRecordPeopleExt,['FirstName','LastName'],false));
(...)
Start := aExternalClient.ServerTimestamp;
(...)
aID := aExternalClient.Add(RExt,true);
(...)
aExternalClient.Retrieve(aID,RExt);
(...)
aExternalClient.BatchStart(TSQLRecordPeopleExt);
aExternalClient.BatchAdd(RExt,true);
(...)
Check(aExternalClient.BatchSend(BatchID)=HTTP_SUCCESS);
Check(aExternalClient.TableHasRows(TSQLRecordPeopleExt));
Check(aExternalClient.TableRowCount(TSQLRecordPeopleExt)=n);
(...)
RExt.FillPrepare(aExternalClient,'FirstName=? and LastName=?',
[RInt.FirstName,RInt.LastName]); // query will use index -> fast :)
while RExt.FillOne do ...
(...)
Updated := aExternalClient.ServerTimestamp;
(...)
aExternalClient.Update(RExt);
aExternalClient.UnLock(RExt);
(...)
aExternalClient.BatchStart(TSQLRecordPeopleExt);
aExternalClient.BatchUpdate(RExt);
(...)
aExternalClient.BatchSend(BatchIDUpdate);
(...)
aExternalClient.Delete(TSQLRecordPeopleExt,i)
(...)
aExternalClient.BatchStart(TSQLRecordPeopleExt);
aExternalClient.BatchDelete(i);
(...)
aExternalClient.BatchSend(BatchIDUpdate);
(...)
for i := 1 to BatchID[high(BatchID)] do begin
RExt.fLastChange := 0;
RExt.CreatedAt := 0;
RExt.YearOfBirth := 0;
ok := aExternalClient.Retrieve(i,RExt,false);
Check(ok=(i and 127<>0),'deletion');
if ok then begin
Check(RExt.CreatedAt>=Start);
Check(RExt.CreatedAt<=Updated);
if i mod 100=0 then begin
Check(RExt.YearOfBirth=RExt.YearOfDeath,'Update');
Check(RExt.LastChange>=Updated);
end else begin
Check(RExt.YearOfBirth<>RExt.YearOfDeath,'Update');
Check(RExt.LastChange>=Start);
Check(RExt.LastChange<=Updated);
end;
end;
end;
(...)
如上可见,使用本地SQLite3引擎或远程数据库引擎没有区别。
从客户端角度看,您只需调用通常的RESTful CRUD方法,即Add() Retrieve() Update() UnLock() Delete()
,或更快的Batch*()
修订,甚至如带有复杂WHERE子句的FillPrepare
、服务端的CreateSQLMultiIndex / CreateMissingTables
等高级方法。
在远程数据库中创建表('CREATE table…’
SQL语句)是由框架在调用CreateMissingTables方法时执行,并根据数据库的要求(如SQLite3的文本将是Oracle的NVARCHAR2
字段)使用适当的列属性。
在外部数据库上创建的表如下:
唯一特别的指令是全局VirtualTableExternalRegister()
函数,它必须运行在服务端(在客户端上运行它没有任何意义,因为客户端没有表,简单的说就是客户端不关心存储,这是服务端的事)。
为了按预期工作,应该在TSQLRestServer
构造函数之前调用VirtualTableExternalRegister()
。当服务端初始化时,ORM服务器必须知道需要管理内部或外部数据库。在上面的代码中,TSQLRestClientDB.Create()
将实例化它自己的嵌入式TSQLRestServerDB
实例。
注意,TSQLRecordExternal.LastChange
字段被定义为TModTime:这样,每次记录更新时,都会存储当前日期和时间,也就是说,如每次的aExternalClient.Add
和aExternalClient.Update
调用。测试代码使用循环来检查RExt.LastChange>=Start
和RExt.LastChange<=Updated
,记录的时间是“服务器时间”,即当前服务器上的日期和时间,在外部数据库的情况下,是远程服务器的时间(在MS SQL将执行select getdate()
获取日期并插入LastChange
)。为了获取服务端时间戳,应该调用Start := aExternalClient.ServerTimestamp
而不是TimeLogNow
本地时间函数。
对CreatedAt
发布字段(定义为TCreateTime)也测试了类似的特性:它将在创建记录时自动设置为当前服务器时间(在修改时不会更改),这就是上述代码检查RExt.CreatedAt<=Updated
的目的。
正如前面看到的,下面一行初始化ORM,以便使用外部数据库连接fProperties
通过SQL访问TSQLRecordPeopleExt
数据:
VirtualTableExternalRegister(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal');
我们还定制了外部表的名称,将默认的'PeopleExt'
(通过从TSQLRecordPeopleExt
去掉TSQLRecord
前缀)改为'PeopleExternal'
。
除了映射表名之外,ORM还能够将TSQLRecord
的发布属性名映射为数据库的各种自定义列名。实际上,现有数据库的很多表没有显式的列名是非常普遍的,当直接映射为TSQLRecord属性名时,这听起来可能非常奇怪,甚至已有数据库的主键与ORM将其命名为ID
的要求也不尽一致。
默认情况下,对于代码驱动的方法,内部属性名称将与外部表列名匹配,请参见TSQLRecordPeopleExt代码优先的字段/列映射。
您可以对默认映射进行定制,例如:
fProperties := TSQLDBSQLite3ConnectionProperties.Create(SQLITE_MEMORY_DATABASE_NAME,'','','');
VirtualTableExternalRegister(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal');
fExternalModel.Props[TSQLRecordPeopleExt].ExternalDB.
MapField('ID','Key').
MapField('YearOfDeath','YOD');
(...) // the remaining code stays the same
作为替代,您可以使用更简洁的VirtualTableExternalMap
函数接口:
fProperties := TSQLDBSQLite3ConnectionProperties.Create(SQLITE_MEMORY_DATABASE_NAME,'','','');
VirtualTableExternalMap(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal').
MapField('ID','Key').
MapField('YearOfDeath','YOD');
之后像平常一样在Delphi代码中使用TSQLRecordPeopleExt
表,包含ID
和YearOfDeath
字段。
但在底层,mORMot ORM将在创建所有需要的SQL语句时进行映射:
TSQLRecord
类将存储在PeopleExternal外部表中;TSQLRecord.ID
字段将是一个外部“Key: INTEGER”
列;”TSQLRecord.YearOfDeath
字段将是一个外部“YOD: INTEGER”
列;因此,结果映射如下:
注意,只有ID
和YearOfDeath
列名是定制的。
由于SQLite3虚拟表的设计,以及目前mORMot内部构件,数据库主键必须是一个整数字段,以便按照ORM的要求进行映射。但是您可以通过stored AS_UNIQUE
代码定义各种辅助键,如对文本字段。
您很可能必须基于现有的数据库,使用许多已经编写好的SQL语句来维护和发展遗留项目,请参阅遗留代码和项目。如您希望将mORMot用于新特性,添加移动或HTML客户端。
在这种情况下,ORM高级特性(如ORM缓存或批处理)可能与遗留代码冲突,因为可能需要共享表。在处理这样一些项目时,这里有一些指导原则。
为了详尽地说明这个问题,我们需要考虑每一个ORM CRUD操作。我们可能需要将它们分为三类:查询、插入和对现有数据的修改。
关于ORM查询,即Retrieve()
方法,ORM缓存可以根据表进行调优,您肯定缺少一些缓存,但请记住:
您可以为该缓存设置一个“超时”周期,以便在大多数情况下仍可从中获益;
您在服务级有一个缓存,在客户级有另一个缓存,因此您可以对其进行优化,使其在客户端上不那么具有侵略性;
您可以根据每个ID优化ORM缓存,以便仍然可以缓存一些不太可能更改的项目。
关于ORM插入,即Add()
或BatchAdd()
方法,在使用外部引擎时,如果一些外部进程可能插入新行,请确保将TSQLRestStorageExternal EngineAddUseSelectMaxID
属性设置为TRUE,以便手工计算下一个最大ID。
但这仍然可能是一个问题,因为外部进程可能在ORM插入期间执行插入。
因此,最好不要使用ORM Add()或BatchAdd()方法,而是依赖于专用的INSERT SQL语句,如托管在服务端基于接口的服务中。
关于ORM修改,即Update() Delete() BatchUpdate() BatchDelete()
方法,它们听起来很安全,可以与修改数据库的外部进程一起使用,只要您使用事务使修改成为原子的,并且不会与遗留代码中的任何并发修改冲突。
在处理一些遗留代码将在后台修改的外部表时,可能更安全的模式是绕过这些ORM方法,并定义基于服务端接口的服务。这些服务可能包含手动SQL,而不是使用神奇的ORM。但是这将取决于您的业务逻辑,并且您将无法从框架的ORM特性中获益。
然而,在应用程序中引入面向服务的体系结构(SOA)将非常有益:ORM不是强制性的,特别是如果您熟悉SQL查询,知道如何使它们尽可能地标准化,拥有大量遗留代码,并且可能已经调优了SQL语句。
要将新的客户端(如移动应用程序或AJAX现代站点)接入到应用程序,必须引入SOA。公平地说,您不应该再直接访问数据库,就像以前使用Delphi应用程序和RAD DB组件那样。
所有新特性,包括用于存储新数据的新表,仍然受益于mORMot的ORM,并且仍然可以托管在相同的外部数据库中,与现有代码共享。
然后,您将能够在遗留代码中识别seam(请参阅遗留代码和项目),并将它们移动到新的mORMot服务中,然后让您的应用程序演化为新的SOA/MVC体系结构,而不会破坏任何东西,也不会从头开始。
如果应用程序可能运行在多个数据库上,那么从一个引擎切换到另一个引擎时,可能很难处理各种潜在的字段名称冲突。因此,ORM将确保没有字段名与底层数据库的SQL关键字冲突。
在代码优先模式下,您可以使用以下方法来确保不会发生这种冲突:
fExternalModel.Props[TSQLRecordPeopleExt].ExternalDB.MapAutoKeywordFields;
对于数据库优先的数据库,将使用以下语法来检查字段名:
fExternalModel.Props[TSQLRecordPeopleExt].ExternalDB. // custom field mapping
MapField('ID','Key').
MapField('YearOfDeath','YOD').
MapAutoKeywordFields;
或者使用简洁完整的接口定义:
VirtualTableExternalMap(fExternalModel,TSQLRecordPeopleExt,fProperties,'PeopleExternal').
MapField('ID','Key').
MapField('YearOfDeath','YOD').
MapAutoKeywordFields
对于数据库优先的数据库,在任何手动字段映射之后调用MapAutoKeywordFields
方法是一个好主意,因为即使是自定义字段名称也可能与SQL关键字冲突。
如果一些字段名可能与SQL关键字冲突,那么它将映射为后跟'_'
。如'Select'
发布属性将映射为表中的SELECT_
列。
即使这个选项默认情况下是禁用的,日志中也会出现一条警告消息,建议使用这个MapAutoKeywordFields
方法,以帮助您识别此类问题。
mORMotDB.pas
单元为框架基于SynDB.pas
的外部数据库实现虚拟表访问。
实际上,这个特性将使用TSQLRestStorageExternal
、TSQLVirtualTableCursorExternal
和TSQLVirtualTableExternal
类,定义如下:
类的注册是通过调用以下新的全局过程完成的:
procedure VirtualTableExternalRegister(aModel: TSQLModel; aClass: TSQLRecordClass;
aExternalDB: TSQLDBConnectionProperties; const aExternalTableName: RawUTF8);
该程序将在服务器端为ORM类注册一个外部数据库:
TSQLRecordVirtualTableAutoID
类(如在这个ORM模型中,TSQLModelRecordProperties.Kind
属性将被覆写为rCustomAutoID
;TSQLVirtualTableExternal
模块关联;TSQLDBConnectionProperties
实例应该由所有类共享,并在ORM不再需要时全局释放;SQLTableName
将在内部用作表名),如果没有指定表名("),将使用SQLTableName
(如名为TSQLCustomer
的类,使用'Customer'
);TSQLRestStorage.AdaptSQLForEngineList
方法。典型用法如下:
aProps := TOleDBMSSQLConnectionProperties.Create('.\SQLEXPRESS','AdventureWorks2008R2','','');
aModel := TSQLModel.Create([TSQLCustomer],'root');
VirtualTableExternalRegister(aModel,TSQLCustomer,aProps,'Sales.Customer');
aServer := TSQLRestServerDB.Create(aModel,'application.db'),true)
如对象关系映射所述,其余所有代码将使用普通ORM类、方法和函数。
为了存储在外部数据库中,ORM记录可以从各种TSQLRecord
类继承。即使这个类没有从TSQLRecordVirtualTableAutoID
继承,一旦为该类调用了VirtualTableExternalRegister
函数,它也会这样做。
与任何普通的TSQLRecord
类一样,ORM核心希望外部表映射一个Integer ID
发布属性,在每次插入记录时自动递增。由于并非所有数据库都处理此类字段,如Oracle自动增量将通过在初始化时执行select max(id) from tablename
语句处理,然后通过线程安全缓存动态计算新插入使用的RowID。
您不必知道数据持久性存储在哪里以及如何存储,框架将为您完成所有底层的数据库工作。由于SQlite3的虚拟表特性,内部和外部表可以在SQL语句中混合使用。根据实现需要,类可以通过内部SQLite3引擎或通过外部数据库持久化,只需在服务端初始化之前调用VirtualTableExternalRegister()
即可。
实际上,TSQLVirtualTableCursorExternal
会根据外部数据库已有的索引,将外部表上的任何查询转换为正确优化的SQL查询。TSQLVirtualTableExternal
还将把SQLite3级上的单个SQL修改语句(如insert / update / delete)转换为外部数据库的远程SQL语句。
大多数情况下,所有RESTful方法(GET/POST/PUT/DELETE)都将由TSQLRestStorageExternal
类直接处理,不会使用虚拟表机制。在实践中,对外部数据库的大多数访问都与直接访问一样快,但是虚拟表总是能够解释任何跨数据库的复杂请求或语句。
直接REST访问将按照以下方式处理,如添加对象时:
通过虚拟表的间接访问将处理如下:
关于速度,这里是回归测试日志文件的摘录(见前一段的代码),它显示了RESTful调用和虚拟表调用之间的区别,处理超过11,000行数据:
- External via REST: 133,666 assertions passed 409.82ms
- External via virtual table: 133,666 assertions passed 1.12s
第一次运行使用TSQLRestServer.StaticVirtualTableDirect
设置为TRUE(这是默认设置),也就是说,它将直接调用TSQLRestStorageExternal
来执行RESTful命令;第二次将把这个属性设置为FALSE,也就是说,它将调用SQLite3引擎,并通过虚拟表机制将其转换为另一个SQL调用。
值得指出的是,该测试使用内存SQLite3数据库(即通过SQLITE_MEMORY_DATABASE_NAME
伪文件名实例化)作为外部数据库,因此我们在这里测试的主要是ORM开销,而不是外部数据库速度。对于真正的基于文件或远程数据库(如MS SQL),远程连接的开销远远大于虚拟表的使用。
在所有情况下,默认的StaticVirtualTableDirect=true
将确保最佳性能。正如数据访问基准测试所述,使用虚拟或直接调用不会影响CRUD操作速度:只要可能,它将绕过虚拟引擎。
下面将详细介绍服务端的多线程能力以及所有可用的设置。
默认情况下,所有ORM读操作将在并发模式下运行,所有ORM写操作将在阻塞模式下执行。所以使用我们的内部SQLite3引擎或大多数外部数据库,既安全又快速。但您可能需要更改此默认行为,具体取决于所连接的外部引擎。
通常TSQLDBConnectionProperties
将从TSQLDBConnectionPropertiesThreadSafe
继承,因此将为每个连接创建一个线程,这是有效的,但是一些驱动可能会有问题。
首先,一些数据库客户端库可能不允许在多个线程之间共享事务,如MS SQL。其他客户端可能会为每个连接消耗大量资源,或者可能没有良好的多线程伸缩能力。有些数据库服务端确实为每个连接的客户端分配不同的进程,如PostgreSQL:您可能希望只使用一个连接来减少服务端资源,因此服务端只运行一个进程。为了避免这些问题,您可以强制所有ORM写操作在一个专用线程中执行,即通过设置amMainThread
(这在没有UI的服务端不太合适),或者更好地通过amBackgroundThread
或amBackgroundORMSharedThread
:
aServer.AcquireExecutionMode[execORMWrite] := amBackgroundThread;
其次,特别是在长时间运行的n层mORMot服务端,可能会出现连接异常中断。如一个晚上没有任何活动之后,对外部数据库的访问可能在早上失败,因为数据库服务器可能断开了连接。
您可以使用TSQLDBConnectionProperties.ConnectionTimeOutMinutes
属性指定最大不活动时间,在此之后将刷新和重新创建所有连接,以避免潜在的断开连接问题。
在实践中,在一段时间后重新创建连接是安全的,不会减慢进程,相反,它可能有助于减少消耗的资源,并稳定长时间运行的n层服务器。
ThreadSafeConnection
方法将检查其TSQLDBConnectionProperties
实例上的最后一个活动,然后调用ClearConnectionPool
,以便在空闲时间过长时释放所有活动连接。
因此,如果您使用ConnectionTimeOutMinutes
属性,您应该确保后台没有其他连接仍然处于活动状态,否则可能会发生一些意外问题。
例如,您应该确保您的mORMot ORM服务器在读写的阻塞模式下运行所有语句:
aServer.AcquireExecutionMode[execORMGet] := am***;
aServer.AcquireExecutionMode[execORMWrite] := am***;
在这里,安全阻塞am***
模式是除amunlock
外的任何模式,即amLocked
、amBackgroundThread
、amBackgroundORMSharedThread
或amMainThread
。