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