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
注意:本文由hieroly翻譯於2024年04月26日