6. 日常的ORM

  在比较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两端,它的属性和方法访问数据的能力非常强大。

6.1. ORM不是数据库

  值得强调的是,您不应该将ORM仅看作是现有DB模式的映射,这是ORM设计中的一个常见误区。

  数据库只是对象持久性的一个方面:

  • 不要只看到简单类型的表(文本/数字…),而要看到高级类型的对象;
  • 不要只看到主/从关系,而要看到逻辑单元;
  • 不要只看到“SQL”,更要看到类;
  • 不要只想“我要如何存储它?”,而是考虑“我需要哪些数据?”

  例如,不要总是试图创建数据透视表(通过TSQLRecordMany属性),而是考虑使用动态数组、TPersistent, TStrings或TCollection类型的发布属性。

  或者考虑使用TRecordReference属性,指向TSQLModel的任何已注册类,而不是为每个潜在表创建一个TSQLRecord属性。

  mORMot框架甚至可以在不使用任何SQL数据库的情况下实现对象的持久化,例如通过TSQLRestStorageInMemory。实际上,ORM内核是经过优化的,且没有与SQL强制绑定。

6.1.1. Objects不是表

  使用ORM,您通常应该比在“普通”关系数据库中定义更少的表,因为您可以使用更高级的TSQLRecord类属性来处理每行数据。

  首先,可能会让数据库架构师感到震惊的,就是最好不要创建主/从关系表,只需创建一个“主”对象,并将详情信息存储在动态数组、TPersistent, TStrings或TCollection属性的JSON中。

  其次,不应该为软件配置创建各种表,我们应该反省一些DB架构师为每个模块或每个数据表设计一个配置表。在ORM中,您可以设计一个配置类,然后使用唯一的表来存储所有JSON格式或类似于dfm的配置数据。不要犹豫将配置与数据分离,不与数据相关的都放在配置中,请参见mORMotOptions单元如何工作。使用我们的框架,您可以直接将任何TSQLRecord或TPersistent实例序列化为JSON,而无需将此TSQLRecord添加到TSQLModel列表。从框架的1.13修订版,您可以在TSQLRecord类中定义TPersistent发布属性,并将自动序列化为数据库中的文本。

6.1.2. Methods不是SQL

  首先,您可能会尝试这样编写代码(这个代码示例发布在我们的论坛上,不算糟糕的代码,只是没有使用框架的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:

  • 通过CreateMissingTables方法创建所有缺失的表,而不是使用"CREATE TABLE IF NOT EXISTS..."SQL语句;
  • 通过TableRowCount方法检查是否存在数据行,而不是用“SELECT COUNT(*) FROM DRIVES”;
  • 使用Delphi的TDrives实例和Add方法添加数据,而不是用“INSERT OR IGNORE INTO DRIVES...”。

  然后,为了查询一些数据,您可能会编写如下代码(从同一论坛文章中提取):

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)随时询问,最好配上源代码示例。

6.1.3. 考虑多层架构

  不要忘记,由于我们的C/S体系结构,框架能够将对象划分为几个层级,参见下面。这种用法不仅是可能的,而且强烈鼓励。

  在客户端应该拥有业务逻辑对象,而服务端同时拥有业务逻辑和数据库对象。

  如果您有一个非常特定的数据库模式,那么业务逻辑对象可以是非常高级的,可以封装一些SQL支持读操作,也可以通过一些RESTful服务支持写操作(参见下面)。

  高级类型的另一种可能是可以自定义SQLite3 SQL函数或存储过程(见下面),都是可以用Delphi来编写的。

6.2. 强大的ORM

  在下面的章节(数据库层、C/S、服务)中深入讨论mORMot材料之前,您可能会发现这个实现听起来可能有些受限。

  以下是一些常见的(有根据的)批评:

  • 关于ORM的方法,我不太喜欢的一点是对现有的Delphi结构的误用,比如定义字符串属性的最大长度的“index n”属性,其他orm使用类属性解决这个问题;
  • “您必须从TSQLRecord继承,并且不能持久化普通类”;
  • “无法轻松地映射现有的复杂数据库”。

  这些担忧是可以理解的,我们的mORMot框架并不满足所有需求,但是值得理解为什么它要这样实现,以及为什么它在ORMs家族中是非常独特的,几乎所有的ORMs都遵循Hibernate的实现方式。

6.2.1. 类定义的误区

  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中对列定义(唯一性、索引、必输)实现了两级管理:

  • 在ORM级与DB相关的(比如索引,它是DB特性,不是业务特性);
  • 在模型级与业务相关的(如唯一性、验证和过滤)。

  当你看到提供的验证和过滤功能,参见过滤和验证部分,你会发现这比单纯使用attributes的常见ORM强大很多:你是怎么验证一个条目是一个电子邮件,怎么做的规则匹配,或者怎么确保它以大写格式存储在DB?

  另一个值得关注的问题是关于安全性,如果远程访问数据,对数据库的全局访问显然是不够的,我们的框架使用ORM处理每个表的CRUD访问,位于DB层之上(并且还为服务提供完整的安全属性),不管底层的DB授权是如何定义的(即使没有用户权限管理的DB(比如内存或SQLite3)也可以这样做)。

  mORMot的观点(并不是唯一的观点)是让DB尽可能安全高效地持久化数据,但在更高层级来实现业务逻辑。这个框架更倾向于约定优于配置,这样可以节省很多时间(如果您像我一样每天使用WCF,您和您的团队都知道.config综合征)。它将设计工作变得与数据库无关(您甚至完全不需要接触SQL数据库),并使框架代码更易于调试和维护,因为我们不必处理所有的DB引擎特性。简而言之,这就是REST的观点,也是成功的主要原因,CRUD足以支持我们的友好设计。

6.2.2. TSQLRecord持久化类不等于全部类

  关于持久化需要从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分离(但继承),有关此特性的详细信息,请参阅下面。

6.2.3. 多ORM支持

  显然,mORMot提供了几种类型的表定义:

  • 基于TSQLRecord / TSQLRecordVirtual的ORM原生类:数据存储不是通过TSQLRestStorageInMemory高速内存表,就是SQLite3表(内存、文件或虚拟),此时,我们对字符串不使用index属性(这些引擎都不支持定义字段长度)。
  • 基于TSQLRecord的ORM管理的外部类:通过调用VirtualTableExternalRegister() / VirtualTableExternalMap()函数注册后,ORM通过SQL创建和管理外部DB表,参见下面。这些类允许通过OleDB、ODBC / ZDBC驱动程序或DB.pas单元,在支持的数据库引擎中创建表,目前支持SQLite3、Oracle、Jet/MSAccess、MS SQL、Firebird、DB2、PostgreSQL、MySQL、Informix和NexusDB。对于ORM管理的外部TSQLRecord类型定义,ORM希望定义文本字段长度(例如RawUTF8或字符串类型的发布属性),对于TQLRead派生类来说,这是唯一需要为这样一个基本实现定义的参数,然后,如果需要的话,可以指定其它的字段/列映射。
  • 基于TSQLRecord的ORM管理的外部ODM类:调用StaticMongoDBRegister()或StaticMongoDBRegisterAll()函数注册后,通过NoSQL和对象文档映射(ODM)创建和管理外部MongoDB集合。在这种情况下,不需要设置文本字段长度的index属性。
  • 基于TSQLRecordMappedAutoID / TSQLRecordMappedForcedID的外部映射类:DB表不是由ORM创建的,而是已经存在于DB中,有时具有非常复杂的布局。这个特性还没有实现,但是已经在路线图中开发了。对于这类型的类,我们可能不使用属性,甚至不使用外部文件,但是我们将依靠代码来定义,使用流畅定义,或者专用类(或接口)。
  • 对于任何类型的Delphi类,可使用CQRS数据存储服务将它们映射成内部TSQLRecord类。

  为什么同时支持多个数据库后端?

  现有的大多数软件体系结构都依赖于每个领域一个专用数据库,因为管理单个服务更方便。
  但是,在某些情况下,同时拥有多个数据库是很有意义的。

  实际上,当您的数据大量增长时,您可能需要将旧数据存档到远程专用数据库中,如使用廉价存储(支持RAID的大容量硬盘),由于很少检索这些数据,因此访问慢不是问题。而将最新数据放在本地高速引擎(在SSD上运行)上访问。

  另一种模式是用于数据分析的专用整合数据库。实际上,SQL规范化对于大多数常见的关系工作都很好,但是有时候需要“去规范化”,如为了统计或业务分析的目的。在这种情况下,专用的整合数据库,包含已经准备好的数据,需在已就绪的去规范化布局中建立索引。

  最后也最重要的是,一些事件溯源架构还期望同时有几个DB后端:

  译者注:事件溯源架构(Event Sourcing architectures),数据用事件表示,我们不再存储数据本身,而是存储与该数据相关的所有事件,包括数据被创建的事件在内,通过保存数据的“完整的历史”来达到任意时刻都能还原数据的目标。

  • 它将把状态存储在一个数据库中(例如,高性能内存中),以便即时处理常见请求;
  • 将修改事件存储在另一个ACID数据库中(如SQLite3、Oracle、Jet/MSAccess、MS SQL、Firebird、DB2、PostgreSQL、MySQL、Informix或NexusDB),还有MongoDB这样的高速NoSQL引擎。

  ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。一个支持事务(Transaction)的数据库,必须要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。

  可以直接访问远程ORM对象(如整合数据库),主要以只读的方式,专门用于报表,mORMot的一个有潜力的CQRS实现模式就是整合数据。由于框架的安全性,远程访问将是安全的,您的客户端将无法更改整合数据内容!

  可以很容易地猜到,这种设计模型与仅为类持久性而构建的基础性ORM相去甚远,mORMot的ORM/ODM提供了上述所有这些发展潜力。

6.2.4. 最好的ORM就是你需要的

  因此,根据您的需要,我们可能会总结一些ORM的潜在用途:

  • 如果你想保存一些数据对象(没有复杂的业务逻辑),轻量化的ORM框架,支持SQLite3,Oracle,Jet/MSAccess,MS SQL,Firebird,DB2,PostgreSQL,MySQL,Informix,NexusDB数据库,还有非SQL引擎,使用TSQLRestStorageInMemory类还可将内容保存在小文件中;
  • 如果您对ORM的理解只是将现有与业务相关的代码、对象持久化,那么mORMot可以帮助您,通过数据存储库服务可自动生成TSQLRecord类;
  • 如果您想要一个非常快速的底层C/S层,mORMot是首选,一些用户正在使用内建的JSON序列化和HTTP服务特性来创建他们的应用程序,使用的是RESTful/SOA架构;
  • 如果您希望映射现有的复杂RDBMS,mORMot将允许将现有的SQL语句作为服务发布,例如使用基于接口的服务,通过良好优化的SynDB.pas数据访问,参见"遗留代码和现有项目”章节;
  • 如果您需要(也许现在不需要,但将来可能需要)创建某种可扩展的领域驱动设计架构,那么mORMot拥有您所需要的所有特性;

  因此,mORMot不仅仅只是一个ORM,也不仅仅是一个通常意义上的ORM。