AppBoxFuture(八): 另類的ORM實現
阿新 • • 發佈:2019-05-31
通常的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