mORMot 1.18 第11章 JSON - JavaScript对象表示法

mORMot 1.18 第11章 JSON - JavaScript对象表示法

JSON是一种用于指定数据结构和数组的行业标准格式。(它是ECMA 404的一个子集。)虽然它最初是在JavaScript语言中定义的,但由于以下原因,它已成为一种流行的互联网格式,用于指定和交换数据:

  • 它很紧凑,使用的数据字节比其他大多数格式都少
  • 当增加足够的空格时,人类可以很容易地阅读
  • 它解析效率高,因此可以非常快速地完成解析

其他替代方案是通用的XML(在REST/HTTP中是JSON的替代方案),或ISO OSI网络模型中的ASN.1/BER(用于LDAP、SNMP和其他一些互联网和OSI协议)。

例如,考虑一个名字数组的JSON。

{
	"employees": [{
			"firstName": "John",
			"lastName": "Doe"
		},
		{
			"firstName": "Anna",
			"lastName": "Smith"
		},
		{
			"firstName": "Peter",
			"lastName": "Jones"
		}
	]
}

使用XML的替代方案是:

<employees>
    <employee>
        <firstName>John</firstName>
        <lastName>Doe</lastName>
    </employee>
    <employee>
        <firstName>Anna</firstName>
        <lastName>Smith</lastName>
    </employee>
    <employee>
        <firstName>Peter</firstName>
        <lastName>Jones</lastName>
    </employee>
</employees>

显然,JSON版本占用的空间更少,而且许多人发现它更具可读性。

一种名为BSON的二进制编码版本因成功的MongoDB NoSQL数据库而流行。它只是计算机优化的JSON版本。

mORMot具有非常快的JSON编码和解码功能。针对服务器的性能,Windows和Linux在Intel版本上进行了手动优化。开放平台版本是高效的Pascal。

JSON/BSON有许多用途。例如,您可能偶尔需要扩展一个字段以容纳比设计时设想的数据类型更多的数据。只要数据在范围内,就可以使用JSON在任何这样的字段中存储任何对象。

一些数据库,特别是PostgreSQL和SQLite3,允许几乎任何大小的数据都适合在文本字段中。它们没有强加像varchar(3)这样的定义所隐含的严格限制。

MongoDB以JSON/BSON格式存储数据属性。我们将在NoSQL章节中介绍这一点。

在REST/HTTP/S协议中,以及通常在HTML5/JavaScript页面使用AJAX(异步JavaScript交换)时,JSON数据会在客户端和服务器之间发送。

它也是一种将数据临时保存到文件中以与其他程序交换的优秀方式,是现代CSV(逗号分隔值)导入系统的替代方案。

11.1 与JSON之间的转换

在像类这样的对象和JSON之间转换的过程被称为序列化。为此,我们将使用TDocVariant,这是一种Delphi Variant类型。

TDocVariants由以下几部分组成:

  • 名称/值对
  • 值可以是任何类型,包括:
    • 值(dvObject子类型)
    • 数组(dvArray子类型)
    • 嵌套的TDocVariant(TDocVariant类型)

JSON的美妙之处在于它可以存储任何动态值对象的内容,你不需要坚持使用预定义的模式。你的对象可以嵌套到任何深度(受可用内存限制)。

赋值可以通过值或通过引用来进行。按值是默认的,也是最安全的,但当运行时必须复制一个大型JSON变量中的所有数据记录时,它的速度会较慢。按引用是最快的选择,并且可以立即进行引用计数赋值。

通过后期绑定在代码中访问属性几乎没有速度损失。此外,序列化和反序列化速度非常快,占用的内存也非常少。

与TDynArray动态数组包装器集成,就像记录序列化一样。

这将在后面进行描述。

任何包含作为已发布属性的变体自定义类型的TSQLRecord实例都将被mORMot的核心识别,并会自动正确地将所有支持的数据库序列化为JSON。数据将存储在文本列中,而不是作为BLOB存储。

此外,任何基于接口的SOA服务都能够使用或发布变体内容。

最后,变体实例与Delphi的IDE完全集成。当你在IDE调试器中显示一个变体时,它将显示为JSON。

在你的程序中使用TDocVariants有两种方式:

  • 作为常规的变体变量,然后使用后期绑定,或者更快的_Safe()来访问数据。
  • 作为TDocVariants,然后返回一个带有variant(sampledocvariant)的实例。

以下是一个示例:

var
  V: variant;
...
  TDocVariant.New(V); // 或者稍微慢一点的 V := TDocVariant.New;
  V.name := 'John';
  V.year := 1972;
  // 现在V包含 {"name":"john","year":1982}
  writeln(V);

var
  V1, V2: variant; // 作为任何变体存储
...
  V1 := _Obj(['name', 'John', 'year', 1972]);
  V2 := _Obj(['name', 'John', 'doc', _Obj(['one', 1, 'two', 2.5])]); // 包含嵌套对象

然后,您可以通过两种方式将这些对象转换为JSON:

  • 使用VariantSaveJson()函数,它直接返回一个UTF-8内容。这是快速的方法。
  • 将变体实例转换为字符串。这种方法较慢,但有效。
writeln(VariantSaveJson(V1));// 显式转换为RawUTF8
writeln(V1); // 从变体隐式转换为字符串
// 这两个命令都将写入'"name":"john","year":1982

writeln(VariantSaveJson(V2)); // 显式转换为RawUTF8
writeln(V2); // 从变体隐式转换为字符串
// 这两个命令都将写入'{”name”:”john”,”doc”:{”one”:1,”two”:2.5}}

在服务器代码中,您可能希望使用更快的方法,但如果您忘记在客户端中使用它,可能不会有太大的区别。

请记住,键名是在运行时确定的,因此如果您在键名上打错字,很可能会收到错误。

您可以使用Exists方法来测试键的存在:


If not V1.Exists('name') then
  V1.name := 'John';

请注意,您可以通过分配或重新分配值来轻松替换任何值。


V1.name := 'Joe';
V1.name := 'Joclyn';

您还可以使用V1.ToJSON进行反序列化,并且可以使用V1.Delete(keyname)释放/删除/删除元素。

可以定义数组:


V1 := _Arr(['John','Mark','Luke']);
V2 := _Obj(['name','John','array', _Arr(['one','two',2.5])]); // 作为嵌套数组
// _Arr() 较慢,适用于客户端,_FastArr() 是服务器的选项。

如果您已经了解JSON,那么有一种高效的方法可以生成TDocVariants。


var
  V1,V2,V3,V4: variant; // 存储为任何变体
...
  V1 := _Json('{"name":"john","year":1982}'); // 严格的JSON语法
  V2 := _Json('{"name:"john",year:1982}');    // MongoDB扩展语法用于名称
  V3 := _Json('{"name":?,"year":?}',[],['john',1982]);
  V4 := _JsonFmt('{%:?,%:?}',['name','year'],['john',1982]);

  writeln(VariantSaveJSON(V1));
  writeln(VariantSaveJSON(V2));
  writeln(VariantSaveJSON(V3));
  writeln( V4 );

所有这四个都会写入 { "name": "john", "year" : 1982 }

V3和V4的标记演示了变量的传递。

11.2 复制变体

通过_Obj()、_Arr()、_JSON()和_JSONFmt()创建的变体通常具有按值复制的模式,这意味着会将数据的副本放置在新变量中。这有两个主要影响:

  • 性能较慢,尤其是当变体很大时
  • 对变体副本所做的更改不会反映到实际对象上

例如:

V1 := _Obj(['name', 'John', 'year', 1973]);
V2 := V1;
V2.name := 'Josh';
Writeln(V1.name, V2.name);

这将同时显示John和Josh,因为变量是解耦的。

这四个函数还有一个可选的第二个参数dvoValueCopiedByReference,它将改变上述程序的输出,以反映相同的耦合变量。

例如:

V1 := _Obj(['name', 'John', 'year', 1973], [dvoValueCopiedByReference]);
V2 := V1;
V2.name := 'Josh';
Writeln(V1.name, V2.name);

结果将是John和Josh。

_ObjFast、_ArrFast()、_JSONFast和_JSONFmtFast这四个函数只是调用同名函数的别名,但设置了dvoValueCopiedByReference参数。

实际上,在典型的Delphi编程中,当使用TObject后代时,您是通过引用来传递数据的,因此这应该是熟悉的领域。

您可以随时将TDocVariant更改为以下两种方式之一:

  • 通过引用使用,调用_UniqueFast(variable)
  • 通过值使用,调用_Unique(variable)

11.3 TDocVariant的用途

您会惊讶于TDocVariants的频繁使用。当您无法使用类或记录,因为您在设计时不知道所有字段时,它们非常有用。

mORMot将在任何定义了变体属性的TSQLRecord派生类中支持TDocVariants;并且它会自动将结果作为JSON存储在数据库的文本字段中。

mORMot在像NoSQL和日志记录这样的情况下使用TDocVariants,以及在所有可能的记录类型未预先定义且无法预先定义的情况下使用。TDocVarient为Delphi带来了一个无模式类,这更类似于Python或JavaScript等后期绑定语言。

TDocVariant是为HTML/JavaScript页面的AJAX查询提供JSON的自然方式。

11.4 数据分片

有时将JSON对象存储在文本字段中是有效的,这被称为分片。


type

  TSQLRecordData = class(TSQLRecord)
  private
    fName: RawUTF8;
    fData: variant;
  publishes 
    property Name: RawUTF8 read fTest write fTest stored AS_UNIQUE;
    property Data: variant read } fData write fData;
  end;

此记录中有三个字段:唯一ID、唯一名称和Data。


var

  aRec: TSQLRecordData;
  aID: TID;

begin

  // 初始化一个记录
  aRec := TSQLRecordData.Create;
  aRec.Name := 'Joe'; // 一个唯一键
  aRec.Data := _JSONFast('{name:"Joe",age:30}');

  // 创建一个TDocVariant
  // 或者我们可以使用这种重载的构造函数来处理简单字段
  aRec := TSQLRecordData.Create(['Joe', _ObjFast(['name', 'Joe', 'age', 30])]);

  // 现在我们可以处理数据,例如通过后期绑定:
  writeln(aRec.Name); // 将输出 'Joe'
  writeln(aRec.Data); // 将输出 '{"name":"Joe","age":30\}'

  // (自动转换为JSON字符串)}
  aRec.Data.age := aRec.Data.age + 1; // 年龄增加一岁
  aRec.Data.interests := 'football'; // 向模式添加属性
  aID := aClient.Add(aRec, true); // 将存储{"name":"Joe","age":31,"interests":"football"}
  aRec.Free;

  // 现在我们可以通过aID创建的整数或通过Name='Joe'来检索数据

end;

在这里,我们已经将JSON数据存储在Data字段中。以下SQL函数可以从JSON描述的对象中返回属性。

JsonGet函数 描述
JsonGet(ArrColumn,0) 从JSON数组中按索引返回属性值
JsonGet(ObjColumn,'PropName') 从JSON对象中按名称返回属性值
JsonGet(ObjColumn,'Obj1.Obj2.Prop') 通过路径(包括嵌套的JSON对象)返回属性值
JsonGet(ObjColumn,'Prop1,Prop2') 从JSON对象中提取按名称指定的属性
JsonGet(ObjColumn,'Prop1,Obj1.Prop') 从JSON对象中提取按名称(包括嵌套的JSON对象)指定的属性
JsonGet(ObjColumn,'Prop*') 从JSON对象中提取按通配符名称指定的属性
JsonGet(ObjColumn,'Prop,Obj1.P') 从JSON对象中提取按通配符名称(包括嵌套的JSON对象)指定的属性

例如:


JsonGet(ObjColumn,'owner') = {"login":"smith","id":123456} 作为文本
JsonGet(ObjColumn,'owner.login') = "smith" 作为文本
JsonGet(ObjColumn,'owner.id') = 123456 作为整数
JsonGet(ObjColumn,'owner.name') = NULL
JsonGet(ObjColumn,'owner.login,owner.id') ={"owner.login":"smith","owner.id":123456} 作为文本
JsonGet(ObjColumn,'owner.I*') = {"owner.id":123456} 作为文本
JsonGet(ObjColumn,'owner.*') = {"owner.login":"smith","owner.id":123456} 作为文本
JsonGet(ObjColumn,'unknown.*') = NULL 

// 使用JsonHas返回True或False
JsonHas(ObjColumn,'owner') = true
JsonHas(ObjColumn,'owner.login') = true
JsonHas(ObjColumn,'owner.name') = false
JsonHas(ObjColumn,'owner.i*') = true
JsonHas(ObjColumn,'owner.n*') = false

热门相关:我的女友不可能是怪物   学霸,你女朋友掉了   贩罪   血战天下   重生全能悍妻:张狂大小姐