通常的ORM實現基於配置或註釋,由反射或Emit生成相應的Sql語句,然後將Sql發送給數據庫解析Sql字符串生成AST再交給優化器處理後執行,返回的數據再經由反射或Emit轉換爲相應的實體實例。作者認爲上述方式主要存在以下兩個問題:
- 實體類代碼是硬編碼的,如果實體類定義變更必須重新編譯應用再部署,不利於實現運行時動態變更實體定義;
- CRUD操作轉換爲Sql的實現複雜,且需要針對不同的數據庫做適配優化。
由於作者追求極致簡單的系統架構以及絲般順滑的開發體驗,所以作者採用了另類的方式在框架內實現了ORM,之於另類在什麼地方我們先通過一些簡單示例後再來說明一下實現原理。
一、CRUD操作
還是用系統自帶的Emploee模型作爲示例,在IDE新建服務模型添加以下代碼保存發佈後可通過主菜單->Service->Invoke運行測試:
//事務新建兩條記錄
var emp1 = new Entities.Emploee();
emp1.Name = "Rick";
emp1.Account = "[email protected]";
emp1.Birthday = new DateTime(1977, 3, 16);
var emp2 = new Entities.Emploee();
emp2.Name = "Johne";
emp2.Account = "[email protected]"
emp2.Birthday = new DateTime(1979, 1, 2);
var txn = await Transaction.BeginAsync();
try {
await EntityStore.SaveAsync(emp1, txn);
await EntityStore.SaveAsync(emp2, txn);
await txn.CommitAsync();
} catch (Exception ex) {
txn.Rollback();
}
//查詢記錄
var q = new TableScan<Entities.Emploee>();
q.Filter(t => t.Name == "Rick");
var emps = await q.ToListAsync();
//更新記錄
emps[0].Name = "Rick Lu";
await EntityStore.SaveAsync(emps[0]);
//刪除記錄
await EntityStore.DeleteAsync(emps[0]);
以上只是已實現的一些Api示例,複雜的如根據索引查詢、聚合查詢等Api正在設計開發中。
二、實現原理
設計時:
這部分實現重度依賴Roslyn功能,服務端在開發人員登錄至IDE後會通過Roslyn生成虛擬項目。
- 實體模型保存時服務端生成虛擬的實體類代碼,並加入虛擬項目內;
- 服務模型保存時服務端生成虛擬的服務類代碼,並加入虛擬項目內;發佈時服務端的編譯引擎解析虛擬的服務代碼,然後轉換爲運行時服務代碼,再通過Roslyn編譯爲動態服務組件。其中虛擬代碼轉換爲運行時代碼的過程主要是:
- 將實體屬性取賦值操作(eg: entity.Name = "aa" 或 var temp = entity.Name)轉換爲針對運行時Entity類的GetXXX()及SetXXX()操作。這裏需要注意的是運行時只存在一個Entity類(類似於KVO通過字典表保存屬性值);
- 將查詢條件轉換爲可序列化的表達式,這樣存儲引擎執行查詢命令時可委託clr emit生成過濾指令,存儲引擎在掃描時直接使用過濾指令計算滿足條件的記錄。
運行時:
這部分實現參考以下流程圖,需要注意的是存儲引擎是基於RocksDB的,實體數據轉換爲KV數據(如Key=Id, Value=[字段標識:字段值;字段標識:字段值]),這樣轉換過程就不需要使用反射或Emit。
三、性能測試
作者做了簡單的性能測試(單節點I74C8G虛擬機):
- 併發插入不帶索引不帶外鍵的簡單實體約28000tps;
- 通過惟一索引查詢約80000qps;
- 帶條件掃描少量記錄約35000qps。
四、查詢限制
- 存儲引擎不支持join,可通過分別查詢出數據後利用Linq做join操作;
- 存儲引擎暫只支持根據Entity.Id或指定索引查詢排序,不支持自定義排序。
五、本篇小結
本篇主要介紹了框架集成的ORM的另類實現,Github上的運行時已經更新可測試。如果您有問題或Bug報告,請留言或在Github提交Issue。