在比较ORM和标准SQL时,必须突出显示一些方面。
首先,您不必担心字段顺序和名称,并且可以在IDE中使用字段补全,输入Baby要方便得多。然后选择Name属性,并访问其值。
ORM代码比SQL可读性强得多。在代码中,您不必从一种语法转换到另一种语法,因为SQL是一种真正的语言(参见http://www.fossil-scm.org/index.html/doc/tip/www/theory1.wiki)。您甚至可以在大多数项目中忘记SQL本身;只有一些与性能相关的或复杂的查询应该用SQL编写,但大多数时候您应尽量避免使用它,感谢object pascal,快乐的编码,您的软件体系结构将为此感谢您。
另一个好处是命名的一致性。例如,如果您想重命名表,该怎么办?只需更改类定义,您的IDE就会为您进行所有重构,而不会丢失任何隐藏的SQL语句。您想重命名或删除字段吗?更改类定义,Delphi编译器将让您知道在代码中使用此属性的所有位置。要向现有数据库添加字段吗?只需添加属性定义,框架就会在数据库模式中为您创建缺少的字段。
另一个与风险相关的改进是关于强类型检查,在编译时包含在Delphi语言中,并且仅在执行时使用SQL。您将避免数据库访问的大多数运行时异常,您的客户端将为此感谢您。总之,忘记数据库表中的字段类型不匹配或类型分配错误。在这种情况下,强类型对于代码SQA(代码质量保证)非常有用,如果您使用一些脚本语言(如JavaScript、Python或Ruby),那么您应该希望在您的项目中也有这些特性!
值得注意的是,我们的框架允许在Delphi代码中编写触发器和存储过程(或类似于存储过程),并且可以在类定义中创建键索引和执行外键检查。
另一个有趣的特性是该框架提供的增强Grid组件,以及通过使用原生JSON流实现C/S数据流的ajax支持。REST协议可以在大多数应用程序中使用,因为该框架为您提供了一个易于使用的“刷新”和缓存机制。您甚至可以离线工作,将远程数据复制到本地数据库。
对于C/S,如下所示,您不必连接到数据库,只需创建一个TSQLRestClient对象的实例(使用您希望使用的通信层:直接访问、Windows消息、命名管道或HTTP),并将其用作任何普通的Delphi对象。所有的SQL编码或通信和错误处理都将由框架来完成。在客户端或服务器端可以使用相同的代码,TSQLRest根对象可以同时用在C/S两端,它的属性和方法访问数据的能力非常强大。
值得强调的是,您不应该将ORM仅看作是现有DB模式的映射,这是ORM设计中的一个常见误区。
数据库只是对象持久性的一个方面:
例如,不要总是试图创建数据透视表(通过TSQLRecordMany属性),而是考虑使用动态数组、TPersistent, TStrings或TCollection类型的发布属性。
或者考虑使用TRecordReference属性,指向TSQLModel的任何已注册类,而不是为每个潜在表创建一个TSQLRecord属性。
mORMot框架甚至可以在不使用任何SQL数据库的情况下实现对象的持久化,例如通过TSQLRestStorageInMemory。实际上,ORM内核是经过优化的,且没有与SQL强制绑定。
使用ORM,您通常应该比在“普通”关系数据库中定义更少的表,因为您可以使用更高级的TSQLRecord类属性来处理每行数据。
首先,可能会让数据库架构师感到震惊的,就是最好不要创建主/从关系表,只需创建一个“主”对象,并将详情信息存储在动态数组、TPersistent, TStrings或TCollection属性的JSON中。
其次,不应该为软件配置创建各种表,我们应该反省一些DB架构师为每个模块或每个数据表设计一个配置表。在ORM中,您可以设计一个配置类,然后使用唯一的表来存储所有JSON格式或类似于dfm的配置数据。不要犹豫将配置与数据分离,不与数据相关的都放在配置中,请参见mORMotOptions单元如何工作。使用我们的框架,您可以直接将任何TSQLRecord或TPersistent实例序列化为JSON,而无需将此TSQLRecord添加到TSQLModel列表。从框架的1.13修订版,您可以在TSQLRecord类中定义TPersistent发布属性,并将自动序列化为数据库中的文本。
首先,您可能会尝试这样编写代码(这个代码示例发布在我们的论坛上,不算糟糕的代码,只是没有使用框架的ORM):
DrivesModel := CreateDrivesModel();
GlobalClient := TSQLRestClientDB.Create(DrivesModel, CreateDrivesModel(), 'drives.sqlite', TSQLRestServerDB);
TSQLRestClientDB(GlobalClient).Server.DB.Execute(
'CREATE TABLE IF NOT EXISTS drives ' +
'(id INTEGER PRIMARY KEY, drive TEXT NOT NULL UNIQUE COLLATE NOCASE);');
for X := 'A' to 'Z' do
begin
TSQLRestClientDB(GlobalClient).Server.DB.Execute(
'INSERT OR IGNORE INTO drives (drive) VALUES ("' + StringToUTF8(X) + ':")');
end;
请不要这样做。
正确的面向ORM的实现应该是:
DrivesModel := TSQLModel.Create([TDrives], 'root');
GlobalClient := TMyClient.Create(DrivesModel, nil, 'drives.sqlite', TSQLRestServerDB);
GlobalClient.CreateMissingTables(0);
if GlobalClient.TableRowCount(TDrives)=0 then
begin
D := TDrives.Create;
try
for X := 'A' to 'Z' do
begin
D.Drive := X;
GlobalClient.Add(D,true);
end;
finally
D.Free;
end;
end;
上面的几行代码没有编写任何SQL,由ORM处理SQL:
CREATE TABLE IF NOT EXISTS..."SQL语句;然后,为了查询一些数据,您可能会编写如下代码(从同一论坛文章中提取):
procedure TMyClient.FillDrives(aList: TStrings);
var
table: TSQLTableJSON;
X, FieldIndex: Integer;
begin
table := TSQLRestClientDB(GlobalClient).ExecuteList([TSQLDrives], 'SELECT * FROM drives');
if (table <> nil) then
try
FieldIndex := table.FieldIndex('drive');
if (FieldIndex >= 0) then
for X := 1 to table.RowCount do
Items.Add(UTF8ToString(table.GetU(X, FieldIndex)));
finally
table.Free;
end;
end;
得益于TSQLTableJSON类,上面的代码比较容易理解,使用了一个FieldIndex临时变量,利用它在循环中参与运算。
也可以这样编码,使用CreateAndFillPrepare然后在循环中调用FillOne方法:
procedure TMyClient.FillDrives(aList: TStrings);
begin
aList.BeginUpdate;
try
aList.Clear;
with TSQLDrives.CreateAndFillPrepare(GlobalClient,'') do
try
while FillOne do
aList.Add(UTF8ToString(Drive));
finally
Free;
end;
finally
aList.EndUpdate;
end;
end;
我们添加了BeginUpdate / EndUpdate VCL方法,使代码更简洁、更快(如果你使用TListBox)。
注意,在上面的代码中,为了从服务器检索数据,创建了一个隐藏的TSQLTableJSON类,ORM引入的抽象方法使代码效率不是最快的,但是不容易出错(例如,Drive是RawUTF8属性),并且更容易理解。
但ORM并非在所有情况下都是完美的。
例如,如果Drive字段是要检索的列内容,那么只请求这个列是有意义的。CreateAndFillPrepare方法的一个缺点是,默认情况下,它从服务器检索所有列内容,即使您只需要一个,这是ORM的一个常见的潜在问题,由于库不知道您需要哪些数据,它将检索所有对象数据,这在某些情况下是多余的。
您可以指定可选的aCustomFieldsCSV参数,以便仅检索Drive属性内容,并节省一些带宽:
with TSQLDrives.CreateAndFillPrepare(GlobalClient,'','Drive') do
对于这种特殊情况,您有一个更高级的方法,直接将TStrings属性作为参数处理:
procedure TMyClient.FillDrives(aList: TStrings);
begin
GlobalClients.OneFieldValues(TSQLDrives,'drive','',aList);
end;
整个查询在一行中完成,不需要写SELECT语句。
对于特定的ID范围,您可能已经编写了一个特殊的WHERE子句,其中使用了准备好的语句:
GlobalClients.OneFieldValues(TSQLDrives,'drive',
'ID>=? AND ID<=?',[],[aFirstID,aLastID],aList);
当然,阅读mORMot.pas的所有(冗长的)接口部分是值得的,例如TSQLRest类,使您对所有可用的高级方法有自己的想法。在下面的章节中,您将找到关于这个特定单元的所有需要的文档。由于我们的框架在实际应用程序中使用,所以应该已经提供了大多数有用的方法。如果您需要额外的高级特性,请在我们的论坛(http://synopse.info)随时询问,最好配上源代码示例。
不要忘记,由于我们的C/S体系结构,框架能够将对象划分为几个层级,参见下面。这种用法不仅是可能的,而且强烈鼓励。
在客户端应该拥有业务逻辑对象,而服务端同时拥有业务逻辑和数据库对象。
如果您有一个非常特定的数据库模式,那么业务逻辑对象可以是非常高级的,可以封装一些SQL支持读操作,也可以通过一些RESTful服务支持写操作(参见下面)。
高级类型的另一种可能是可以自定义SQLite3 SQL函数或存储过程(见下面),都是可以用Delphi来编写的。
在下面的章节(数据库层、C/S、服务)中深入讨论mORMot材料之前,您可能会发现这个实现听起来可能有些受限。
以下是一些常见的(有根据的)批评:
这些担忧是可以理解的,我们的mORMot框架并不满足所有需求,但是值得理解为什么它要这样实现,以及为什么它在ORMs家族中是非常独特的,几乎所有的ORMs都遵循Hibernate的实现方式。
Attributes首次出现是在Delphi 2010中,值得说的是FPC也有另一种实现语法。旧版本的Delphi(仍然大量使用)语言中没有Attributes,所以最新版本无法与Delphi6兼容(对于我们的单元,我们也希望实现兼容)。
对“滥用index”的批评是正确的,但这是在RTTI中获得类信息的最简单也是唯一的方法,由于这个参数被忽略,很多类都没使用,所以它被重用了(也用于动态数组属性,以支持快速查找)。
对于“stored AS_UNIQUE”属性也存在“误解”,该属性用于标识列的唯一性。
在很多ORM中,使用Attributes来描述表是非常常见的方法。
另一方面,一些编程人员的类定义混合了DB和逻辑,导致DB相关的代码在一定程度上影响了业务级的类定义。
这就是为什么其他类型的ORM提供了使用外部文件将类映射到表的方法(一些ORM两种定义方式都支持),这也导致为什么在那些日子里,即使是代码专家也认为Attributes的过度使用降低了代码的可维护性。
在处理客户端-服务端ORM时,Attributes确实缩减了代码量,就像我们的一样:在客户端,这些Attributes是不需要的(客户端不需要知道任何关于数据库的信息),所有DB相关代码都连接到服务端应用程序,对mORMot来说,这是一种强有力的论据。
出于同样的原因,在mORMot中对列定义(唯一性、索引、必输)实现了两级管理:
当你看到提供的验证和过滤功能,参见过滤和验证部分,你会发现这比单纯使用attributes的常见ORM强大很多:你是怎么验证一个条目是一个电子邮件,怎么做的规则匹配,或者怎么确保它以大写格式存储在DB?
另一个值得关注的问题是关于安全性,如果远程访问数据,对数据库的全局访问显然是不够的,我们的框架使用ORM处理每个表的CRUD访问,位于DB层之上(并且还为服务提供完整的安全属性),不管底层的DB授权是如何定义的(即使没有用户权限管理的DB(比如内存或SQLite3)也可以这样做)。
mORMot的观点(并不是唯一的观点)是让DB尽可能安全高效地持久化数据,但在更高层级来实现业务逻辑。这个框架更倾向于约定优于配置,这样可以节省很多时间(如果您像我一样每天使用WCF,您和您的团队都知道.config综合征)。它将设计工作变得与数据库无关(您甚至完全不需要接触SQL数据库),并使框架代码更易于调试和维护,因为我们不必处理所有的DB引擎特性。简而言之,这就是REST的观点,也是成功的主要原因,CRUD足以支持我们的友好设计。
关于持久化需要从TSQLRecord继承,并非任意PODO(纯旧的Delphi对象)都能持久化的事实,我们的目标实际上非常类似于领域驱动设计的“Layer Supertype”模式,正如Martin Fowler所解释的那样:在整个系统中,某一层的对象存在重复的方法,这很常见,您可以将所有这些方法移动到一个公共层成为基类。
译者注:Layer Supertype,如果一层中的组件具有相同的一组行为,就可以将这些行为提取到一个公共类或组件中,并使层中的所有组件都继承该公共类或组件;换句话说,就是如果多个类有同样的成员,那么建立一个基类,让所有的类继承这个基类。
实际上,由于TSQLRecord / TSQLRest / ORM远程访问的支持,您已经具备C/S的所有CRUD能力,这些类是通用的抽象超类,已经可以在项目中使用。它已经做了大量的优化(例如,具备缓存和其他不错的特性),所以我不认为重新设计一个数据库的CRUD服务是值得的。您可以使用用户/组属性安全地访问ORM类。几乎所有的东西都是根据TSQLRecord类定义通过RTTI由代码创建的。因此,基于它可能比重新定义所有类层次结构更快,也更安全。
公平地说,大多数Java或c#的DDD框架都希望新的实体类从另一个实体类继承,或者向POJO/POCO添加类属性来定义持久化细节,所以在这一点上我们也不是唯一这样做的!
但是,担心不能持久保存所有类(需要从TSQLRecord继承)的确意义重大,特别是在DDD建模上下文中,DDD对象与框架解耦是有益的,否则可能会污染业务逻辑,所有这些担心与期望都可能会打破DDD模式所要求的持久性透明原则。
译者注:Persistence Ignorance principle,持久性透明原则,说白了就是把DDD中的实体、值对象、服务与数据存储功能完全隔离,使他们不掺杂任何与数据存储相关的代码。
这就是为什么我们使用数据存储服务,在框架中添加了持久化任何普通类的能力,但仍然使用底层的ORM,以实现对任何SQL或NoSQL数据库引擎的实际持久性。可以从任何Delphi持久化类关联TSQLRecord类,然后mORMot在两个类实例之间自动维护一个映射,这样数据访问就可以定义为清晰的CQRS数据存储服务。
例如,TUser类可以这样使用数据存储服务实现持久化:
type
IDomUserCommand = interface(IDomUserQuery)
['{D345854F-7337-4006-B324-5D635FBED312}']
function Add(const aAggregate: TUser): TCQRSResult;
function Update(const aUpdatedAggregate: TUser): TCQRSResult;
function Delete: TCQRSResult;
function Commit: TCQRSResult;
end;
在这里,写操作是在IDomUserCommand服务中定义的,它与用于读取操作的IDomUserQuery分离(但继承),有关此特性的详细信息,请参阅下面。
显然,mORMot提供了几种类型的表定义:
为什么同时支持多个数据库后端?
现有的大多数软件体系结构都依赖于每个领域一个专用数据库,因为管理单个服务更方便。
但是,在某些情况下,同时拥有多个数据库是很有意义的。
实际上,当您的数据大量增长时,您可能需要将旧数据存档到远程专用数据库中,如使用廉价存储(支持RAID的大容量硬盘),由于很少检索这些数据,因此访问慢不是问题。而将最新数据放在本地高速引擎(在SSD上运行)上访问。
另一种模式是用于数据分析的专用整合数据库。实际上,SQL规范化对于大多数常见的关系工作都很好,但是有时候需要“去规范化”,如为了统计或业务分析的目的。在这种情况下,专用的整合数据库,包含已经准备好的数据,需在已就绪的去规范化布局中建立索引。
最后也最重要的是,一些事件溯源架构还期望同时有几个DB后端:
译者注:事件溯源架构(Event Sourcing architectures),数据用事件表示,我们不再存储数据本身,而是存储与该数据相关的所有事件,包括数据被创建的事件在内,通过保存数据的“完整的历史”来达到任意时刻都能还原数据的目标。
ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。一个支持事务(Transaction)的数据库,必须要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。
可以直接访问远程ORM对象(如整合数据库),主要以只读的方式,专门用于报表,mORMot的一个有潜力的CQRS实现模式就是整合数据。由于框架的安全性,远程访问将是安全的,您的客户端将无法更改整合数据内容!
可以很容易地猜到,这种设计模型与仅为类持久性而构建的基础性ORM相去甚远,mORMot的ORM/ODM提供了上述所有这些发展潜力。
因此,根据您的需要,我们可能会总结一些ORM的潜在用途:
因此,mORMot不仅仅只是一个ORM,也不仅仅是一个通常意义上的ORM。