Zongsoft.Data 是一個類 GraphQL 風格的 ORM(Object/Relational Mapping) 數據訪問框架。很高興我們的 ORM 數據訪問框架在歷經兩個 SaaS 產品的應用之後,今天正式宣佈對外推廣! ...
Zongsoft.Data 發佈公告
很高興我們的 ORM 數據訪問框架(Zongsoft.Data)在歷經兩個 SaaS 產品的應用之後,今天正式宣佈對外推廣!
這是一個類 GraphQL 風格的 ORM(Object/Relational Mapping) 數據訪問框架。
又一個輪子?
在很長時間里,.NET 陣營似乎一直缺乏一個被普遍使用的 ORM 數據訪問框架,從最早的原生 ADO.NET 到舶來品 iBatis.NET 和 Hibernate.NET,後來又經歷了 Linq for SQL 與 Entity Framework 的混戰,可能是因為 Entity Framework 早期版本的模糊定位和反覆變更的設計導致了它失之霸主之位,進而造就了一段百舸爭流、群雄共逐的戰國時代。在歷經漫長而反覆的期待、失望、糾結和痛苦之後,我終於決定動手造一個輪子。
設計理念
在開始動手之前,先確定以下基本設計原則:
- 資料庫優先(Database First)
- 嚴格的 POCO/POJO 支持
- 映射模型與代碼完全隔離
- 禁止業務層出現 SQL 和類 SQL 代碼
在一個業務系統中,數據結構及其關係毋庸置疑是最底層的基礎性結構,資料庫應由系統架構師或開發負責人進行仔細設計 (No Schema/Weakly Schema 的思潮是塗抹了蜂蜜的毒藥),數據訪問映射以資料庫表結構關係為基石,在此之上業務層亦以概念映射模型為準繩,層級之間相互隔離。
領域模型實體避免通過註解 (標簽) 來進行元數據定義,應確保嚴格符合 POCO/POJO 範式。通過語義化的 Schema 來聲明訪問的數據結構關係,禁止應用層的 SQL 和 Linq 式的類 SQL 代碼可降低業務層對數據層的依賴、提升代碼可維護性外,還具備更加統一可控的便利性,併為數據訪問引擎的實現提供了更大的優化空間和自由度。
範例說明
下麵通過三個的例子 (註:例子均基於 Zongsoft.Community 項目) 來佐證上面的部分設計理念,更多示例和闡述請參考 Zongsoft.Data 項目的 README.md 文檔和 Zongsoft.Community 項目的代碼。
提示: 下麵的範例均基於 Zongsoft.Community 開源項目,該項目是一個完整的論壇社區的後臺程式。你可能需要預先閱讀一下該項目的《資料庫表結構設計》文檔,以便更好的理解範例代碼的業務邏輯。
示例一
導航查詢及導航過濾
var forums = this.DataAccess.Select<Forum>(
Condition.Equal("SiteId", this.User.SiteId) &
Condition.In("Visibility", Visibility.Internal, Visibility.Public) |
(
Condition.Equal("Visibility", Visibility.Specified) &
Condition.Exists("Users",
Condition.Equal("UserId", this.User.UserId) &
(
Condition.Equal("IsModerator", true) |
Condition.NotEqual("Permission", Permission.None)
)
)
),
"*, MostRecentThread{ThreadId,Title,Creator{Name,Nickname,Avatar}}"
);
上述數據訪問的查詢方法大致生成如下SQL腳本:
SELECT
t.*,
t1.ThreadId AS 'MostRecentThread.ThreadId',
t1.Title AS 'MostRecentThread.Title',
t1.CreatorId AS 'MostRecentThread.CreatorId',
t2.UserId AS 'MostRecentThread.Creator.UserId',
t2.Name AS 'MostRecentThread.Creator.Name',
t2.Nickname AS 'MostRecentThread.Creator.Nickname',
t2.Avatar AS 'MostRecentThread.Creator.Avatar'
FROM Forum t
LEFT JOIN Thread AS t1 ON
t.MostRecentThreadId=t1.ThreadId
LEFT JOIN UserProfile AS t2 ON
t1.CreatorId=t2.UserId
WHERE
t.SiteId = @p1 AND
t.Visibility IN (@p2, @p3) OR
(
t.Visibility = @p4 AND
EXISTS
(
SELECT u.SiteId, u.ForumId, u.UserId
FROM ForumUser u
WHERE u.SiteId = t.SiteId AND
u.ForumId = t.ForumId AND
u.UserId = @p5 AND
(
u.IsModerator = @p6 OR
u.Permission != @p7
)
)
);
上述示例通過
Select
查詢方法的schema
參數 (即值為*, MostRecentThread{ThreadId,Title,Creator{Name,Nickname,Avatar}}
的參數) 從數據結構關係的層次指定了查詢數據的形狀,因而不再需要 SQL 或類 SQL 語法中 JOIN 這樣命令式的語法元素,它不光提供了更簡潔且語義化的 API 訪問方式,而且還給數據訪問引擎底層提供了更大的優化空間和自由度。如果將
Select
查詢方法的schema
參數值改為*,Moderators{*},MostRecentThread{ThreadId,Title,Creator{Name,Nickname,Avatar}}
後,數據訪問引擎會將查詢內部分解為一對多的兩條 SQL 語句進行迭代執行,而這些都不需要業務層進行分拆處理,因而提升了效率並降低了業務層的複雜度。註: 將 Schema 模式表達式通過 Web API 提供給前端應用,將大大減少後端開發的工作量,提升前後端的工作效率。
示例二
一對多的關聯新增
// 構建待新增的實體對象
var forum = new
{
SiteId = this.User.SiteId,
GroupId = 100,
Name = "xxxx",
// 一對多的導航屬性
Users = new ForumUser[]
{
new ForumUser { UserId = 1001, IsModerator = true },
new ForumUser { UserId = 1002, Permission = Permission.Read },
new ForumUser { UserId = 1003, Permission = Permission.Write },
}
}
// 執行數據新增操作
this.DataAccess.Insert<Forum>(forum, "*, Users{*}");
上述數據訪問的新增方法大致生成如下SQL腳本:
/* 主表插入語句,執行一次 */
INSERT INTO Forum (SiteId,ForumId,GroupId,Name,...) VALUES (@p1,@p2,@p3,@p4,...);
/* 子表插入語句,執行多次 */
INSERT INTO ForumUser (SiteId,ForumId,UserId,Permission,IsModerator) VALUES (@p1,@p2,@p3,@p4,@p5);
上述示例通過
Insert
新增方法的schema
參數(即值為*,User{*}
的參數)指定了新增數據的形狀,由數據訪問引擎根據映射定義自動處理底層的 SQL 執行方式,確保業務層代碼的簡潔和更高的執行效率。
示例三
一對一和一對多的關聯更新,對於“一對多”的導航屬性,還能確保該屬性值 (集合類型) 以 UPSERT 模式寫入。
public bool Approve(ulong threadId)
{
//構建更新的條件
var criteria =
Condition.Equal(nameof(Thread.ThreadId), threadId) &
Condition.Equal(nameof(Thread.Approved), false) &
Condition.Equal(nameof(Thread.SiteId), this.User.SiteId) &
Condition.Exists("Forum.Users",
Condition.Equal(nameof(Forum.ForumUser.UserId), this.User.UserId) &
Condition.Equal(nameof(Forum.ForumUser.IsModerator), true));
//執行數據更新操作
return this.DataAccess.Update<Thread>(new
{
Approved = true,
ApprovedTime = DateTime.Now,
Post = new
{
Approved = true,
}
}, criteria, "*,Post{Approved}") > 0;
}
上述數據訪問的更新方法大致生成如下SQL腳本:
/* 以下代碼為支持 OUTPUT/RETURNING 子句的資料庫(如:SQLServer,Oracle,PostgreSQL) */
/* 根據更新的關聯鍵創建臨時表 */
CREATE TABLE #TMP
(
PostId bigint NOT NULL
);
/* 更新主表,並將更新的關聯鍵輸出到記憶體臨時表 */
UPDATE T SET
T.[Approved]=@p1,
T.[ApprovedTime]=@p2
OUTPUT DELETED.PostId INTO #TMP
FROM [Community_Thread] AS T
LEFT JOIN [Community_Forum] AS T1 ON /* Forum */
T1.[SiteId]=T.[SiteId] AND
T1.[ForumId]=T.[ForumId]
WHERE
T.[ThreadId]=@p3 AND
T.[Approved]=@p4 AND
T.[SiteId]=@p5 AND EXISTS (
SELECT [SiteId],[ForumId]
FROM [Community_ForumUser]
WHERE [SiteId]=T1.[SiteId] AND
[ForumId]=T1.[ForumId] AND
[UserId]=@p6 AND
[IsModerator]=@p7
);
/* 更新關聯表 */
UPDATE T SET
T.[Approved]=@p1
FROM [Community_Post] AS T
WHERE EXISTS (
SELECT [PostId]
FROM #TMP
WHERE [PostId]=T.[PostId]);
上述示例通過
Update
更新方法的schema
參數(即值為*,Post{Approved}
的參數)指定了更新數據的形狀,數據訪問引擎將根據資料庫類型生成高效的 SQL 語句,對於業務層而言這一切都是無感的、透明的。對於一對多的導航屬性,數據訪問引擎預設將以 UPSERT 模式處理子集的寫入,關於 UPSERT 更多信息請參考 Zongsoft.Data 項目文檔。
性能
我們希望提供最佳的綜合性價比,對於一個 ORM 數據訪問引擎來說,性能的關註點主要 (不限) 有這些要素:
- 生成簡潔高效的 SQL 腳本,並儘可能利用特定資料庫的最新 SQL 語法;
- 數據查詢結果的實體組裝(Populate)過程必須高效;
- 避免反射,有效的語法樹緩存。
實現層面我們採用 Emitting 動態編譯技術對實體組裝(Populate)、數據參數綁定等進行預熱處理,可查閱 DataPopulator 等相關類的源碼深入瞭解。
其他
得益於 “以聲明方式來表達數據結構關係” 的語義化設計理念,相對於命令式設計而言,它使得程式意圖更加聚焦,天然地對底層數據的表達和優化更加寬容與自由。
更多詳細內容 (譬如:讀寫分離、繼承表、數據模式、映射文件、過濾器、驗證器、類型轉換、數據隔離) 請查閱相關文檔。
支持贊助
我們歡迎並期待任何形式的推廣支持!
如果你認同我們的設計理念請為這個項目點贊(Star),如果你認為該項目很有用,並且希望支持它未來的發展,請給予必要的資金來支持它:
- 關註 Zongsoft 微信公眾號,對我們的文章進行打賞;
- 加入 Zongsoft 知識星球圈,可以獲得線上問答和技術支持;
- 如果您的企業需要現場技術支持與輔導,又或者需要開發新功能、即刻的錯誤修複等請發郵件給我。
提醒: 本文可能會更新,請閱讀原文:http://zongsoft.com/blog/zh-cn/zongsoft/announcing-data-engine,以避免因內容陳舊而導致的謬誤,同時亦有更好的閱讀體驗。