從最先期入門時的單表操做,php
到後來接觸了 left join、right join、inner join 查詢,java
由於經費有限,須要不斷在多表查詢中折騰解決實際需求,不知道是否有過這樣的經歷?git
本文從實際開發需求講解導航屬性(ManyToOne、OneToMany、ManyToMany)的設計思路,和到底解決了什麼問題。提示:如下示例代碼使用了 FreeSql 語法,和一些僞代碼。github
FreeSql 是 .Net ORM,能支持 .NetFramework4.0+、.NetCore、Xamarin、XAUI、Blazor、以及還有說不出來的運行平臺,由於代碼綠色無依賴,支持新平臺很是簡單。目前單元測試數量:5000+,Nuget下載數量:180K+,源碼幾乎天天都有提交。值得高興的是 FreeSql 加入了 ncc 開源社區:https://github.com/dotnetcore/FreeSql,加入組織以後社區責任感更大,須要更努力作好品質,爲開源社區出一份力。sql
QQ羣:4336577(已滿)、8578575(在線)、52508226(在線)數據庫
爲何要重複造輪子?json
FreeSql 主要優點在於易用性上,基本是開箱即用,在不一樣數據庫之間切換兼容性比較好。做者花了大量的時間精力在這個項目,肯請您花半小時瞭解下項目,謝謝。功能特性以下:c#
FreeSql 使用很是簡單,只須要定義一個 IFreeSql 對象便可:數據結構
static IFreeSql fsql = new FreeSql.FreeSqlBuilder() .UseConnectionString(FreeSql.DataType.MySql, connectionString) .UseAutoSyncStructure(true) //自動同步實體結構到數據庫 .Build(); //請務一定義成 Singleton 單例模式
left join、right join、inner join 從表的外鍵看來,主要是針對一對1、多對一的查詢,好比 Topic、Type 兩個表,一個 Topic 只能屬於一個 Type:函數
select topic.*, type.name from topic inner join type on type.id = topic.typeid
查詢 topic 把 type.name 一塊兒返回,一個 type 能夠對應 N 個 topic,對於 topic 來說是 N對1,因此我命名爲 ManyToOne
在 c# 中使用實體查詢的時候,N對1 場景查詢容易,可是接收對象不方便,以下:
fsql.Select<Topic, Type>() .LeftJoin((a,b) => a.typeid == b.Id) .ToList((a,b) => new { a, b })
這樣只能返回匿名類型,除非本身再去建一個 TopicDto,可是查詢場景真的太多了,幾乎沒法窮舉 TopicDto,隨着需求的變化,後面這個 Dto 會很氾濫愈來愈多。
因而聰明的人類想到了導航屬性,在 Topic 實體內增長 Type 屬性接收返回的數據。
fsql.Select<Topic>() .LeftJoin((a,b) => a.Type.id == a.typeid) .ToList();
返回數據後,可使用 [0].Type.name 獲得分類名稱。
通過一段時間的使用,發現 InnerJoin 的條件老是在重複編寫,每次都要用大腦回憶這個條件(論頭髮怎麼掉光的)。
進化一次以後,咱們把 join 的條件作成了配置:
class Topic { public int typeid { get; set; } [Navigate(nameof(typeid))] public Type Type { get; set; } } class Type { public int id { get; set; } public string name { get; set; } }
查詢的時候變成了這樣:
fsql.Select<Topic>() .Include(a => a.Type) .ToList();
返回數據後,一樣可使用 [0].Type.name 獲得分類名稱。
[Navigate(nameof(typeid))] 理解成,Topic.typeid 與 Type.id 關聯,這裏省略了 Type.id 的配置,由於 Type.id 是主鍵(已知條件無須配置),從而達到簡化配置的效果
.Include(a => a.Type) 查詢的時候會自動轉化爲:.LeftJoin(a => a.Type.id == a.typeid)
思考:ToList 默認返回 topic.* 和 type.* 不對,由於當 Topic 下面的導航屬性有不少的時候,每次都返回全部導航屬性?
因而:ToList 的時候只會返回 Include 過的,或者使用過的 N對1 導航屬性字段。
fsql.Select<Topic>().ToList(); 返回 topic.*
fsql.Select<Topic>().Include(a => a.Type).ToList(); 返回 topic.* 和 type.*
fsql.Select<Topic>().Where(a => a.Type.name == "c#").ToList(); 返回 topic.* 和 type.*,此時不須要顯式使用 Include(a => a.Type)
fsql.Select
有了這些機制,各類複雜的 N對1,就很好查詢了,好比這樣的查詢:
fsql.Select<Tag>().Where(a => a.Parent.Parent.name == "粵語").ToList(); //該代碼產生三個 tag 表 left join 查詢。 class Tag { public int id { get; set; } public string name { get; set; } public int? parentid { get; set; } public Tag Parent { get; set; } }
是否是比本身使用 left join/inner join/right join 方便多了?
一對一 和 N對1 解決目的是同樣的,都是爲了簡化多表 join 查詢。
好比 order, order_detail 兩個表,一對一場景:
fsql.Select<order>().Include(a => a.detail).ToList(); fsql.Select<order_detail>().Include(a => a.order).ToList();
查詢的數據同樣的,只是返回的 c# 類型不同。
一對一,只是配置上有點不一樣,使用方式跟 N對1 同樣。
一對一,要求兩邊都存在目標實體屬性,而且兩邊都是使用主鍵作 Navigate。
class order { public int id { get; set; } [Navigate(nameof(id))] public order_detail detail { get; set; } } class order_detail { public int orderid { get; set; } [Navigate(nameof(orderid))] public order order { get; set; } }
1對N,和 N對1 是反過來看
topic 相對於 type 是 N對1
type 相對於 topic 是 1對N
因此,咱們在 Type 實體類中能夠定義 List<Topic> Topics { get; set; } 導航屬性
class Type { public int id { get; set; } public List<Topic> Topics { get; set; } }
1對N 導航屬性的主要優點:
把 Type.name 爲 c# java php,以及它們的 topic 查詢出來:
方法一:
fsql.Select<Type>() .IncludeMany(a => a.Topics) .Where(a => new { "c#", "java", "php" }.Contains(a.name)) .ToList();
[ { name : "c#", Topics: [ 文章列表 ] } ... ]
這種方法是從 Type 方向查詢的,很是符合使用方的數據格式要求。
最終是分兩次 SQL 查詢數據回來的,大概是:
select * from type where name in ('c#', 'java', 'php') select * from topics where typeid in (上一條SQL返回的id)
方法二:從 Topic 方向也能夠查詢出來:
fsql.Select<Topic>() .Where(a => new { "c#", "java", "php" }.Contains(a.Type.name) .ToList();
一次 SQL 查詢返回全部數據的,大概是:
select * from topic left join type on type.id = topic.typeid where type.name in ('c#', 'java', 'php')
解釋:方法一 IncludeMany 雖然是分開兩次查詢的,可是 IO 性能遠高於 方法二。方法二查詢簡單數據還行,複雜一點很容易產生大量重複 IO 數據。而且方法二返回的數據結構 List<Topic>,通常不符合使用方要求。
IncludeMany 第二次查詢 topic 的時候,如何把記錄分配到 c# java php 對應的 Type.Topics 中?
因此這個時候,配置一下導航關係就好了。
N對1,這樣配置的(從本身身上找一個字段,與目標類型主鍵關聯):
class Topic { public int typeid { get; set; } [Navigate(nameof(typeid))] public Type Type { get; set; } }
1對N,這樣配置的(從目標類型上找字段,與本身的主鍵關聯):
class Type { public int id { get; set; } [Navigate(nameof(Topic.typeid))] public List<Topic> Topics { get; set; } }
觸類旁通:
IncludeMany 級聯查詢,在實際開發中,還能夠 IncludeMany(a => a.Topics, then => then.IncludeMany(b => b.Comments))
假設,還須要把 topic 對應的 comments 也查詢出來。最多會產生三條SQL查詢:
select * from type where name in ('c#', 'java', 'php') select * from topic where typeid in (上一條SQL返回的id) select * from comment where topicid in (上一條SQL返回的id)
思考:這樣級聯查詢實際上是有缺點的,好比 c# 下面有1000篇文章,那不是都返回了?
IncludeMany(a => a.Topics.Take(10))
這樣就能解決每一個分類只返回 10 條數據了,這個功能 ef/efcore 目前作不到,直到 efcore 5.0 才支持,這多是不少人忌諱 ef 導航屬性的緣由之一吧。幾個月前我測試了 efcore 5.0 sqlite 該功能是報錯的,也許只支持 sqlserver。而 FreeSql 沒有數據庫種類限制,仍是那句話:都是親兒子!
關於 IncludeMany 還有更多功能請到 github wiki 文檔中瞭解。
實踐中發現,N對1 不適合作級聯保存。保存 Topic 的時候把 Type 信息也保存?我我的認爲自下向上保存的功能太不可控了,FreeSql 目前不支持自下向上保存。
FreeSql 支持的級聯保存,是自上向下。例如保存 Type 的時候,也同時能保存他的 Topic。
級聯保存,建議用在不過重要的功能,或者測試數據添加:
var repo = fsql.GetRepository<Type>(); repo.DbContextOptions.EnableAddOrUpdateNavigateList = true; repo.DbContextOptions.NoneParameter = true; repo.Insert(new Type { name = "c#", Topics = new List<Topic>(new[] { new Topic { ... } }) });
先添加 Type,若是他是自增,拿到自增值,向下賦給 Topics 再插入 topic。
多對可能是很常見的一種設計,如:Topic, Tag, TopicTag
class Topic { public int id { get; set; } public string title { get; set; } [Navigate(ManyToMany = typeof(TopicTag))] public List<Tag> Tags { get; set; } } public Tag { public int id { get; set; } public string name { get; set; } [Navigate(ManyToMany = typeof(TopicTag))] public List<Topic> Topics { get; set; } } public TopicTag { public int topicid { get; set; } public int tagid { get; set; } [Navigate(nameof(topicid))] public Topic Topic { get; set; } [Navigate(nameof(tagid))] public Tag Tag { get; set; } }
看着以爲複雜??看完後面查詢多麼簡單的時候,真的什麼都值了!
N對N 導航屬性的主要優點:
把 Tag.name 爲 c# java php,以及它們的 topic 查詢出來:
fsql.Select<Tag>() .IncludeMany(a => a.Topics) .Where(a => new { "c#", "java", "php" }.Contains(a.name)) .ToList();
[ { name : "c#", Topics: [ 文章列表 ] } ... ]
最終是分兩次 SQL 查詢數據回來的,大概是:
select * from tag where name in ('c#', 'java', 'php') select * from topic where id in (select topicid from topictag where tagid in(上一條SQL返回的id))
若是 Tag.name = "c#" 下面的 Topic 記錄太多,只想返回 top 10:
.IncludeMany(a => a.Topics.Take(10))
也能夠反過來查,把 Topic.Type.name 爲 c# java php 的 topic,以及它們的 Tag 查詢出來:
fsql.Select<Topic>() .IncludeMany(a => a.Tags) .Where(a => new { "c#", "java", "php" }.Contains(a.Type.name)) .ToList();
[ { title : "FreeSql 1.8.1 正式發佈", Type: { name: "c#" } Tags: [ 標籤列表 ] } ... ]
N對N 級聯查詢,跟 1對N 同樣,都是用 IncludeMany,N對N IncludeMany 也能夠繼續向下 then。
查詢 Tag.name = "c#" 的全部 topic:
fsql.Select<Topic>() .Where(a => a.Tags.AsSelect().Any(b => b.name = "c#")) .ToList();
產生的 SQL 大概是這樣的:
select * from topic where id in ( select topicid from topictag where tagid in ( select id from tag where name = 'c#' ) )
級聯保存,建議用在不過重要的功能,或者測試數據添加:
var repo = fsql.GetRepository<Topic>(); repo.DbContextOptions.EnableAddOrUpdateNavigateList = true; repo.DbContextOptions.NoneParameter = true; repo.Insert(new Topic { title = "FreeSql 1.8.1 正式發佈", Tags = new List<Tag>(new[] { new Tag { name = "c#" } }) });
插入 topic,再判斷 Tag 是否存在(若是不存在則插入 tag)。
獲得 topic.id 和 tag.id 再插入 TopicTag。
另外提供的方法 repo.SaveMany(topic實體, "Tags") 完整保存 TopicTag 數據。好比當 topic實體.Tags 屬性爲 Empty 時,刪除 topic實體 存在於 TopicTag 全部表數據。
SaveMany機制:完整保存,對比 TopicTag 表已存在的數據,計算出添加、修改、刪除執行。
父子關係,實際上是 ManyToOne、OneToMany 的綜合體,本身指向本身,經常使用於樹形結構表設計。
父子關係,除了能使用 ManyToOne、OneToMany 的使用方法外,還提供了 CTE遞歸查詢、內存遞歸組裝數據 功能。
public class Area { [Column(IsPrimary = true)] public string Code { get; set; } public string Name { get; set; } public string ParentCode { get; set; } [Navigate(nameof(ParentCode))] public Area Parent { get; set; } [Navigate(nameof(ParentCode))] public List<Area> Childs { get; set; } } var repo = fsql.GetRepository<Area>(); repo.DbContextOptions.EnableAddOrUpdateNavigateList = true; repo.DbContextOptions.NoneParameter = true; repo.Insert(new Area { Code = "100000", Name = "中國", Childs = new List<Area>(new[] { new Area { Code = "110000", Name = "北京", Childs = new List<Area>(new[] { new Area{ Code="110100", Name = "北京市" }, new Area{ Code="110101", Name = "東城區" }, }) } }) });
配置好父子屬性以後,就能夠這樣用了:
var t1 = fsql.Select<Area>().ToTreeList(); Assert.Single(t1); Assert.Equal("100000", t1[0].Code); Assert.Single(t1[0].Childs); Assert.Equal("110000", t1[0].Childs[0].Code); Assert.Equal(2, t1[0].Childs[0].Childs.Count); Assert.Equal("110100", t1[0].Childs[0].Childs[0].Code); Assert.Equal("110101", t1[0].Childs[0].Childs[1].Code);
查詢數據原本是平面的,ToTreeList 方法將返回的平面數據在內存中加工爲樹型 List 返回。
很常見的無限級分類表功能,刪除樹節點時,把子節點也處理一下。
fsql.Select<Area>() .Where(a => a.Name == "中國") .AsTreeCte() .ToDelete() .ExecuteAffrows(); //刪除 中國 下的全部記錄
若是軟刪除:
fsql.Select<Area>() .Where(a => a.Name == "中國") .AsTreeCte() .ToUpdate() .Set(a => a.IsDeleted, true) .ExecuteAffrows(); //軟刪除 中國 下的全部記錄
若不作數據冗餘的無限級分類表設計,遞歸查詢少不了,AsTreeCte 正是解決遞歸查詢的封裝,方法參數說明:
參數 | 描述 |
---|---|
(可選) pathSelector | 路徑內容選擇,能夠設置查詢返回:中國 -> 北京 -> 東城區 |
(可選) up | false(默認):由父級向子級的遞歸查詢,true:由子級向父級的遞歸查詢 |
(可選) pathSeparator | 設置 pathSelector 的鏈接符,默認:-> |
(可選) level | 設置遞歸層級 |
經過測試的數據庫:MySql8.0、SqlServer、PostgreSQL、Oracle、Sqlite、達夢、人大金倉
姿式一:AsTreeCte() + ToTreeList
var t2 = fsql.Select<Area>() .Where(a => a.Name == "中國") .AsTreeCte() //查詢 中國 下的全部記錄 .OrderBy(a => a.Code) .ToTreeList(); //非必須,也可使用 ToList(見姿式二) Assert.Single(t2); Assert.Equal("100000", t2[0].Code); Assert.Single(t2[0].Childs); Assert.Equal("110000", t2[0].Childs[0].Code); Assert.Equal(2, t2[0].Childs[0].Childs.Count); Assert.Equal("110100", t2[0].Childs[0].Childs[0].Code); Assert.Equal("110101", t2[0].Childs[0].Childs[1].Code); // WITH "as_tree_cte" // as // ( // SELECT 0 as cte_level, a."Code", a."Name", a."ParentCode" // FROM "Area" a // WHERE (a."Name" = '中國') // union all // SELECT wct1.cte_level + 1 as cte_level, wct2."Code", wct2."Name", wct2."ParentCode" // FROM "as_tree_cte" wct1 // INNER JOIN "Area" wct2 ON wct2."ParentCode" = wct1."Code" // ) // SELECT a."Code", a."Name", a."ParentCode" // FROM "as_tree_cte" a // ORDER BY a."Code"
姿式二:AsTreeCte() + ToList
var t3 = fsql.Select<Area>() .Where(a => a.Name == "中國") .AsTreeCte() .OrderBy(a => a.Code) .ToList(); Assert.Equal(4, t3.Count); Assert.Equal("100000", t3[0].Code); Assert.Equal("110000", t3[1].Code); Assert.Equal("110100", t3[2].Code); Assert.Equal("110101", t3[3].Code); //執行的 SQL 與姿式一相同
姿式三:AsTreeCte(pathSelector) + ToList
設置 pathSelector 參數後,如何返回隱藏字段?
var t4 = fsql.Select<Area>() .Where(a => a.Name == "中國") .AsTreeCte(a => a.Name + "[" + a.Code + "]") .OrderBy(a => a.Code) .ToList(a => new { item = a, level = Convert.ToInt32("a.cte_level"), path = "a.cte_path" }); Assert.Equal(4, t4.Count); Assert.Equal("100000", t4[0].item.Code); Assert.Equal("110000", t4[1].item.Code); Assert.Equal("110100", t4[2].item.Code); Assert.Equal("110101", t4[3].item.Code); Assert.Equal("中國[100000]", t4[0].path); Assert.Equal("中國[100000] -> 北京[110000]", t4[1].path); Assert.Equal("中國[100000] -> 北京[110000] -> 北京市[110100]", t4[2].path); Assert.Equal("中國[100000] -> 北京[110000] -> 東城區[110101]", t4[3].path); // WITH "as_tree_cte" // as // ( // SELECT 0 as cte_level, a."Name" || '[' || a."Code" || ']' as cte_path, a."Code", a."Name", a."ParentCode" // FROM "Area" a // WHERE (a."Name" = '中國') // union all // SELECT wct1.cte_level + 1 as cte_level, wct1.cte_path || ' -> ' || wct2."Name" || '[' || wct2."Code" || ']' as cte_path, wct2."Code", wct2."Name", wct2."ParentCode" // FROM "as_tree_cte" wct1 // INNER JOIN "Area" wct2 ON wct2."ParentCode" = wct1."Code" // ) // SELECT a."Code" as1, a."Name" as2, a."ParentCode" as5, a.cte_level as6, a.cte_path as7 // FROM "as_tree_cte" a // ORDER BY a."Code"
微軟製造了優秀的語言 c#,利用語言特性能夠作一些很是好用的功能,在 ORM 中使用導航屬性很是適合。
ManyToOne(N對1) 提供了簡單的多表 join 查詢;
OneToMany(1對N) 提供了簡單可控的級聯查詢、級聯保存功能;
ManyToMany(多對多) 提供了簡單的多對多過濾查詢、級聯查詢、級聯保存功能;
父子關係 提供了經常使用的 CTE查詢、刪除、遞歸功能;
但願正在使用的、善良的您能動一動小手指,把文章轉發一下,讓更多人知道 .NET 有這樣一個好用的 ORM 存在。謝謝了!!
FreeSql 開源協議 MIT https://github.com/dotnetcore/FreeSql,能夠商用,文檔齊全。QQ羣:4336577(已滿)、8578575(在線)、52508226(在線)
若是你有好的 ORM 實現想法,歡迎給做者留言討論,謝謝觀看!