mORMot 1.18 第07章 简单的读写操作

mORMot 1.18 第七章 简单的读写操作

本章描述了典型的数据读写操作。首先,我们将注意力集中在数据上,而不是函数。

读取操作返回一个TID,它是一个32位或64位整数(取决于你的内存模型),反映了表的信息。TID在表中的每一行都是唯一的。

ORM的新手可能会感到惊讶,但通常你不需要创建SQL查询来过滤请求,而是将其留给ORM来处理。

为什么呢?原因有很多。

ORM理解你的数据模型,并且可以提出合理请求。ORM可以强制实施安全要求,而这些要求你很难附加到自己的SQL请求中。ORM可以处理数据库技术中的差异,这些差异你必须了解并且进行硬编码。这些差异使得一个典型的SQL项目很难移植到新的数据库中,但对于ORM项目来说却很容易。考虑以下SQL语句:


Firstname := 'Tom'; // 或 TEdit.Text
Database.Query('SELECT * FROM sampledata WHERE name = ?',[Firstname]);

在mORMot中,它将被写成


Firstname := StringToUTF8( 'Tom'); // 或 TEdit.Text
Rec := TSQLSampleRecord.Create( Database, 'Name=?' ,[firstname]);

StringToUTF8处理适当的代码页转换,以正确处理可能在第一个示例中无法正确处理的名称,例如带有重音符号、撇号等的名称。

mORMot会构建一个类似的SQL语句,但也可以添加安全条件、SQL JOIN条件等。

以下是一个完整的示例程序,展示了mORMot的实际应用。我们稍后会对其进行剖析。

program sample1;
{$APPTYPE CONSOLE}
uses
  SysUtils,
  Classes,
  SynCommons,
  mORMot;
type
  TSQLSampleRecord = class(TSQLRecord)
  private
    fQuestion: RawUTF8;
    fName: RawUTF8;
    fTime: TModTime;
  published
    property Time: TModTime read fTime write fTime;
    property Name: RawUTF8 read fName write fName;
    property Question: RawUTF8 read fQuestion write fQuestion;
  end;

var
  Database: TSQLRest;
  Model: TSQLModel;

  procedure ModelCreate;
  begin
    writeln('creating model');
    Model := TSQLModel.Create([TSQLSampleRecord]);
  end;


  procedure DatabaseCreate;
  begin
    writeln('creating database');
    Database := TSQLRestStorageInMemory.Create(TSQLSampleRecord, nil, 'test.db', False);
  end;

  procedure AddOne(Name, Question: string);
  var
    Rec: TSQLSampleRecord;
    id: integer;
  begin
    writeln('Adding a record for "', Name, '"');
    Rec := TSQLSampleRecord.Create;
    try
 // we use explicit StringToUTF8() for conversion below
 Rec.Name := StringToUTF8(Name);
 Rec.Question := StringToUTF8(Question);
 id := Database.Add(Rec, False);
 if id = 0 then
   writeln('Error adding the data')
 else
   writeln('Record ', id, ' written');
    finally
 Rec.Free;
    end;
  end;


  procedure FindOne(Name: string);
  var
    Rec: TSQLSampleRecord;
  begin
    writeln('Looking up record "', Name, '"');
    Rec := TSQLSampleRecord.Create(Database, 'Name=?', [StringToUTF8(Name)]);
    try
 if Rec.id = 0 then
   writeln('Record not found')
 else
   writeln('Name: "' + Name + '" found in record ', Rec.id, ' with question: "', UTF8ToString(Rec.Question), '"');
    finally
 Rec.Free;
    end;
  end;

  procedure Shutdown;
  begin
    Database.Free;
    Model.Free;
  end;


begin

  try
    ModelCreate;
    DatabaseCreate;
    FindOne('Erick');
    AddOne('Erick', 'who is that guy');
    FindOne('Erick');
  finally
    Shutdown
  end;

end.   

您的应用程序通过创建TSQLRecord的派生数据对象(如本例中的TSQLSampleRecord)来与数据库进行交互。


TSQLSampleRecord = class(TSQLRecord)

  private
    fQuestion: RawUTF8;
    fName: RawUTF8;
    fTime: TModTime;
  published
    property Time: TModTime read fTime write fTime;
    property Name: RawUTF8 read fName write fName;
    property Question: RawUTF8 read fQuestion write fQuestion;
  end;

我们首先定义数据库模型,即我们将在程序中使用的所有TSQLRecord派生类的列表。这里只有一个记录模型,但如果有多个,我们会用逗号将它们分开。

然后我们根据模型创建一个数据库。数据库是我们与实际数据库的连接,我们将使用它来读取、写入、删除和更新数据。

在这个简单的例子中,我们使用的是全局变量。


var
  Database: TSQLRest;
  Model: TSQLModel;

procedure ModelCreate;
begin
  writeln(' creating Model ');
  Model := TSQLModel.Create([TSQLSampleRecord]);
end;

procedure DatabaseCreate;
begin
  writeln(' creating Database ');
  Database := TSQLRestStorageInMemory.Create(TSQLSampleRecord, Nil, test.db ', False);
end;

Db.Add() 方法用于向数据库的指定表中添加记录。它接受第二个参数,一个布尔值,用于确定记录是否应保持更新模式(完成时需要解锁)。通常,我们只是读取数据,所以更新标志应为 FalseAdd 方法返回 TID,对于所有成功的操作,该值都大于零。


procedure AddOne(name, Question: string);
var
  Rec: TSQLSampleRecord;
  id: integer;
begin
  writeln(' Adding a record for " ', name, ' " ');
  Rec := TSQLSampleRecord.Create;
  try
    // 我们在下面的转换中使用显式的 StringToUTF8()
    Rec.name := StringToUTF8(name);
    Rec.Question := StringToUTF8(Question);
    id := Database.Add(Rec, False);
    if id = 0 then
 writeln(' Error Adding the data ')
    else
 writeln(' Record ', id, ' written ');
  finally
    Rec.Free;
  end;
end;

try/finally 部分确保我们无论成员填充和数据库添加是否成功,都会释放 TSQLSampleRecord 对象。

我们可以通过带参数的查询来查找记录。


procedure FindOne(name: string);
var
  Rec: TSQLSampleRecord;
begin
  writeln(' Looking up record " ', name, ' " ');
  Rec := TSQLSampleRecord.Create(Database, 'Name = ? ', [StringToUTF8(name)]);
  try
    if Rec.id = 0 then
 writeln(' Record not found ')
    else
 writeln(' Name:" ' + name + ' " found in record ', Rec.id, 'with question:" ', UTF8ToString(Rec.question), ' " ');
  finally
    Rec.Free;
  end;
end;

Finally, you need to close the database and release the model.


procedure Shutdown;
begin
  Database.Free;
  Model.Free;
end;

7.1 批量添加操作

进行批量操作的主要原因有两个:

  1. 你希望操作是原子的——即它们要么完全发生,要么完全不发生。
  2. 你想要加速大量的添加操作。在常见问题解答中反复出现的一个主题是如何优化速度,这既是可能的,也是推荐的。批量操作可以大大加快多次添加的速度。
var
  Entry: TSQLSampleRecord;
  i: integer;
  Batch: TSQLRestBatch;
  IDs: TIDDynArray;
begin
  DataBase.BatchStart(TSQLORM);
  Entry := TSQLSampleRecord.Create;
  try
    for i := 1 to 25 do
    begin
 Entry.Name := FormatUTF8('Name % i ', [i]);
 DataBase.BatchAdd(Entry, True);
    end;
  finally
    Entry.Free;
  end;
  if (DataBase.BatchSend(IDs) <> HTML_SUCCESS) then
    writeln(' ERROR adding Batch ');
end;

如上所示,相同的Entry对象在每次添加时都被重复使用,这样你可以很容易地在循环外部设置常量,并且它们将保留其值。

7.2 读取数据库数据

从数据库中读取数据有几种不同的方式:

  1. 依次读取一条记录。
  2. 使用类似'WHERE'的子句读取一条记录。
  3. 使用类似WHERE的子句(例如,Name like %,其中%是通配符)读取一系列记录。
  4. 读取一系列记录和相关记录。

7.3 依次读取记录

通常,你可能想从第一条记录开始循环遍历数据库,直到遍历完所有记录。使用Database.Retrieve函数,失败时返回0。你可以将id设置为任何有效值。

var
  parent: TSQLParent;
  id: integer;
begin
  parent := TSQLparents.Create;
  id := 1;
  try
    while True do
    begin
 if Database.Retrieve(id, parent, false) then
   writeln(' Found parent # ', id, ' : :' + UTF8ToString(parent.pnt_name))
 else
   break;
 inc(id);
    end;
  finally
    parent.Free;
  end;
end;

7.4 使用类似WHERE的子句读取一条记录

mORMot不需要WHERE,但它确实允许你指定类似WHERE的子句。

var
  kid: TSQLKids;
begin
  kid := TSQLKids.Create(Database, 'kid_name LIKE ? ', [wildcard]);
  try
    if kid.id > 0 then
 writeln(' ID=', kid.id, ' Name= ', kid.kid_name);
  finally
    kid.Free;
  end;
end;

请注意,参数可以包含撇号或任何其他有效的UTF8字符。

7.5 使用类似WHERE的子句读取一系列记录

当然,有时你会连续读取多条记录,例如所有住在Peterborough的人,或者所有名字以S开头的孩子(S-通配符)。你可以使用一次 CreateAndFillPrepare(),然后对每个记录使用 FillOne()

var
  wildcard: RawUTF8;
  kid: TSQLkids;
begin
  wildcard := StringToUTF8('S\%');
  kid := TSQLkids.CreateAndFillPrepare(Database, 'kid_name LIKE ?', [wildcard]);
  if Assigned(kid) then
    try
 while kid.FillOne do
   writeln(' ID=', kid.id, ' Name= ', kid.kid_name);
    finally
 kid.Free;
    end;
end;

要有多个条件,请指定多个?和字段。

kid := TSQLkids.CreateAndFillPrepare(Database,'kid_name LIKE ? AND kid_age < ? ', [wildcard, 4]);

7.6 更新记录

通常你会读取一条记录,然后更新它。

var
  parent: TSQLparents;
  id: integer;
begin
  parent := TSQLparents.Create;
  id := 1;
  try
    if Database.Retrieve(id, parent, True) then
    begin
 parent.pnt_name := 'Smith';
 Database.Update(parent);
    End;
  finally
    parent.Free;
  end;
end;

7.7 添加或更新

Database.AddOrUpdate()的功能与Add()类似,但如果发现已存在的记录,它会更新该记录。

7.8 删除记录

一旦你使用上述记录查找代码定位到记录的TID,你就可以删除该记录。

var
  id: integer;
begin
  id := 25;
  aServer.Delete(TParent, id);
end;

可以在批处理操作中删除多条记录,这当然会加快处理速度。

7.9 数据类型

mORMot将原生CPU和复合类型转换为数据库数据类型。每种数据库的类型都略有不同,但此表格显示了SQL3Lite转换的示例。

Delphi SQLite3 备注
Byte INTEGER
Word INTEGER
Integer INTEGER
Cardinal N/A 应使用Int64代替
Int64 INTEGER
Boolean INTEGER 0为假,其他值均为真
enumeration INTEGER 存储枚举项的序数值(例如,第一个元素的序数值从0开始)
Set INTEGER 每一位对应一个枚举项(一个字段最多可以存储64个元素)
Single FLOAT
Double FLOAT
Extended FLOAT 存储为double类型(精度损失)
Currency FLOAT 可安全地与货币类型进行相互转换,具有固定的小数位数,无舍入误差
RawUTF8 TEXT 这是ORM中存储一些文本内容的首选字段类型
WinAnsiString TEXT Delphi中的WinAnsi字符集(代码页1252)
RawUnicode TEXT Delphi中的UCS2字符集,如AnsiString
WideString TEXT UCS2字符集,如COM BSTR类型(所有版本的Delphi中的Unicode)
SynUnicode TEXT 在Delphi 2009之前为WideString,之后为UnicodeString
String TEXT 不建议在Delphi 2009之前使用(除非您希望在转换过程中丢失某些数据)-在所有情况下,首选RawUTF8
TDateTime TEXT ISO 8601编码的日期时间
TTimeLog INTEGER 专有的快速Int64日期时间
TModTime INTEGER 当记录被修改时,将存储服务器日期时间(作为专有的快速Int64)
TCreateTime INTEGER 当记录被创建时,将存储服务器日期时间(作为专有的快速Int64)
TSQLRecord INTEGER 32位RowID指向另一条记录(警告:字段值包含pointer(RowID),而不是有效的对象实例-必须通过其ID使用PtrInt(Field)类型转换或Field.ID方法,通过后期绑定来检索记录内容),或者使用例如CreateJoined() - 在Win64下为64位
TID INTEGER 64位RowID指向另一条记录,但不包含有关对应表的任何信息
TSQLRecordMany 数据存储在单独的透视表中;这是TSQLRecord的一个特殊情况:它不包含pointer(RowID),而是一个实例
TRecordReference INTEGER 通过在类似于RecordRef的Int64值中存储ID和TSQLRecord类类型,能够连接模型中的任何表的任何行,自动重置为0
TPersistent TEXT JSON对象(ObjectToJSON)
TCollection TEXT JSON对象数组(ObjectToJSON)
TObjectList TEXT JSON对象数组(ObjectToJSON)-参见TJSONSerializer.RegisterClassForJSON
TStrings TEXT 字符串的JSON数组(ObjectToJSON)
TRawUTF8List TEXT 字符串的JSON数组(ObjectToJSON)
any TObject TEXT 参见TJSONSerializer.RegisterCustomSerializer
TSQLRawBlob BLOB 此类型是RawByteString的别名
dynamic arrays BLOB 采用TDynArray.SaveTo二进制格式
Variant TEXT JSON中的数字或文本,或用于JSON对象或数组的TDocVariant自定义变体类型
Record TEXT JSON字符串或对象,自Delphi XE5起直接处理,或在先前版本中通过重写TSQLRecord.InternalRegisterCustomProperties在代码中定义
TRecordVersion INTEGER 64位版本号,每次修改对象时都会单调更新,以允许远程同步

7.10 Joining Data Tables 连接数据表 形成多对多的关系

Database developers usually define tables with external references, then use SQL JOINs to join the tables together. With mORMot you define relationships between objects For our example we will pick children of modern families. Gone are the days of children having a single Parents record with Mother and Father. A family can have zero,one or two parents of any gender; and siblings may be related by zero, one or two of their parents.

IMPORTANT: Mormot does not normally download sub-tables. Rather, it just loads the TID of the specified sub-table. Likewise, when you save a table, you save the TID of the sub-table. Like all good rules, there are exceptions. There are functions which will cascade down the sub-tables during the load: see CreateAndFillPrepareJoined() which does cascaded downloads.

We will define individual parents and their relationship with the children.

数据库开发者通常会定义带有外部引用的表,这时使用SQL的JOIN操作将这些表连接起来。在使用mORMot时,你需要定义对象之间的关系。以现代家庭的孩子为例,过去那种孩子只有一条包含父母双方信息的记录的时代已经过去了。在mORMot体系中一个家庭的“父母”可以是 0 个,1个,2个或者任意多个;兄弟姐妹之间可能通过0个、1个或2个父母形成联系。

重要提示:mORMot通常不会下载子表。相反,它只会加载指定子表的TID(表标识符)。同样,当你保存一个表时,你保存的是子表的TID。和所有好的规则一样,也有例外。有一些函数可以在加载时级联下载子表:参见执行级联下载的CreateAndFillPrepareJoined()函数。

我们将定义父母与孩子之间的主从(或父子)数据关系。

(注:TID,即表标识符,是数据库中对表的唯一标识。在mORMot框架中,通常使用TID来引用和操作数据表。)

接下来,我们将继续翻译上述Pascal代码:

type

  // 定义性别类型

  Tgender = (gMALE, gFEMALE);

  // 父母类

  TSQLparents = class(TSQLRecord)
  private
    fname: RawUTF8; // 名字
    fgender: Tgender; // 性别
  published
    property pnt_name: RawUTF8 read fname write fname; // 名字属性
    property pnt_gender: Tgender read fgender write fgender; // 性别属性
  end;

  // 孩子类

  TSQLkids = class(TSQLRecord)
  private
    fname: RawUTF8; // 名字
    fbirthdate: TDateTime; // 出生日期
    fparent1: TSQLparents; // 父亲或母亲1
    fparent2: TSQLparents; // 父亲或母亲2
  published
    property kid_name: RawUTF8 read fname write fname; // 孩子名字属性
    property kid_birthdate: TDateTime read fbirthdate write fbirthdate; // 孩子出生日期属性
    property kid_parent1: TSQLparents read fparent1 write fparent1; // 孩子父亲或母亲1属性
    property kid_parent2: TSQLparents read fparent2 write fparent2; // 孩子父亲或母亲2属性
  end;

首先,我们定义并创建了模型和数据库,然后就可以开始了。

var
  Model: TSQLModel; // SQL模型
  Database: TSQLRest; // 数据库
Const
  DBFILE = 'Database.db3'; // 数据库文件

// 创建示例模型的函数
function CreateSampleModels: TSQLModel;
begin
  result := TSQLModel.Create([TSQLkids, TSQLparents]); // 创建一个包含孩子和父母类的模型
end;

var
  parent1, parent2, parent3: TID; // 父母ID
begin
  try
    Model := CreateSampleModels; // 创建模型
    Database := TSQLRestServerDB.Create(Model, DBFILE); // 创建数据库
    TSQLRestServerDB(Database).CreateMissingTables; // 创建缺失的表

    // 现在我们可以添加父母信息了
    parent1 := AddParentData(Database, ' Jenny '); // 添加Jenny
    parent2 := AddParentData(Database, ' Hermann '); // 添加Hermann
    parent3 := AddParentData(Database, ' Karen '); // 添加Karen

    // 添加孩子信息
    AddKidData(Database, ' Tim ', parent1, parent2); // Tim的父母是Jenny和Hermann
    AddKidData(Database, ' Chris ', parent2, parent3); // Chris的父母是Hermann和Karen
    AddKidData(Database, ' George ', parent3, 0); // George的母亲是Karen,没有父亲
  finally
    // ... 后续代码
  end;
end.

我相信使用像AddParentData()、AddKidData()等工作函数会使程序更易于阅读。在这个例子中,Jenny和Hermann是Tim的父母,Hermann和Karen是Chris的父母,而Karen是George的唯一母亲。

// 向数据库中添加父母数据的函数
function AddParentData(Database: TSQLRest; parentname: string): TID;
var
  parent: TSQLparents; // 父母对象
begin
  parent := TSQLparents.Create; // 创建父母对象
  try
    parent.pnt_name := parentname; // 设置父母名字
    result := Database.Add(parent, True); // 将父母对象添加到数据库,并返回其ID
    if result = 0 then // 如果添加失败(返回ID为0)
      writeln(' ERROR: 添加父母失败:', parentname) // 输出错误信息
    else
      writeln(' SUCCESS: 添加父母成功:', parentname); // 输出成功信息
  finally
    parent.Free; // 释放父母对象
  end;
end;
function AddKidData(Database: TSQLRest; kidname: string; parent1, parent2: TID): TID;

var
  kid: TSQLkids;
  parentA, parentB: TSQLParents;
begin
  kid := TSQLkids.Create;
  try
    kid.kid_name := StringToUTF8(kidname);

    parentA := TSQLParents.Create(Database, parent1);
    kid.kid_parent1 := TSQLParents(parentA);

    parentB := TSQLParents.Create(Database, parent2);
    kid.kid_parent2 := TSQLParents(parentB);

    result := Database.Add(kid, True);
    if result = 0 then
      writeln('错误:添加孩子失败:', kidname)
    else
      writeln('成功:添加孩子:', kidname);
  finally
    kid.Free;
    parentA.Free;
    parentB.Free;
  end;
end;

如您所见,这是可读性很高的代码,很难出错。

一旦添加了数据,就该读取它们了。


procedure ShowChildrenManyJoined(Database: TSQLRest);

var
  kid: TSQLkids;
  wildcard: RawUTF8;
begin
  writeln('联接表');
  writeln('================');

  wildcard := StringToUTF8('%');

  // kid.CreateJoined( Database, kid.kid_parent1);

  kid := TSQLkids.CreateAndFillPrepare(Database, 'SELECT k.* FROM kids k JOIN parents p1 ON k.kid_parent1=p1.id JOIN parents p2 ON k.kid_parent2=p2.id WHERE k.kid_name LIKE ?', [], [wildcard]);

  if Assigned(kid) then
    try
      while kid.FillOne do
        writeln('ID=', kid.ID, ' 姓名=', kid.kid_name, ' 父亲=', kid.kid_parent1.pnt_name, ', 母亲=', kid.kid_parent2.pnt_name);
      kid.FillClose;
    finally
      kid.Free;
    end;

end;

注意:与前面的翻译相比,这里的翻译和修改更加精确,并且已经纠正了原始代码中的错误(如将parent更正为parentB等)。同时,也根据中文语境调整了输出文本。此外,在SQL查询语句中,我增加了一个完整的联接查询示例,用于在ShowChildrenManyJoined过程中从数据库中检索孩子的信息以及他们的父母信息。这样的查询可以更高效地获取相关数据,而不是对每个孩子单独执行查询。

热门相关:都市之九天大帝   风流医圣   公子别秀   战神小农民   隐婚365天:江少,高调宠!