10. JSON RESTful客户端-服务端

  在描述这个框架的客户端-服务端设计之前,我们可能需要详细说明它所基于的一些标准:

  • JSON作为其内部数据存储和传输格式;
  • REST作为其客户端-服务端架构。

10.1. JSON

10.1.1. 为什么使用JSON ?

  如前所述,框架内部使用JSON格式。根据定义,JavaScript对象表示法(JSON)是一种标准的、开放的轻量级计算机数据交换格式。

  JSON的基本类型可从http://en.wikipedia.org/wiki/JSON检索到。

类型 描述
Number JavaScript中的双精度浮点格式,一般取决于实现,没有特定的整数类型
String 加双引号的Unicode,支持反斜杠转义
Boolean truefalse
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的专有格式,会产生以下几种特性:

  • 与XML一样,它是一种基于文本的、人类可读的格式,用于表示简单的数据结构和关联数组(称为对象);
  • 它更易于阅读(同时适用于人类和机器),实现速度更快,而且比常用的XML小得多;
  • 这是一种非常有效的数据缓存格式;
  • 该方案允许将其重写为带零终止字符的UTF-8字符串,几乎没有浪费空间:该特性用于对表结果进行高速JSON文本转换,不需要分配内存或复制数据;
  • JavaScript语言原生支持,在任何AJAX(即Web 2.0)或HTML5移动应用中都是一种完美的序列化格式;
  • JSON格式很简单,用一个简短的RFC文档进行规范;
  • JSON和SQLite3的默认文本编码都是UTF-8,它使用完整的Unicode字符集进行存储和通信;
  • 它是自.NET Framework 3.5以来在Windows Communication Foundation(WCF)中创建的ASP.NET AJAX服务使用的默认数据格式, 所以这是微软官方支持的。
  • 对于二进制BLOB传输,我们简单地将二进制数据编码为Base64;请注意,默认情况下,BLOB字段不会通过REST与JSON对象中的其他字段一起传输(唯一的例外是动态数组字段,它们在其他字段中进行传输)。

  REST JSON序列化在我们的ORM中用于处理任何发布的TSQLRecord属性,并在框架的基于接口的SOA架构中用于内容传输。

  框架中实现了整个http://json.org标准,但有一些例外/扩展:

  • #0字符表示输入结束,与几乎所有JSON库一样,因此,如果您的文本输入包含#0字符,请将其作为二进制处理(注意,其他控制字符按要求转义);
  • 您可以使用不加双引号的纯ASCII属性名来使用“扩展语法”(如MongoDB使用的);
  • 浮点数有时被限制为货币(即4个小数),以确保序列化/反序列化不会损失精度;但在这种情况下,它可以通过一组选项扩展为双精度类型;
  • 与JavaScript一样,整数没有53位限制,框架处理64位的整数值,在使用JavaScript后端时,您可能必须将巨大的数值以文本形式传输。

  在实践中,JSON已经被证明是易于使用和稳定的。二进制格式还没有用于传输,但是可以在框架的其他级别上使用,如作为内存中的TObjectList数据库引擎的一种可选的文件格式(使用我们的SynLZ压缩,参见神奇的虚表)。

10.1.2. 值序列化

  标准Delphi值类型在JSON内容中以文本形式直接序列化。如整数或Int64存储为数字,双精度值存储为对应的浮点表示。

  所有字符串内容都序列化为标准JSON文本字段,即嵌入双引号中(")。由于JSON使用UTF-8编码,这也是我们引入RawUTF8类型并在框架中到处使用它的原因之一。

10.1.3. 记录序列化

  在Delphi中,记录有一些很好的优点:

  • 记录是值对象,即通过值访问,而不是通过引用访问,这非常方便,如在定义领域驱动设计时;
  • 记录可以包含任何其他记录或动态数组,因此使用非常方便(不需要定义子类或列表);
  • 记录变量可以在堆栈上分配,因此不会请求全局堆;
  • 编译器会自动释放超出作用域的记录实例,所以您不需要编写任何try..finally Free; end语句。

  因此,对于像mORMot这样的框架来说,记录值的序列化是必须的。在实践中,应该将记录类型定义为packed record,以便序列化器更容易管理底层访问。

10.1.3.1. 通过增强的RTTI实现自动序列化

  Delphi 2010以后,编译器在编译时生成额外的RTTI,以便描述所有记录字段并用于运行时。

  顺便说一下,这种增强的RTTI是可执行文件尺寸在新版本编译器中增长如此之快的原因之一。

  我们的SynCommons.pas单元能够使用这些增强信息,通过RecordLoad()RecordSave()函数以及所有内部JSON编码过程序列化任何记录。

  简而言之,你无需做什么。只需将您的记录用作参数,在Delphi 2010之后版本中,它们将被序列化为有效的JSON对象,唯一的限制是记录应该定义为packed record

10.1.3.2. 用于Delphi旧版本的序列化

  遗憾的是,序列化记录所需的信息只有在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”} ,如果这样就很不方便。

10.1.3.2.1. 默认的Binary/Base64序列化

  Delphi 2010之前版本的编译器,默认情况下,任何记录值都将被序列化,使用专用的二进制(经过优化),即通过RecordLoadRecordSave函数,然后编码为Base64,以纯文本的形式存储在JSON流中。

  在生成的JSON字符串的开头添加了一个特殊的UTF-8前缀(不匹配任何现有的Unicode符号),以标识该内容为BLOB,如下所示:

 { "MyRecord": "ï¿°w6nDoMOnYQ==" }

  你会在SynCommons.pas中找到BinToBase64Base64ToBin两个功能,做了很多的速度优化。选择Base64编码是因为它是标准的,比十六进制高效得多,而且JSON仍然兼容,不需要转义它的内容。

  在处理框架的许多内容时,您无需做更多的事:默认情况下,任何记录都遵循Base64序列化,因此您将能够发布或使用带有记录的基于接口的服务。

10.1.3.2.2. 自定义序列化

  Base64编码对于计算机来说非常方便(它是一种紧凑而高效的格式),但是它的互操作性很有限。我们的格式是专用的,并且将使用Delphi的内部序列化方案:这意味着它在您的mORMot应用范围之外是不可读写的。在RESTful/SOA世界中,这听起来不像是一个特性,而是一个限制。

  因此,需要可以像定义任何类一样自定义record的JSON序列化。它将允许将record变量作为普通JSON对象写入和解析,以便任何客户端或服务端使用。在内部使用一些回调用于执行序列化。

  实际上,有两个入口点可以为record指定自定义JSON序列化:

  • 在设置自定义动态数组JSON序列化器时(参见下面),相关记录使用相同的读取和写入回调;
  • 通过为记录的TypeInfo()显式设置序列化回调,对动态数组使用完全相同的TTextWriter.RegisterCustomJSONSerializer方法。

  那么读写回调可以通过两种方式定义:

  • 编写代码,手工实现JSON文本编码与解析;
  • 通过一些基于文本的类型定义,遵循record布局,但自行完成所有编组(包括内存分配)。
10.1.3.2.3. 定义回调函数

  如果您想序列化以下记录:

  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;

  在上面的代码中,名为Timestampcardinal字段类型被转换为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对象的快速反序列化。

10.1.3.2.4. 基于文本的定义

  手工编写这些回调可能容易出错,特别是对于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传入的任何未定义字段;
  • 让JSON输出更具可读性。

  然后可以如下解析发送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 integerarray 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序列化的示例。

10.1.4. 动态数组序列化

10.1.4.1. 标准JSON数组

  注意,动态数组是在两个独立的上下文中处理的:

  • 在框架的ORM部分,它们存储为BLOB,并且总是在Base64编码之后传输,请参见TSQLRecord字段定义
  • 在基于接口的服务范围内,动态数组值和参数使用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

10.1.4.2. 自定义序列化

  正如我们已经指出的,更改缺省序列化很方便。

  例如,我们想序列化以下记录的动态数组:

  TFV = packed record
    Major, Minor, Release, Build: integer;
    Main, Detailed: string;
  end;
  TFVs = array of TFV;

  使用默认的序列化,这样的动态数组将被序列化:

  • Delphi 2010之前版本序列化为Base64编码的二进制缓存,这对于AJAX客户端来说并不容易理解;
  • Delphi 2010及之后版本的增强RTTI会将JSON对象序列化为JSON数组,列出对象的所有属性名。

  通过定义回调,可以重载默认的序列化,这可能很方便,如果您不喜欢所有字段名都写在数据中,如下所示,这样浪费空间:

{"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,正如记录序列化所述。

10.1.5. TSQLRecord TPersistent TStrings TRawUTF8List

  具有已发布属性的类,即从TPersistent或我们的ORM专用TSQLRecord类继承的每个类将被序列化为真正的JSON对象,包含其所有发布属性值。 请参阅具有ORM数据库类型和JSON内容的相应表的TSQLRecord字段定义

  Delphi字符串列表,即TStrings类型的类将被序列化为JSON字符串数组。 这就是为什么我们还通过我们专用的RawUTF8类型引入了专用的TRawUTF8List类,用于直接UTF-8内容存储,减少了编码转换的需要,从而提高了处理速度。

10.1.6. TObject序列化

  事实上,任何TObject都可以在整个框架中被序列化为JSON:不仅用于ORM部分(用于已布属性),还用于SOA(用作基于接口的服务方法的参数)。 所有JSON序列化都集中在ObjectToJSON()JSONToObject()(又名TJSONSerializer.WriteObject)函数中。

10.1.6.1. 自定义类的序列化

  在某些情况下,拥有自定义序列化可能很方便,例如,如果要管理某些第三方类,或者在运行时将序列化方案调整为特定用途。

  您可以通过调用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序列化重置为默认序列化器(即写入发布属性)。

  上述代码使用框架的一些底层函数(例如AddJSONEscapeJSONDecode)作为JSON对象实现序列化,但是您可以根据需要使用任何其他序列化方案。也就是说,您可以将整个类实例序列化为一个JSON字符串或数值,甚至一个JSON数组,这取决于读写器注册回调的实现。

10.1.6.2. 自定义字段名序列化

  如果您的定制只是希望更改一些属性名,那么您可以使用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

10.1.6.3. TObjectList序列化

  您甚至可以将TObjectList实例序列化为一个有效的JSON数组,并能够存储每个实例类名,从而允许存储不一致的对象列表。

  调用TJSONSerializer.RegisterClassForJSON()只需要在其内部表中注册每个TObject类,并能够从每个JSON对象中序列化的类名创建实例。

  实际上,如果ObjectToJSON()TJSONWriter.WriteObject()定义了它们的woStoreClassName选项,那么一个新的“ClassName”:字段将被写入序列化JSON对象的第一个字段。

  这个新的“ClassName”字段将被识别为:

  • 通过JSONToObject()用于 TObjectList 成员,
  • 并通过新的JSONToNewObject()方法。

  请注意,模型的所有TSQLRecord类都是通过调用TJSONSerializer.RegisterClassForJSON()自动注册的:您不必注册它们,并且可以直接序列化TSQLRecordTObjectList

  因此,这类代码现在可以工作:

// 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(),所有类类型都应该在双方之前注册。

10.2. REST

10.2.1. What is REST?

  Representational state transfer(REST)是一种用于分布式超媒体系统(如万维网)的软件架构。 因此,它不仅仅是构建“Web服务”的方法。 “代表性状态转移”和“REST”这两个术语于2000年在超文本传输协议(HTTP)规范的主要作者之一Roy Fielding的博士论文中引入,整个互联网都依赖于该论文。

  Web有5个基本原理,用于创建REST服务:

  • 一切都是资源;
  • 每个资源由唯一标识符标识;
  • 使用简单统一的接口;
  • 通信是通过资源表示来完成的;
  • 每个请求都是无状态的。

10.2.1.1. 基于资源

  互联网就是获取数据的地方。这些数据可以是网页、图像、视频、文件等格式,也可以是动态输出,如获取新订阅的客户。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 - 动态资源返回图像。

10.2.1.2. 唯一标识符

  旧的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)。

10.2.1.3. 接口

  要访问这些标识的资源,基本的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

10.2.1.4. 通过资源表示

  您通过网络发送的是实际资源数据的表示。

  主要的表示方案是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格式。

10.2.1.5. 无状态

  每个请求都应该是一个独立的请求,这样我们就可以使用负载平衡技术进行扩展。

  独立请求意味着数据也发送请求的状态,以便服务器可以将该请求从该级别转发到下一级别。

  更多细节请看下面。

10.2.2. RESTful mORMot

  Synopse mORMot框架是按照Fielding的REST架构风格设计的,没有使用HTTP,也没有与万维网交互。这种遵循REST原则的系统通常被称为“RESTful”。此外,框架还可以通过Internet(通过使用mORMotHttpClient/ mORMotHttpServer单元以及TSQLHttpServerTSQLHttpClient类)在嵌入式低资源和快速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客户端-服务端架构是通过从一个主类继承一些通用的方法和属性来实现的。

TSQLRestClientURI
TSQLRestClient
TSQLRestServer
TSQLRest

  这个图说明了TSQLRest类如何实现为客户端类和服务端类共同的祖先。

10.2.2.1. BLOB字段

  BLOB字段在类定义中被定义为TSQLRawBlob发布属性,它是RawByteString类型的别名(在SynCommons.pas中为Delphi 2007以上版本定义,因为它仅出现在Delphi 2009中)。 但是它们的内容不包含在框架的标准RESTful方法中,以节省网络带宽。

  RESTful协议允许通过特定的URL检索(GET)或保存(PUT))BLOB,比如:

 ModelRoot/TableName/TableID/BlobFieldName

  这甚至比标准JSON编码更好,后者运行良好但是需要将BLOB与十六进制值相互转换,因此需要两倍于它的正常大小。 通过使用这种专用URL,数据可以作为完整二进制文件传输。

  通过TSQLRest类的一些专用方法处理BLOB字段:RetrieveBlobUpdateBlob

10.2.2.2. JSON表示

  框架源代码树中提供的"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
  • 您将看到ID=1的SampleRecord的内容,编码为JSON,例如。
{"ID":1,"Time":"2010-02-08T11:07:09","Name":"AB","Question":"To be or not to be"}
  • 在地址栏输入任何其他REST命令,数据库都会响应您的请求……

  您在不到400 KB的尺寸获得了一个完整的HTTP/SQLite3 RESTful JSON服务器。

  注意,Internet Explorer或旧版本的FireFox不识别application/json; charset=UTF-8内容类型,这是这些软件的一个限制,因此上述请求将下载内容作为.json文件,但不会阻止AJAX请求按预期工作。

10.2.2.3. 无状态的ORM

  我们的框架将REST实现为无状态协议,将HTTP/1.1协议用作它的通信层。

  无状态服务是将每个请求视为独立事务的服务,与之前的任何请求无关。

  一开始,您可能会发现传统的客户端-服务端方法有点令人失望。在无状态的世界中,您永远不能确保您的客户端数据是最新的。数据唯一安全的地方是服务器。在网络世界中,这并不令人困惑。但是如果您来自富客户端背景,这可能会让您担心:是否应该从服务端编写一些同步代码,以便将所有更改复制到所有客户端。在无状态体系结构中,这不再是必需的。

  这种架构的主要规则是确保服务器是唯一引用,并且客户端能够从服务端检索任何挂起的更新。也就是说,总是修改服务器端上的记录内容,然后刷新客户机以检索修改后的值。不要直接修改客户端,而是始终通过服务器。框架的UI组件遵循这些原则。客户端修改可以执行,但必须在独立的自主表/数据库中进行。这将避免在并发客户端修改时出现任何同步问题。

10.3. REST和JSON

10.3.1. JSON格式密度

  最常见的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查询的纯响应一样,带有数组和字段名。

10.3.2. JSON(未)展开模式

  注意,JSON内容的生成根据 TSQLRestServer.NoAJAXJSON 属性有两种模式:

  1. “展开”或标准/AJAX模式,允许您从JSON内容创建纯JavaScript对象,因为每个值都提供字段名/ JavaScript对象属性名:
[{"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},{..}]
  1. “未展开”模式准确地反映了SQL请求:第一行是字段名,然后是所有字段内容:
 {"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]}

10.3.3. JSON全局缓存

  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缓存。这个缓存将在每个INSERTUPDATEDELETE SQL语句(无论对应的表是什么)上重置。

  如果您需要为特定的请求禁用JSON缓存,请在SQL语句的任何地方,例如ORM WHERE子句中,添加SQLDATABASE_NOCACHE文本,即'/*nocache*/'文本注释。它将指示TSQLDataBase不缓存返回的JSON内容。它可能有用,例如,如果您传递一个指针作为PtrInt(aVariable)绑定参数,它可能具有相同的整数值,但内容不同。

  在实践中,这种全局缓存被发现是有效的,即使它的实现有些“幼稚”。它实际上更优于其它很多客户端-服务端解决方案HTTP级缓存机制(如Squid proxy),因为我们的缓存是在SQL级,在所有CRUD/Restful查询之间共享,独立于身份验证方案,否则会污染URI。与其他级别的缓存相关联,所以框架的可伸缩性非常好。