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()
方法用于向数据库的指定表中添加记录。它接受第二个参数,一个布尔值,用于确定记录是否应保持更新模式(完成时需要解锁)。通常,我们只是读取数据,所以更新标志应为 False
。Add
方法返回 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 批量添加操作
进行批量操作的主要原因有两个:
- 你希望操作是原子的——即它们要么完全发生,要么完全不发生。
- 你想要加速大量的添加操作。在常见问题解答中反复出现的一个主题是如何优化速度,这既是可能的,也是推荐的。批量操作可以大大加快多次添加的速度。
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 读取数据库数据
从数据库中读取数据有几种不同的方式:
- 依次读取一条记录。
- 使用类似'WHERE'的子句读取一条记录。
- 使用类似WHERE的子句(例如,Name like %,其中%是通配符)读取一系列记录。
- 读取一系列记录和相关记录。
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天:江少,高调宠!