在描述这个框架的客户端-服务端设计之前,我们可能需要详细说明它所基于的一些标准:
如前所述,框架内部使用JSON格式。根据定义,JavaScript对象表示法(JSON)是一种标准的、开放的轻量级计算机数据交换格式。
JSON的基本类型可从http://en.wikipedia.org/wiki/JSON检索到。
| 类型 | 描述 |
|---|---|
| Number | JavaScript中的双精度浮点格式,一般取决于实现,没有特定的整数类型 |
| String | 加双引号的Unicode,支持反斜杠转义 |
| Boolean | true或false |
| Array | 用逗号分隔并括在方括号内的有序序列值,这些值不需要是相同类型的 |
| Object | 成对的key:value无序集合,键和值之间用':'字符分隔,键值对之间用逗号分隔,并用括号括起来;键必须是字符串,并且不应该重名 |
| null | 空的/未定义的值 |
可以在“结构字符”(即括号“{ }[ ]”、冒号":"和逗号",”)周围自由添加无意义的空格。
下面的示例显示了描述人员的对象的JSON表示。
该对象定义了用于名称和姓氏的字符串字段、用于年龄的数字字段、表示个人地址的对象和电话号码对象数组。
{
"firstName": "John",
"lastName": "Smith",
"age": 25,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": 10021
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "fax",
"number": "646 555-4567"
};
]
}
使用此方案而不是其他类似XML的专有格式,会产生以下几种特性:
REST JSON序列化在我们的ORM中用于处理任何发布的TSQLRecord属性,并在框架的基于接口的SOA架构中用于内容传输。
框架中实现了整个http://json.org标准,但有一些例外/扩展:
#0字符表示输入结束,与几乎所有JSON库一样,因此,如果您的文本输入包含#0字符,请将其作为二进制处理(注意,其他控制字符按要求转义);在实践中,JSON已经被证明是易于使用和稳定的。二进制格式还没有用于传输,但是可以在框架的其他级别上使用,如作为内存中的TObjectList数据库引擎的一种可选的文件格式(使用我们的SynLZ压缩,参见神奇的虚表)。
标准Delphi值类型在JSON内容中以文本形式直接序列化。如整数或Int64存储为数字,双精度值存储为对应的浮点表示。
所有字符串内容都序列化为标准JSON文本字段,即嵌入双引号中(")。由于JSON使用UTF-8编码,这也是我们引入RawUTF8类型并在框架中到处使用它的原因之一。
在Delphi中,记录有一些很好的优点:
try..finally Free; end语句。 因此,对于像mORMot这样的框架来说,记录值的序列化是必须的。在实践中,应该将记录类型定义为packed record,以便序列化器更容易管理底层访问。
Delphi 2010以后,编译器在编译时生成额外的RTTI,以便描述所有记录字段并用于运行时。
顺便说一下,这种增强的RTTI是可执行文件尺寸在新版本编译器中增长如此之快的原因之一。
我们的SynCommons.pas单元能够使用这些增强信息,通过RecordLoad()和RecordSave()函数以及所有内部JSON编码过程序列化任何记录。
简而言之,你无需做什么。只需将您的记录用作参数,在Delphi 2010之后版本中,它们将被序列化为有效的JSON对象,唯一的限制是记录应该定义为packed record。
遗憾的是,序列化记录所需的信息只有在Delphi 2010之后才可用。
如果您的应用程序是在旧版本上开发的(如Delphi 7、Delphi 2007或Delphi 2009),您将无法直接将记录作为纯JSON对象自动序列化。
您有几个可用的路径:
record被序列化为二进制,并编码为Base64文本;record定义为纯文本。 请注意,任何自定义序列化(通过回调或文本定义)都将覆盖以前注册的所有方法,包括使用的RTTI增强机制。您可以更改默认的序列化,以便轻松满足您的需求。SynCommons.pas就是这样处理TGUID内容,TGUID被序列化为标准的JSON文本(如“C9A646D3-9C61-4CB7-BFCD-EE2522C8F633”),而不是遵循TGUID record所定义的 RTTI,即{“D1”:12345678,“D2”:23023,“D3”:9323,“D4”:“0123456789ABCDEF”} ,如果这样就很不方便。
Delphi 2010之前版本的编译器,默认情况下,任何记录值都将被序列化,使用专用的二进制(经过优化),即通过RecordLoad和RecordSave函数,然后编码为Base64,以纯文本的形式存储在JSON流中。
在生成的JSON字符串的开头添加了一个特殊的UTF-8前缀(不匹配任何现有的Unicode符号),以标识该内容为BLOB,如下所示:
{ "MyRecord": "ï¿°w6nDoMOnYQ==" }
你会在SynCommons.pas中找到BinToBase64和Base64ToBin两个功能,做了很多的速度优化。选择Base64编码是因为它是标准的,比十六进制高效得多,而且JSON仍然兼容,不需要转义它的内容。
在处理框架的许多内容时,您无需做更多的事:默认情况下,任何记录都遵循Base64序列化,因此您将能够发布或使用带有记录的基于接口的服务。
Base64编码对于计算机来说非常方便(它是一种紧凑而高效的格式),但是它的互操作性很有限。我们的格式是专用的,并且将使用Delphi的内部序列化方案:这意味着它在您的mORMot应用范围之外是不可读写的。在RESTful/SOA世界中,这听起来不像是一个特性,而是一个限制。
因此,需要可以像定义任何类一样自定义record的JSON序列化。它将允许将record变量作为普通JSON对象写入和解析,以便任何客户端或服务端使用。在内部使用一些回调用于执行序列化。
实际上,有两个入口点可以为record指定自定义JSON序列化:
TypeInfo()显式设置序列化回调,对动态数组使用完全相同的TTextWriter.RegisterCustomJSONSerializer方法。那么读写回调可以通过两种方式定义:
record布局,但自行完成所有编组(包括内存分配)。如果您想序列化以下记录:
TSQLRestCacheEntryValue = record
ID: TID;
Timestamp: cardinal;
JSON: RawUTF8;
end;
使用以下代码:
TTextWriter.RegisterCustomJSONSerializer(
TypeInfo(TSQLRestCacheEntryValue),
TTestServiceOrientedArchitecture.CustomReader,
TTestServiceOrientedArchitecture.CustomWriter
);
希望按如下格式输出:
{"ID":1786554763,"Timestamp":323618765,"JSON":"D:\\TestSQL3.exe"}
因此,可以定义写回调函数:
class procedure TTestServiceOrientedArchitecture.CustomWriter(
const aWriter: TTextWriter; const aValue);
var V: TSQLRestCacheEntryValue absolute aValue;
begin
aWriter.AddJSONEscape(['ID',V.ID,'Timestamp',Int64(V.Timestamp),'JSON',V.JSON]);
end;
在上面的代码中,名为Timestamp的cardinal字段类型被转换为Int64:正如AddJSONEscape方法的文档所述,array of const默认用整数值处理所有cardinal(这是Delphi编译器的一个限制)。通过Int64强制类型转换,cardinal值将按要求传送,而不是错误的负数> $7fffffff版本。
另一方面,对应的读回调函数为:
class function TTestServiceOrientedArchitecture.CustomReader(P: PUTF8Char;
var aValue; out aValid: Boolean): PUTF8Char;
var V: TSQLRestCacheEntryValue absolute aValue;
Values: array[0..2] of TValuePUTF8Char;
begin
result := JSONDecode(P,['ID','Timestamp','JSON'],@Values);
if result=nil then
aValid := false else begin
V.ID := GetInt64(Values[0].Value);
V.Timestamp := GetCardinal(Values[1].Value);
Values[2].ToUTF8(V.JSON);
aValid := true;
end;
end;
这里,JSONDecode()用于JSON对象的快速反序列化。
手工编写这些回调可能容易出错,特别是对于Reader事件。
您可以使用TTextWriter.RegisterCustomJSONSerializerFromText方法以基于文本的格式方便地定义record,同样,这些类型需要定义为packed record,以便文本定义不依赖于特定编译器的字段对齐特性。
同样,TSQLRestCacheEntryValue可以定义为典型的pascalrecord:
const
__TSQLRestCacheEntryValue = 'ID: Int64; Timestamp: cardinal; JSON: RawUTF8';
或用更短的句法:
const
__TSQLRestCacheEntryValue = 'ID Int64 Timestamp cardinal JSON RawUTF8';
这两个声明实现了相同的定义。注意,提供的文本应该与原始record类型定义完全匹配:不要交换顺序或遗漏属性!
按照惯例,我们在记录名称之前使用两个下划线字符(__),以便容易地标识定义。将它作为一个常量来编写可能确实很方便,它接近于记录类型定义本身,而不是内嵌在RegisterCustomJSONSerializerFromText()中调用。
然后按如下方式注册您的类型:
TTextWriter.RegisterCustomJSONSerializerFromText(
TypeInfo(TSQLRestCacheEntryValue),__TSQLRestCacheEntryValue);
现在您可以直接序列化任何记录值:
Cache.ID := 10;
Cache.Timestamp := 200;
Cache.JSON := 'test';
U := RecordSaveJSON(Cache,TypeInfo(TSQLRestCacheEntryValue));
Check(U='{"ID":10,"Timestamp":200,"JSON":"test"}');
您还可以反序列化现有的JSON内容:
U := '{"ID":210,"Timestamp":2200,"JSON":"test2"}';
RecordLoadJSON(Cache,@U[1],TypeInfo(TSQLRestCacheEntryValue));
Check(Cache.ID=210);
Check(Cache.Timestamp=2200);
Check(Cache.JSON='test2');
请注意,这个基于文本的定义非常强大,能够处理任何级别的嵌套记录或动态数组。
默认情况下,将以紧凑的形式编写JSON内容,并且只希望从JSON中传入需要的字段。您可以在注册时指定一些选项,以忽略所有未定义的字段。当您希望使用一些远程服务,并且只对几个字段感兴趣时,这非常有用。
例如,我们可以定义客户端对RESTful服务的访问,比如api.github.com:
type
TTestCustomJSONGitHub = packed record
name: RawUTF8;
id: cardinal;
description: RawUTF8;
fork: boolean;
owner: record
login: RawUTF8;
id: cardinal;
end;
end;
TTestCustomJSONGitHubs = array of TTestCustomJSONGitHub;
const
__TTestCustomJSONGitHub = 'name RawUTF8 id cardinal description RawUTF8 '+
'fork boolean owner{login RawUTF8 id cardinal}';
注意{ }格式定义了一个嵌套的记录,作为嵌套record .. end的一个更短的替代语法。
您还必须声明record packed。否则,您可能会遇到意外的访问冲突问题,因为对齐可能不同,这取决于本地设置和编译器修订。
现在我们可以注册记录,并提供一些额外的选项:
TTextWriter.RegisterCustomJSONSerializerFromText(TypeInfo(TTestCustomJSONGitHub),
__TTestCustomJSONGitHub).Options := [soReadIgnoreUnknownFields,soWriteHumanReadable];
这里我们定义了:
然后可以如下解析发送JSON:
var git: TTestCustomJSONGitHubs;
...
U := zendframeworkJson;
Check(DynArrayLoadJSON(git,@U[1],TypeInfo(TTestCustomJSONGitHubs))<>nil);
U := DynArraySaveJSON(git,TypeInfo(TTestCustomJSONGitHubs));
您可以看到,record序列化在动态数组级别自动实现,这在我们的示例中非常方便,因为api.github.com RESTful服务返回JSON数组。
它将转换160kb的非常冗长的JSON信息:
[{"id":8079771,"name":"Component_ZendAuthentication","full_name":"zendframework/Component_ZendAuthentication","owner":{"login":"zendframework","id":296074,"avatar_url":"https://1.gravatar.com/avatar/460576a0866d93fdacb597da4b90f233?d=https%3A%2F%2Fidenticons.github.com%2F292b7433472e2946c926bdca195cec8c.png&r=x","gravatar_id":"460576a0866d93fdacb597da4b90f233","url":"https://api.github.com/users/zendframework","HTTP_url":"https://github.com/zendframework","followers_url":"https://api.github.com/users/zendframework/followers","following_url":"https://api.github.com/users/zendframework/following{/other_user}","gists_url":"https://api.github.com/users/zendframework/gists{/gist_id}","starred_url":"https://api.github.com/users/zendframework/starred{/owner}{/repo}",...
成为更小的(6 KB)可读的JSON内容,只包含我们需要的信息:
[
{
"name": "Component_ZendAuthentication",
"id": 8079771,
"description": "Authentication component from Zend Framework 2",
"fork": true,
"owner":
{
"login": "zendframework",
"id": 296074
}
},
{
"name": "Component_ZendBarcode",
"id": 8079808,
"description": "Barcode component from Zend Framework 2",
"fork": true,
"owner":
{
"login": "zendframework",
"id": 296074
}
},
...
在解析过程中,所有不需要的JSON成员都将被忽略。解析器将跳过数据复制,而不做任何临时内存分配。这与其他现有的Delphi JSON解析器有很大的不同,后者首先将所有JSON树的值创建到内存中,然后根据请求浏览所有分支。
还要注意,字段是按照TTestCustomJSONGitHub记录定义排序的,该记录定义可能与原始JSON布局不同(这里颠倒了name/id字段,并将owner字段放在了每个项目结尾)。
使用mORMot,您可以直接访问Delphi代码中的内容,如下所示:
if git[0].id=8079771 then begin
Check(git[0].name='Component_ZendAuthentication');
Check(git[0].description='Authentication component from Zend Framework 2');
Check(git[0].fork=true);
Check(git[0].owner.login='zendframework');
Check(git[0].owner.id=296074);
end;
我们不需要使用中间对象(如gitarray.Value[0].Value['owner'].Value['login']等一些模糊的表达式)。您的代码将更具可读性,如果您错误拼写了字段名,编译时会报错,并且在IDE中很容易调试(因为record可以很容易检查)。
序列化可以处理任何类型的嵌套记录或动态数组,包括简单类型的动态数组(如array of integer或array of RawUTF8),或record的动态数组:
type
TTestCustomJSONRecord = packed record
A,B,C: integer;
D: RawUTF8;
E: record E1,E2: double; end;
F: TDateTime;
end;
TTestCustomJSONArray = packed record
A,B,C: integer;
D: RawByteString;
E: array of record E1: double; E2: string; end;
F: TDateTime;
end;
TTestCustomJSONArraySimple = packed record
A,B: Int64;
C: array of SynUnicode;
D: RawUTF8;
end;
对应的文本定义如下:
const
__TTestCustomJSONRecord = 'A,B,C integer D RawUTF8 E{E1,E2 double} F TDateTime';
__TTestCustomJSONArray = 'A,B,C integer D RawByteString E[E1 double E2 string] F TDateTime';
__TTestCustomJSONArraySimple = 'A,B Int64 C array of synunicode D RawUTF8';
以下类型可通过这种特性来处理:
| Delphi类型 | 备注 |
|---|---|
| boolean | 序列化为JSON布尔值 |
| byte word integer cardinal Int64 single double currency TUnixTime |
序列化为JSON数值 |
| string RawUTF8 SynUnicode WideString |
序列化为JSON字符串 |
| DateTime TTimeLog |
序列化为JSON文本,ISO 8601编码 |
| RawByteString | 序列化为JSON null或base64编码的JSON字符串 |
| RawJSON | 存储为未序列化的原始JSON内容 (如各种值、对象或数组) |
| TGUID | 序列化为JSON文本的GUID |
nested record |
序列化为JSON对象record ... end;或{…}标识中的嵌套定义 |
| nested registered record | 序列化为JSON,对应于定义的回调 |
dynamic array of record |
序列化为JSON数组 标识为 array of ... 或 [ ... ] |
| dynamic array of simple types | 序列化为JSON数组 标识为 array of integer |
| static array | 序列化为JSON数组 使用增强的RTTI处理,不使用文本定义 |
| variant | 序列化为JSON,完全支持TDocVariant自定义变体类型 |
对于其他类型(如枚举或集合),您可以简单地使用对应于二进制值的无符号整数类型,例如byte word cardinal Int64(取决于初始值的sizeof())。
例如,void TTestCustomJSONRecord可以序列化为:
{"A":0,"B":0,"C":0,"D":"","E":{"E1":0,"E2":0},"F":""}
或者void TTestCustomJSONArray可以序列化为:
{"A":0,"B":0,"C":0,"D":null,"E":[],"F":""}
或者void TTestCustomJSONArraySimple可以序列化为:
{"A":0,"B":0,"C":[],"D":""}
您可以参考提供的回归测试(在TTestLowLevelTypes.EncodeDecodeJSON中)来获得更多定制JSON序列化的示例。
注意,动态数组是在两个独立的上下文中处理的:
TDynArray包装器中提供的高级JSON序列化,即可以是一个真正的JSON数组,也可以在Delphi 2010之前版本使用通用二进制和Base64编码。 实际上,这个TDynArray包装器,参见TDynArray动态数组包装器,可以识别最常见的array of byte, word, integer, cardinal, Int64, double, currency, RawUTF8, SynUnicode, WinAnsiString, string。它们将被序列化为一个有效的JSON数组,即匹配类型(数字、浮点值或字符串)的有效JSON元素列表。
如果您有任何需要处理的关于标准动态数组的提议,请随时在论坛上发表您的建议!
对Delphi 2010及之后版本,框架将使用增强的RTTI创建一个JSON数组,该数组对应于每个动态数组项的数据,就像记录序列化一样。
对于Delphi 2009之前的编译器版本,动态数组(如array of packed record)将被默认序列化为二进制,然后进行Base64编码。这种方法总是有效的,但不利于AJAX客户端处理。
当然,您的应用程序可以通过TTextWriter.RegisterCustomJSONSerializer()类方法为任何其他动态数组提供自定义JSON序列化。为了正确地处理JSON数组的序列化和反序列化,需要与动态数组类型信息一起定义两个回调。
作为一种替代方法,您可以调用RegisterCustomJSONSerializerFromText方法,以一种方便的基于文本的格式定义记录,请参见前面。
事实上,如果您注册了一个动态数组自定义序列化器,它也将用于相关的内部record。
正如我们已经指出的,更改缺省序列化很方便。
例如,我们想序列化以下记录的动态数组:
TFV = packed record
Major, Minor, Release, Build: integer;
Main, Detailed: string;
end;
TFVs = array of TFV;
使用默认的序列化,这样的动态数组将被序列化:
通过定义回调,可以重载默认的序列化,这可能很方便,如果您不喜欢所有字段名都写在数据中,如下所示,这样浪费空间:
{"Major":1,"Minor":2001,"Release":3001,"Build":4001,"Main":"1","Detailed":"1001"}
为了该记录添加自定义序列化,我们需要实现两个回调。
我们希望的格式是一个包含所有字段的JSON数组,即:
[1,2001,3001,4001,"1","1001"]
这个方案比默认JSON对象格式短两倍多。
我们也可以使用其它方案,如使用JSONEncode()函数和JSON对象,或任何其他有效的JSON内容。
代码如下:
class procedure TCollTstDynArray.FVWriter(const aWriter: TTextWriter; const aValue);
var V: TFV absolute aValue;
begin
aWriter.Add('[%,%,%,%,"%","%"]',
[V.Major,V.Minor,V.Release,V.Build,V.Main,V.Detailed],twJSONEscape);
end;
该事件将写入动态数组的一个条目,而不包含最后一个','(将由TTextWriter.AddDynArrayJSON)。在这个方法中,twJSONEscape用于将提供的字符串内容转义为有效的JSON字符串(带有双引号和正确的UTF-8编码)。
当然,写程序的人比读程序的人更容易理解代码:
class function TCollTstDynArray.FVReader(P: PUTF8Char; var aValue;
out aValid: Boolean): PUTF8Char;
var V: TFV absolute aValue;
begin // '[1,2001,3001,4001,"1","1001"],[2,2002,3002,4002,"2","1002"],...'
aValid := false;
result := nil;
if (P=nil) or (P^<>'[') then
exit;
inc(P);
V.Major := GetNextItemCardinal(P);
V.Minor := GetNextItemCardinal(P);
V.Release := GetNextItemCardinal(P);
V.Build := GetNextItemCardinal(P);
V.Main := UTF8ToString(GetJSONField(P,P));
V.Detailed := UTF8ToString(GetJSONField(P,P));
if P=nil then
exit;
aValid := true;
result := P; // ',' or ']' for last item of array
end;
读方法应该返回一个指针,指向JSON输入缓冲区的下一个分隔符,该分隔符就在这个项目之后(',' 或 ']')。
注册过程本身非常简单:
TTextWriter.RegisterCustomJSONSerializer(TypeInfo(TFVs),
TCollTstDynArray.FVReader,TCollTstDynArray.FVWriter);
然后,从用户代码的角度来看,这个动态数组处理不会改变:一旦注册,JSON序列化器就会在框架的任何地方使用,只要这个类型是全局注册的。
下面是一个使用JSON对象的写方法,该方法可用于Delphi 2009及之后,以获得类似于通过增强RTTI的序列化。
class procedure TCollTstDynArray.FVWriter2(const aWriter: TTextWriter; const aValue);
var V: TFV absolute aValue;
begin
aWriter.AddJSONEscape(['Major',V.Major,'Minor',V.Minor,'Release',V.Release,
'Build',V.Build,'Main',V.Main,'Detailed',V.Detailed]);
end;
这将创建如下JSON内容:
{"Major":1,"Minor":2001,"Release":3001,"Build":4001,"Main":"1","Detailed":"1001"}
我们也可以使用类似的回调,如我们希望更改属性名,或者根据某些默认值忽略属性名。
那么对应的读回调可以写成:
class function TCollTstDynArray.FVReader2(P: PUTF8Char; var aValue;
out aValid: Boolean): PUTF8Char;
var V: TFV absolute aValue;
Values: array[0..5] of TValuePUTF8Char;
begin
aValid := false;
result := JSONDecode(P,['Major','Minor','Release','Build','Main','Detailed'],@Values);
if result=nil then
exit; // result^ = ',' or ']' for last item of array
V.Major := Values[0].ToInteger;
V.Minor := Values[1].ToInteger;
V.Release := Values[2].ToInteger;
V.Build := Values[3].ToInteger;
V.Main := Values[4].ToString;
V.Detailed := Values[5].ToString;
aValid := true;
end;
大部分JSON解码过程是在JSONDecode()函数中执行的,该函数将Values[].Value/ValueLen耦合指向P ^缓冲区内以空值终止的未转义内容。事实上,反序列化不会分配内存,因此会非常快。
如果您想回到默认的二进制+ Base64编码序列化,您可以如下运行注册方法:
TTextWriter.RegisterCustomJSONSerializer(TypeInfo(TFVs),nil,nil);
或者使用void定义调用基于文本的注册:
TTextWriter.RegisterCustomJSONSerializerFromText(TypeInfo(TTestCustomJSONGitHub),'');
您现在可以定义自定义JSON序列化器,以上述代码开始作为参考,或者通过RegisterCustomJSONSerializerFromText()方法基于文本的定义。
请注意,如果与其项目动态数组相对应的记录具有一些关联的RTTI(即,如果它包含一些引用计数类型,如任何字符串),则在mORMot服务过程中它将被序列化为JSON,正如记录序列化所述。
具有已发布属性的类,即从TPersistent或我们的ORM专用TSQLRecord类继承的每个类将被序列化为真正的JSON对象,包含其所有发布属性值。 请参阅具有ORM数据库类型和JSON内容的相应表的TSQLRecord字段定义。
Delphi字符串列表,即TStrings类型的类将被序列化为JSON字符串数组。 这就是为什么我们还通过我们专用的RawUTF8类型引入了专用的TRawUTF8List类,用于直接UTF-8内容存储,减少了编码转换的需要,从而提高了处理速度。
事实上,任何TObject都可以在整个框架中被序列化为JSON:不仅用于ORM部分(用于已布属性),还用于SOA(用作基于接口的服务方法的参数)。 所有JSON序列化都集中在ObjectToJSON()和JSONToObject()(又名TJSONSerializer.WriteObject)函数中。
在某些情况下,拥有自定义序列化可能很方便,例如,如果要管理某些第三方类,或者在运行时将序列化方案调整为特定用途。
您可以通过调用TJSONSerializer.RegisterCustomSerializer类方法来添加任何类的定制序列化。将为特定的类类型定义两个回调,并将用于序列化或反序列化对象实例。回调函数是对象的类方法(procedure() of object),而不是普通函数(对于一些经过演化的对象,在序列化期间使用上下文可能是有意义的)。
在这个特性的当前实现中,回调期望底层实现。也就是说,它们的实现代码应该遵循函数JSONToObject()模式,即调用底层GetJSONField()函数解码JSON内容,遵循函数TJSONSerializer.WriteObject()模式,即aSerializer.Add/AddInstanceName/AddJSONEscapeString将类实例编码为JSON。
注意,该进程在"{...}"JSON对象之外调用,允许任何序列化方案:甚至类内容也可以根据请求序列化为JSON字符串、JSON数组或JSON数字。
例如,我们想定制如下类的序列化(在syncommon.pas中定义):
TFileVersion = class
protected
fDetailed: string;
fBuildDateTime: TDateTime;
public
Major: Integer;
Minor: Integer;
Release: Integer;
Build: Integer;
BuildYear: integer;
Main: string;
published
property Detailed: string read fDetailed write fDetailed;
property BuildDateTime: TDateTime read fBuildDateTime write fBuildDateTime;
end;
默认情况下,因为它是在{$M+} ... {$M-}条件中定义的,RTTI对于发布属性是可用的(就像它从TPersistent继承一样)。也就是说,默认的JSON序列化是:
{"Detailed":"1.2.3.4","BuildDateTime":"1911-03-14T00:00:00"}
这是在TSynLog内容中序列化或当前AJAX使用时所期望的。
我们想把这个类序列化成:
{"Major":1,"Minor":2001,"Release":3001,"Build":4001,"Main":"1","BuildDateTime":"1911-03-14"}
因此,我们将定义写入器回调,如下所示:
class procedure TCollTstDynArray.FVClassWriter(const aSerializer: TJSONSerializer;
aValue: TObject; aOptions: TTextWriterWriteObjectOptions);
var V: TFileVersion absolute aValue;
begin
aSerializer.AddJSONEscape(['Major',V.Major,'Minor',V.Minor,'Release',V.Release,'Build',V.Build,'Main',V.Main,'BuildDateTime',DateTimeToIso8601Text(V.BuildDateTime)]);
end;
大部分JSON序列化工作将在AddJSONEscape方法中完成,期望JSON对象描述是一个名/值对数组。
那么关联的读回调可以是这样:
class function TCollTstDynArray.FVClassReader(const aValue: TObject; aFrom: PUTF8Char;
var aValid: Boolean; aOptions: TJSONToObjectOptions): PUTF8Char;
var V: TFileVersion absolute aValue;
Values: array[0..5] of TValuePUTF8Char;
begin
result := JSONDecode(aFrom,['Major','Minor','Release','Build','Main','BuildDateTime'],@Values);
aValid := (result<>nil);
if aValid then begin
V.Major := Values[0].ToInteger;
V.Minor := Values[1].ToInteger;
V.Release := Values[2].ToInteger;
V.Build := Values[3].ToInteger;
V.Main := Values[4].ToString;
V.BuildDateTime := Iso8601ToDateTimePUTF8Char(Values[5].Value,Values[5].ValueLen);
end;
end;
这里,JSONDecode函数将JSON对象反序列化为一个PUTF8Char值数组,不需要任何内存分配,实际上是Values[].Value将指向aFrom内存缓冲区中的未转义和#0终止的内容,所以解码非常快。
然后,注册步骤定义为:
TJSONSerializer.RegisterCustomSerializer(TFileVersion,
TCollTstDynArray.FVClassReader,TCollTstDynArray.FVClassWriter);
如果您想禁用自定义序列化,可以调用与此相同的方法:
TJSONSerializer.RegisterCustomSerializer(TFileVersion,nil,nil);
这将把指定类的JSON序列化重置为默认序列化器(即写入发布属性)。
上述代码使用框架的一些底层函数(例如AddJSONEscape和JSONDecode)作为JSON对象实现序列化,但是您可以根据需要使用任何其他序列化方案。也就是说,您可以将整个类实例序列化为一个JSON字符串或数值,甚至一个JSON数组,这取决于读写器注册回调的实现。
如果您的定制只是希望更改一些属性名,那么您可以使用TJSONSerializer.RegisterCustomSerializerFieldNames类方法。
例如,给定以下类:
type
TMyClass = class(TSynPersistent)
private
FLength: Integer;
FColor: Integer;
FName: RawUTF8;
published
property Color: Integer read FColor write FColor;
property Length: Integer read FLength write FLength;
property Name: RawUTF8 read FName write FName;
end;
您可以如下使用默认序列化:
var
O: TMyClass;
json: RawUTF8;
begin
O := TMyClass.Create;
O.Color := 10;
O.Length := 20;
O.Name := 'one';
json := ObjectToJSON(O);
writeln(json); // {"Color":10,"Length":20,"Name":"one"}
然后切换为定制序列化:
TJSONSerializer.RegisterCustomSerializerFieldNames(TMyClass, ['name','length'], ['n','len']);
json := ObjectToJSON(O);
writeln(json); // {"Color":10,"len":20,"n":"one"}
并返回正常/默认序列化:
TJSONSerializer.RegisterCustomSerializerFieldNames(TMyClass, [], []);
json := ObjectToJSON(O);
writeln(json); // {"Color":10,"Length":20,"Name":"one"}
你可以忽略一些字段,通过设置目标名称为'':
TJSONSerializer.RegisterCustomSerializerFieldNames(TMyClass, ['length'], ['']);
json := ObjectToJSON(O);
writeln(json); // {"Color":10,"Name":"one"}
O.Free;
end;
因此,此方法可能有助于处理已存在的JSON对象,例如从第三方REST服务器检索的JSON对象。
请注意TJSONSerializer.RegisterCustomSerializerFieldNames方法不接受TSQLRecord类,因为ORM序列化是在它自己的(优化的)集合中处理的,如果需要,您可以使用ORM级映射,请参阅数据库优先的ORM。
您甚至可以将TObjectList实例序列化为一个有效的JSON数组,并能够存储每个实例类名,从而允许存储不一致的对象列表。
调用TJSONSerializer.RegisterClassForJSON()只需要在其内部表中注册每个TObject类,并能够从每个JSON对象中序列化的类名创建实例。
实际上,如果ObjectToJSON()或TJSONWriter.WriteObject()定义了它们的woStoreClassName选项,那么一个新的“ClassName”:字段将被写入序列化JSON对象的第一个字段。
这个新的“ClassName”字段将被识别为:
JSONToObject()用于 TObjectList 成员,JSONToNewObject()方法。 请注意,模型的所有TSQLRecord类都是通过调用TJSONSerializer.RegisterClassForJSON()自动注册的:您不必注册它们,并且可以直接序列化TSQLRecord的TObjectList。
因此,这类代码现在可以工作:
// register the type (but Classes.RegisterClass list is also checked)
TJSONSerializer.RegisterClassForJSON([TComplexNumber]);
// create an instance by reading the textual class name field
J := '{"ClassName":"TComplexNumber", "Real": 10.3, "Imaginary": 7.92 }';
P := @J[1]; // make local copy of constant
Comp := TComplexNumber(JSONToNewObject(P,Valid));
// here Comp is a valid unserialized object :)
Check(Valid);
Check(Comp.ClassType=TComplexNumber);
CheckSame(Comp.Real,10.3);
CheckSame(Comp.Imaginary,7.92);
// do not forget to free the memory (Comp can be nill if JSON was not valid)
Comp.Free;
因此,内部TObjectList进程将依赖于一个类似的进程,动态创建适当的类实例。您甚至可以让多个类出现在一个TObjectList中:唯一的先决条件是,通过调用TJSONSerializer.RegisterClassForJSON(),所有类类型都应该在双方之前注册。
Representational state transfer(REST)是一种用于分布式超媒体系统(如万维网)的软件架构。 因此,它不仅仅是构建“Web服务”的方法。 “代表性状态转移”和“REST”这两个术语于2000年在超文本传输协议(HTTP)规范的主要作者之一Roy Fielding的博士论文中引入,整个互联网都依赖于该论文。
Web有5个基本原理,用于创建REST服务:
互联网就是获取数据的地方。这些数据可以是网页、图像、视频、文件等格式,也可以是动态输出,如获取新订阅的客户。REST中的第一个要点是开始根据资源而不是物理文件进行思考。
您可以通过某个URI访问资源,例如。
http://www.mysite.com/pictures/logo.png - 图像资源;http://www.mysite.com/index.html - 静态资源;http://www.mysite.com/Customer/1001 - 返回XML或JSON内容的动态资源;http://www.mysite.com/Customer/1001/Picture - 动态资源返回图像。旧的web技术,例如aspx或ColdFusion,确实通过指定参数来请求资源。
http://www.mysite.com/Default.aspx?a=1;a=2&b=1&a=3
在REST中,我们向当前URI添加了另一个约束:实际上,每个URI应该惟一地表示数据收集的每个项。
例如,对于获取的客户和订单,您可以看到以下独特的URI格式:
| 客户数据 | URI |
|---|---|
| 获取名为“杜邦”的客户详细信息 | http://www.mysite.com/Customer/dupont |
| 获取名为“史密斯”的客户详细信息 | http://www.mysite.com/Customer/smith |
| 获得客户“杜邦”的订单 | http://www.mysite.com/Customer/dupont/Orders |
| 获得客户“史密斯”的订单 | http://www.mysite.com/Customer/smith/Orders |
在这里,“杜邦”和“史密斯”被用作指定客户的唯一标识符。在实践中,名称远非唯一的,因此大多数系统使用惟一的ID(比如整数、十六进制数字或GUID)。
要访问这些标识的资源,基本的CRUD活动由一组HTTP谓词标识:
| HTTP方法 | 处理 |
|---|---|
| GET | 列出集合的成员(一个或多个) |
| PUT | 更新集合的成员 |
| POST | 在集合中创建一个新条目 |
| DELETE | 删除集合中的一个成员 |
然后,在URI级别,您可以定义集合的类型,例如http://www.mysite.com/Customer来标识客户,或者http://www.mysite.com/Customer/1234/Orders来获取给定的订单。
这个HTTP方法和URI的组合替换了一个基于英语的方法列表,比如GetCustomer / InsertCustomer / UpdateOrder / RemoveOrder。
您通过网络发送的是实际资源数据的表示。
主要的表示方案是XML和JSON。
例如,以下是如何从GET方法检索客户数据:
<Customer>
<ID>1234</ID>
<Name>Dupond</Name>
<Address>Tree street</Address>
</Customer>
下面是一个简单的JSON片段,用于创建一个带有名称和地址的新客户记录(由于我们创建了一个新记录,这里我们将他命名为“Dupond”——以D结尾——而不是“Dupont”):
{"Customer": {"Name":"Dupond", "Address":"Tree street"}}
因此,对于使用POST命令传输的数据,RESTful服务器将返回刚刚创建的ID。
请参阅JSON,原因是在mORMot中,我们更喜欢使用JSON格式。
每个请求都应该是一个独立的请求,这样我们就可以使用负载平衡技术进行扩展。
独立请求意味着数据也发送请求的状态,以便服务器可以将该请求从该级别转发到下一级别。
更多细节请看下面。
Synopse mORMot框架是按照Fielding的REST架构风格设计的,没有使用HTTP,也没有与万维网交互。这种遵循REST原则的系统通常被称为“RESTful”。此外,框架还可以通过Internet(通过使用mORMotHttpClient/ mORMotHttpServer单元以及TSQLHttpServer和TSQLHttpClient类)在嵌入式低资源和快速HTTP服务器上提供标准的HTTP/1.1页面。
实现了标准的RESTful方法,即GET/PUT/POST/DELETE。
在标准REST定义中添加了以下方法,用于锁定单个记录和处理数据库事务(这会加快数据库进程):
LOCK锁定集合中的某个成员;UNLOCK解锁以解锁集合中的一个成员;BEGIN开始发起交易;END结束提交事务;ABORT中止以回滚事务。 GET方法具有可选的分页特性,与用于数据分页的YUI数据源请求语法兼容,请参阅TSQLRestServer.URI方法和http://developer.yahoo.com/yui/datatable/#data。当然,这打破了“每个资源都由惟一标识符标识”RESTful原则,但是使用它要容易得多,例如实现分页或自定义过滤。
从Delphi代码的角度来看,RESTful客户端-服务端架构是通过从一个主类继承一些通用的方法和属性来实现的。
这个图说明了TSQLRest类如何实现为客户端类和服务端类共同的祖先。
BLOB字段在类定义中被定义为TSQLRawBlob发布属性,它是RawByteString类型的别名(在SynCommons.pas中为Delphi 2007以上版本定义,因为它仅出现在Delphi 2009中)。 但是它们的内容不包含在框架的标准RESTful方法中,以节省网络带宽。
RESTful协议允许通过特定的URL检索(GET)或保存(PUT))BLOB,比如:
ModelRoot/TableName/TableID/BlobFieldName
这甚至比标准JSON编码更好,后者运行良好但是需要将BLOB与十六进制值相互转换,因此需要两倍于它的正常大小。 通过使用这种专用URL,数据可以作为完整二进制文件传输。
通过TSQLRest类的一些专用方法处理BLOB字段:RetrieveBlob和UpdateBlob。
框架源代码树中提供的"04 - HTTP Client-Server"示例程序可以用来展示框架是如何支持AJAX的,并且可以与其他任何基于JSON的REST服务器(如CouchDB)进行比较。
首先,通过将Unit2.pas中的参数从true更改为false来取消身份验证:
DB := TSQLRestServerDB.Create(Model,ChangeFileExt(paramstr(0),'.db3'),
false);
并在Project04Client.dpr中注释以下行:
Form1.Database := TSQLHttpClient.Create(Server,'8080',Form1.Model);
// TSQLHttpClient(Form1.Database).SetUser('User','synopse');
Application.Run;
然后您可以使用浏览器测试JSON内容:
Project04Server.exe程序:后台HTTP服务器及其SQLite3数据库引擎;Project04Client.exe实例,并添加/查找任何条目,以稍微填充数据库;Project04Client.exe程序; http://localhost:8080/root
TSQLHttpServer Server Error 400
http://localhost:8080/root/SampleRecord
SampleRecord 的ID结果,这些id编码为JSON列表,例如。 [{"ID":1},{"ID":2},{"ID":3},{"ID":4}]
http://localhost:8080/root/SampleRecord/1
SampleRecord的内容,编码为JSON,例如。{"ID":1,"Time":"2010-02-08T11:07:09","Name":"AB","Question":"To be or not to be"}
您在不到400 KB的尺寸获得了一个完整的HTTP/SQLite3 RESTful JSON服务器。
注意,Internet Explorer或旧版本的FireFox不识别application/json; charset=UTF-8内容类型,这是这些软件的一个限制,因此上述请求将下载内容作为.json文件,但不会阻止AJAX请求按预期工作。
我们的框架将REST实现为无状态协议,将HTTP/1.1协议用作它的通信层。
无状态服务是将每个请求视为独立事务的服务,与之前的任何请求无关。
一开始,您可能会发现传统的客户端-服务端方法有点令人失望。在无状态的世界中,您永远不能确保您的客户端数据是最新的。数据唯一安全的地方是服务器。在网络世界中,这并不令人困惑。但是如果您来自富客户端背景,这可能会让您担心:是否应该从服务端编写一些同步代码,以便将所有更改复制到所有客户端。在无状态体系结构中,这不再是必需的。
这种架构的主要规则是确保服务器是唯一引用,并且客户端能够从服务端检索任何挂起的更新。也就是说,总是修改服务器端上的记录内容,然后刷新客户机以检索修改后的值。不要直接修改客户端,而是始终通过服务器。框架的UI组件遵循这些原则。客户端修改可以执行,但必须在独立的自主表/数据库中进行。这将避免在并发客户端修改时出现任何同步问题。
最常见的RESTful JSON对JSON内容使用了一种冗长的格式:如http://bitworking.org/news/restful_json建议将整个URI放入JSON内容;
[
"http://example.org/coll/1",
"http://example.org/coll/2",
"http://example.org/coll/3",
...
"http://example.org/coll/N",
]
框架的其余实现将返回最简洁的JSON内容,其中包含一个对象数组:
[{"ID":1},{"ID":2},{"ID":3},{"ID":4}]
根据设置的不同,mORMot服务器实际上可能返回另一种格式(见下面的非展开格式),它可以更短,因为它不复制字段名:
{"fieldCount":1,"values":["ID",1,2,3,4,5,6,7]}
它节约了带宽和保留可读性:如果您能够将GET请求发送到URI http://example.org/coll,那么您将能够在每个请求的开头追加这个URI,这难道没有意义吗?
在所有情况下,Synopse mORMot框架总是返回JSON内容,就像SQL查询的纯响应一样,带有数组和字段名。
注意,JSON内容的生成根据 TSQLRestServer.NoAJAXJSON 属性有两种模式:
[{"ID":0,"Int":0,"Test":"abcde+¬ef+á+¬","Unicode":"abcde+¬ef+á+¬","Ansi":"abcde+¬ef+á+¬","ValFloat":3.14159265300000E+0000,"ValWord":1203,"ValDate":"2009-03-10T21:19:36","Next":0},{..}]
{"fieldCount":9,"values":["ID","Int","Test","Unicode","Ansi","ValFloat","ValWord","ValDate","Next",0,0,"abcde+¬ef+á+¬","abcde+¬ef+á+¬","abcde+¬ef+á+¬",3.14159265300000E+0000,1203,"2009-03-10T21:19:36",0,..]}
默认情况下,NoAJAXJSON属性在TSQLRestServer.ExportServerNamedPipe调用时设置为true:如果您使用命名管道进行通信,您可能不会使用JavaScript客户端,因为所有浏览器都只通过HTTP进行通信!
否则,NoAJAXJSON属性将设置为false。如果从不执行JavaScript,您可以强制将其值设置为true,将节省一些带宽,如果不展开JSON内容,即使使用Delphi解析JSON内容也会更快。
在“未展开”的模式中,如下JSON内容:
[{"ID":1},{"ID":2},{"ID":3},{"ID":4},{"ID":5},{"ID":6},{"ID":7}]
将以更短的形式传送:
{"fieldCount":1,"values":["ID",1,2,3,4,5,6,7]}
SQlite3级的全局缓存用于增强框架的可伸缩性,其结果编码采用JSON存储。
为了加快服务器响应时间,特别是在并发客户端访问中,不需要对每个请求都调用内部数据库引擎。事实上,已经引入了一个全局缓存来存储最新的SQL SELECT语句结果,直接以JSON格式存储在内存中。
SQLite3引擎访问在SQL/JSON缓存级别受到保护,通过大多数TSQLRestServerDB方法中的DB.LockJSON()调用。
TSynCache实例在TSQLDataBase内部全局实例中实例化,如下所示:
constructor TSQLRestServerDB.Create(aModel: TSQLModel; aDB: TSQLDataBase;
aHandleUserAuthentication: boolean);
begin
fStatementCache.Init(aDB.DB);
aDB.UseCache := true; // we better use caching in this JSON oriented use
(...)
这将在SQL级别启用全局JSON缓存。这个缓存将在每个INSERT、UPDATE或DELETE SQL语句(无论对应的表是什么)上重置。
如果您需要为特定的请求禁用JSON缓存,请在SQL语句的任何地方,例如ORM WHERE子句中,添加SQLDATABASE_NOCACHE文本,即'/*nocache*/'文本注释。它将指示TSQLDataBase不缓存返回的JSON内容。它可能有用,例如,如果您传递一个指针作为PtrInt(aVariable)绑定参数,它可能具有相同的整数值,但内容不同。
在实践中,这种全局缓存被发现是有效的,即使它的实现有些“幼稚”。它实际上更优于其它很多客户端-服务端解决方案HTTP级缓存机制(如Squid proxy),因为我们的缓存是在SQL级,在所有CRUD/Restful查询之间共享,独立于身份验证方案,否则会污染URI。与其他级别的缓存相关联,所以框架的可伸缩性非常好。