.NET ORM 導(dǎo)航屬性可以解決什么問題撒蟀?

寫在開頭

從最早期入門時(shí)的單表操作,

到后來接觸了 left join棒口、right join、inner join 查詢辜膝,

因?yàn)榻?jīng)費(fèi)有限无牵,需要不斷在多表查詢中折騰解決實(shí)際需求,不知道是否有過這樣的經(jīng)歷内舟?

本文從實(shí)際開發(fā)需求講解導(dǎo)航屬性(ManyToOne合敦、OneToMany、ManyToMany)的設(shè)計(jì)思路验游,和到底解決了什么問題充岛。提示:以下示例代碼使用了 FreeSql 語法,和一些偽代碼耕蝉。


入戲準(zhǔn)備

FreeSql 是 .Net ORM崔梗,能支持 .NetFramework4.0+、.NetCore垒在、Xamarin蒜魄、XAUI、Blazor场躯、以及還有說不出來的運(yùn)行平臺(tái)谈为,因?yàn)榇a綠色無依賴,支持新平臺(tái)非常簡(jiǎn)單踢关。目前單元測(cè)試數(shù)量:5000+伞鲫,Nuget下載數(shù)量:180K+,源碼幾乎每天都有提交签舞。值得高興的是 FreeSql 加入了 ncc 開源社區(qū):https://github.com/dotnetcore/FreeSql秕脓,加入組織之后社區(qū)責(zé)任感更大柒瓣,需要更努力做好品質(zhì),為開源社區(qū)出一份力吠架。

QQ群:4336577(已滿)芙贫、8578575(在線)、52508226(在線)

為什么要重復(fù)造輪子傍药?

image

FreeSql 主要優(yōu)勢(shì)在于易用性上磺平,基本是開箱即用,在不同數(shù)據(jù)庫(kù)之間切換兼容性比較好拐辽。作者花了大量的時(shí)間精力在這個(gè)項(xiàng)目褪秀,肯請(qǐng)您花半小時(shí)了解下項(xiàng)目,謝謝薛训。功能特性如下:

  • 支持 CodeFirst 對(duì)比結(jié)構(gòu)變化遷移;
  • 支持 DbFirst 從數(shù)據(jù)庫(kù)導(dǎo)入實(shí)體類仑氛;
  • 支持 豐富的表達(dá)式函數(shù)乙埃,自定義解析;
  • 支持 批量添加锯岖、批量更新介袜、BulkCopy;
  • 支持 導(dǎo)航屬性出吹,貪婪加載遇伞、延時(shí)加載、級(jí)聯(lián)保存捶牢;
  • 支持 讀寫分離鸠珠、分表分庫(kù),租戶設(shè)計(jì)秋麸;
  • 支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/達(dá)夢(mèng)/神通/人大金倉(cāng)/MsAccess渐排;

FreeSql 使用非常簡(jiǎn)單,只需要定義一個(gè) IFreeSql 對(duì)象即可:

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.MySql, connectionString)
    .UseAutoSyncStructure(true) //自動(dòng)同步實(shí)體結(jié)構(gòu)到數(shù)據(jù)庫(kù)
    .Build(); //請(qǐng)務(wù)必定義成 Singleton 單例模式

ManyToOne 多對(duì)一

left join灸蟆、right join驯耻、inner join 從表的外鍵看來,主要是針對(duì)一對(duì)一炒考、多對(duì)一的查詢可缚,比如 Topic、Type 兩個(gè)表斋枢,一個(gè) Topic 只能屬于一個(gè) Type:

select
topic.*, type.name
from topic
inner join type on type.id = topic.typeid

查詢 topic 把 type.name 一起返回帘靡,一個(gè) type 可以對(duì)應(yīng) N 個(gè) topic,對(duì)于 topic 來講是 N對(duì)1杏慰,所以我命名為 ManyToOne

在 c# 中使用實(shí)體查詢的時(shí)候测柠,N對(duì)1 場(chǎng)景查詢?nèi)菀琢毒希墙邮諏?duì)象不方便,如下:

fsql.Select<Topic, Type>()
  .LeftJoin((a,b) => a.typeid == b.Id)
  .ToList((a,b) => new { a, b })

這樣只能返回匿名類型轰胁,除非自己再去建一個(gè) TopicDto谒主,但是查詢場(chǎng)景真的太多了,幾乎無法窮舉 TopicDto赃阀,隨著需求的變化霎肯,后面這個(gè) Dto 會(huì)很泛濫越來越多。

image

于是聰明的人類想到了導(dǎo)航屬性榛斯,在 Topic 實(shí)體內(nèi)增加 Type 屬性接收返回的數(shù)據(jù)观游。

fsql.Select<Topic>()
   .LeftJoin((a,b) => a.Type.id == a.typeid)
   .ToList();

返回?cái)?shù)據(jù)后,可以使用 [0].Type.name 得到分類名稱驮俗。

經(jīng)過一段時(shí)間的使用懂缕,發(fā)現(xiàn) InnerJoin 的條件總是在重復(fù)編寫,每次都要用大腦回憶這個(gè)條件(論頭發(fā)怎么掉光的)王凑。

image

進(jìn)化一次之后搪柑,我們把 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; }
}

查詢的時(shí)候變成了這樣:

fsql.Select<Topic>()
   .Include(a => a.Type)
   .ToList();

返回?cái)?shù)據(jù)后,同樣可以使用 [0].Type.name 得到分類名稱索烹。

  • [Navigate(nameof(typeid))] 理解成工碾,Topic.typeid 與 Type.id 關(guān)聯(lián),這里省略了 Type.id 的配置百姓,因?yàn)?Type.id 是主鍵(已知條件無須配置)渊额,從而達(dá)到簡(jiǎn)化配置的效果

  • .Include(a => a.Type) 查詢的時(shí)候會(huì)自動(dòng)轉(zhuǎn)化為:.LeftJoin(a => a.Type.id == a.typeid)


思考:ToList 默認(rèn)返回 topic.* 和 type.* 不對(duì),因?yàn)楫?dāng) Topic 下面的導(dǎo)航屬性有很多的時(shí)候垒拢,每次都返回所有導(dǎo)航屬性旬迹?

于是:ToList 的時(shí)候只會(huì)返回 Include 過的,或者使用過的 N對(duì)1 導(dǎo)航屬性字段求类。

  • 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.*舱权,此時(shí)不需要顯式使用 Include(a => a.Type)

  • fsql.Select<Topic>().ToList(a => new { Topic = a, TypeName = a.Type.name }); 返回 topic.* 和 type.name


有了這些機(jī)制,各種復(fù)雜的 N對(duì)1仑嗅,就很好查詢了宴倍,比如這樣的查詢:

fsql.Select<Tag>().Where(a => a.Parent.Parent.name == "粵語").ToList();
//該代碼產(chǎn)生三個(gè) 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 方便多了仓技?


OneToOne 一對(duì)一

一對(duì)一 和 N對(duì)1 解決目的是一樣的鸵贬,都是為了簡(jiǎn)化多表 join 查詢。

比如 order, order_detail 兩個(gè)表脖捻,一對(duì)一場(chǎng)景:

fsql.Select<order>().Include(a => a.detail).ToList();

fsql.Select<order_detail>().Include(a => a.order).ToList();

查詢的數(shù)據(jù)一樣的阔逼,只是返回的 c# 類型不一樣。

一對(duì)一地沮,只是配置上有點(diǎn)不同嗜浮,使用方式跟 N對(duì)1 一樣羡亩。

一對(duì)一,要求兩邊都存在目標(biāo)實(shí)體屬性危融,并且兩邊都是使用主鍵做 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; }
}
image

OneToMany 一對(duì)多

1對(duì)N,和 N對(duì)1 是反過來看

topic 相對(duì)于 type 是 N對(duì)1

type 相對(duì)于 topic 是 1對(duì)N

image

所以吉殃,我們?cè)?Type 實(shí)體類中可以定義 List<Topic> Topics { get; set; } 導(dǎo)航屬性

class Type
{
    public int id { get; set; }
    public List<Topic> Topics { get; set; }
}

1對(duì)N 導(dǎo)航屬性的主要優(yōu)勢(shì):

  • 查詢 Type 的時(shí)候可以把 topic 一起查詢出來辞居,并且還是用 Type 作為返回類型。
  • 添加 Type 的時(shí)候蛋勺,把 Topics 一起添加
  • 更新 Type 的時(shí)候瓦灶,把 Topics 一起更新
  • 刪除 Type 的時(shí)候,沒動(dòng)作( ef 那邊是用數(shù)據(jù)庫(kù)外鍵功能刪除子表記錄的)

OneToMany 級(jí)聯(liá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 方向查詢的贼陶,非常符合使用方的數(shù)據(jù)格式要求。

最終是分兩次 SQL 查詢數(shù)據(jù)回來的巧娱,大概是:

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 查詢返回所有數(shù)據(jù)的每界,大概是:

select * from topic
left join type on type.id = topic.typeid
where type.name in ('c#', 'java', 'php')
image

解釋:方法一 IncludeMany 雖然是分開兩次查詢的,但是 IO 性能遠(yuǎn)高于 方法二家卖。方法二查詢簡(jiǎn)單數(shù)據(jù)還行,復(fù)雜一點(diǎn)很容易產(chǎn)生大量重復(fù) IO 數(shù)據(jù)庙楚。并且方法二返回的數(shù)據(jù)結(jié)構(gòu) List<Topic>上荡,一般不符合使用方要求。

IncludeMany 第二次查詢 topic 的時(shí)候馒闷,如何把記錄分配到 c# java php 對(duì)應(yīng)的 Type.Topics 中酪捡?

所以這個(gè)時(shí)候,配置一下導(dǎo)航關(guān)系就行了纳账。

N對(duì)1逛薇,這樣配置的(從自己身上找一個(gè)字段,與目標(biāo)類型主鍵關(guān)聯(lián)):

class Topic
{
    public int typeid { get; set; }
    [Navigate(nameof(typeid))]
    public Type Type { get; set; }
}

1對(duì)N疏虫,這樣配置的(從目標(biāo)類型上找字段永罚,與自己的主鍵關(guān)聯(lián)):

class Type
{
    public int id { get; set; }
    [Navigate(nameof(Topic.typeid))]
    public List<Topic> Topics { get; set; }
}

舉一反三:

IncludeMany 級(jí)聯(lián)查詢,在實(shí)際開發(fā)中卧秘,還可以 IncludeMany(a => a.Topics, then => then.IncludeMany(b => b.Comments))

假設(shè)呢袱,還需要把 topic 對(duì)應(yīng)的 comments 也查詢出來。最多會(huì)產(chǎn)生三條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)

思考:這樣級(jí)聯(lián)查詢其實(shí)是有缺點(diǎn)的翅敌,比如 c# 下面有1000篇文章羞福,那不是都返回了?

IncludeMany(a => a.Topics.Take(10))

這樣就能解決每個(gè)分類只返回 10 條數(shù)據(jù)了蚯涮,這個(gè)功能 ef/efcore 目前做不到治专,直到 efcore 5.0 才支持卖陵,這可能是很多人忌諱 ef 導(dǎo)航屬性的原因之一吧。幾個(gè)月前我測(cè)試了 efcore 5.0 sqlite 該功能是報(bào)錯(cuò)的张峰,也許只支持 sqlserver泪蔫。而 FreeSql 沒有數(shù)據(jù)庫(kù)種類限制,還是那句話:都是親兒子挟炬!

關(guān)于 IncludeMany 還有更多功能請(qǐng)到 github wiki 文檔中了解鸥滨。

image

OneToMany 級(jí)聯(lián)保存

實(shí)踐中發(fā)現(xiàn),N對(duì)1 不適合做級(jí)聯(lián)保存谤祖。保存 Topic 的時(shí)候把 Type 信息也保存婿滓?我個(gè)人認(rèn)為自下向上保存的功能太不可控了,F(xiàn)reeSql 目前不支持自下向上保存粥喜。

FreeSql 支持的級(jí)聯(lián)保存凸主,是自上向下。例如保存 Type 的時(shí)候额湘,也同時(shí)能保存他的 Topic卿吐。

級(jí)聯(lián)保存,建議用在不太重要的功能锋华,或者測(cè)試數(shù)據(jù)添加:

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衍腥。


ManyToMany 多對(duì)多

多對(duì)多是很常見的一種設(shè)計(jì),如: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; }
}

看著覺得復(fù)雜纳猫?婆咸?看完后面查詢多么簡(jiǎn)單的時(shí)候,真的什么都值了芜辕!

N對(duì)N 導(dǎo)航屬性的主要優(yōu)勢(shì):

  • 查詢 Topic 的時(shí)候可以把 Tag 一起查詢出來尚骄,并且還是用 Topic 作為返回類型。
  • 添加 Topic 的時(shí)候侵续,把 Tags 一起添加
  • 更新 Topic 的時(shí)候倔丈,把 Tags 一起更新
  • 刪除 Topic 的時(shí)候,沒動(dòng)作( ef 那邊是用數(shù)據(jù)庫(kù)外鍵功能刪除子表記錄的)

ManyToMany 級(jí)聯(liá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 查詢數(shù)據(jù)回來的乃沙,大概是:

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 正式發(fā)布",
  Type: { name: "c#" }
  Tags: [ 標(biāo)簽列表 ]
}
...
]

N對(duì)N 級(jí)聯(lián)查詢,跟 1對(duì)N 一樣,都是用 IncludeMany蜀铲,N對(duì)N IncludeMany 也可以繼續(xù)向下 then边琉。

查詢 Tag.name = "c#" 的所有 topic:

fsql.Select<Topic>()
   .Where(a => a.Tags.AsSelect().Any(b => b.name = "c#"))
   .ToList();

產(chǎn)生的 SQL 大概是這樣的:

select * from topic
where id in ( 
    select topicid from topictag 
    where tagid in ( select id from tag where name = 'c#' ) 
)

ManyToMany 級(jí)聯(lián)保存

級(jí)聯(lián)保存,建議用在不太重要的功能记劝,或者測(cè)試數(shù)據(jù)添加:

var repo = fsql.GetRepository<Topic>();
repo.DbContextOptions.EnableAddOrUpdateNavigateList = true;
repo.DbContextOptions.NoneParameter = true;
repo.Insert(new Topic
{
  title = "FreeSql 1.8.1 正式發(fā)布",
  Tags = new List<Tag>(new[] {
    new Tag { name = "c#" }
  })
});

插入 topic变姨,再判斷 Tag 是否存在(如果不存在則插入 tag)。

得到 topic.id 和 tag.id 再插入 TopicTag厌丑。

另外提供的方法 repo.SaveMany(topic實(shí)體, "Tags") 完整保存 TopicTag 數(shù)據(jù)定欧。比如當(dāng) topic實(shí)體.Tags 屬性為 Empty 時(shí),刪除 topic實(shí)體 存在于 TopicTag 所有表數(shù)據(jù)怒竿。

SaveMany機(jī)制:完整保存砍鸠,對(duì)比 TopicTag 表已存在的數(shù)據(jù),計(jì)算出添加耕驰、修改爷辱、刪除執(zhí)行。

image

父子關(guān)系

父子關(guān)系朦肘,其實(shí)是 ManyToOne饭弓、OneToMany 的綜合體,自己指向自己媒抠,常用于樹形結(jié)構(gòu)表設(shè)計(jì)弟断。

父子關(guān)系,除了能使用 ManyToOne趴生、OneToMany 的使用方法外阀趴,還提供了 CTE遞歸查詢、內(nèi)存遞歸組裝數(shù)據(jù) 功能冲秽。

image
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 = "中國(guó)",
  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 = "東城區(qū)" },
      })
    }
  })
});

遞歸數(shù)據(jù)

配置好父子屬性之后,就可以這樣用了:

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);

查詢數(shù)據(jù)本來是平面的矩父,ToTreeList 方法將返回的平面數(shù)據(jù)在內(nèi)存中加工為樹型 List 返回锉桑。


CTE遞歸刪除

很常見的無限級(jí)分類表功能,刪除樹節(jié)點(diǎn)時(shí)窍株,把子節(jié)點(diǎn)也處理一下民轴。

fsql.Select<Area>()
  .Where(a => a.Name == "中國(guó)")
  .AsTreeCte()
  .ToDelete()
  .ExecuteAffrows(); //刪除 中國(guó) 下的所有記錄

如果軟刪除:

fsql.Select<Area>()
  .Where(a => a.Name == "中國(guó)")
  .AsTreeCte()
  .ToUpdate()
  .Set(a => a.IsDeleted, true)
  .ExecuteAffrows(); //軟刪除 中國(guó) 下的所有記錄

CTE遞歸查詢

若不做數(shù)據(jù)冗余的無限級(jí)分類表設(shè)計(jì),遞歸查詢少不了球订,AsTreeCte 正是解決遞歸查詢的封裝后裸,方法參數(shù)說明:

參數(shù) 描述
(可選) pathSelector 路徑內(nèi)容選擇,可以設(shè)置查詢返回:中國(guó) -> 北京 -> 東城區(qū)
(可選) up false(默認(rèn)):由父級(jí)向子級(jí)的遞歸查詢冒滩,true:由子級(jí)向父級(jí)的遞歸查詢
(可選) pathSeparator 設(shè)置 pathSelector 的連接符微驶,默認(rèn):->
(可選) level 設(shè)置遞歸層級(jí)

通過測(cè)試的數(shù)據(jù)庫(kù):MySql8.0、SqlServer、PostgreSQL因苹、Oracle苟耻、Sqlite、達(dá)夢(mèng)扶檐、人大金倉(cāng)

姿勢(shì)一:AsTreeCte() + ToTreeList

var t2 = fsql.Select<Area>()
  .Where(a => a.Name == "中國(guó)")
  .AsTreeCte() //查詢 中國(guó) 下的所有記錄
  .OrderBy(a => a.Code)
  .ToTreeList(); //非必須凶杖,也可以使用 ToList(見姿勢(shì)二)
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" = '中國(guó)')

// 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"

姿勢(shì)二:AsTreeCte() + ToList

var t3 = fsql.Select<Area>()
  .Where(a => a.Name == "中國(guó)")
  .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);
//執(zhí)行的 SQL 與姿勢(shì)一相同

姿勢(shì)三:AsTreeCte(pathSelector) + ToList

設(shè)置 pathSelector 參數(shù)后,如何返回隱藏字段款筑?

var t4 = fsql.Select<Area>()
  .Where(a => a.Name == "中國(guó)")
  .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("中國(guó)[100000]", t4[0].path);
Assert.Equal("中國(guó)[100000] -> 北京[110000]", t4[1].path);
Assert.Equal("中國(guó)[100000] -> 北京[110000] -> 北京市[110100]", t4[2].path);
Assert.Equal("中國(guó)[100000] -> 北京[110000] -> 東城區(qū)[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" = '中國(guó)')

// 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"
image

總結(jié)

微軟制造了優(yōu)秀的語言 c#智蝠,利用語言特性可以做一些非常好用的功能,在 ORM 中使用導(dǎo)航屬性非常適合奈梳。

  • ManyToOne(N對(duì)1) 提供了簡(jiǎn)單的多表 join 查詢杈湾;

  • OneToMany(1對(duì)N) 提供了簡(jiǎn)單可控的級(jí)聯(lián)查詢、級(jí)聯(lián)保存功能颈嚼;

  • ManyToMany(多對(duì)多) 提供了簡(jiǎn)單的多對(duì)多過濾查詢毛秘、級(jí)聯(lián)查詢、級(jí)聯(lián)保存功能阻课;

  • 父子關(guān)系 提供了常用的 CTE查詢叫挟、刪除、遞歸功能限煞;

希望正在使用的抹恳、善良的您能動(dòng)一動(dòng)小手指,把文章轉(zhuǎn)發(fā)一下署驻,讓更多人知道 .NET 有這樣一個(gè)好用的 ORM 存在奋献。謝謝了!旺上!

FreeSql 開源協(xié)議 MIT https://github.com/dotnetcore/FreeSql瓶蚂,可以商用,文檔齊全宣吱。QQ群:4336577(已滿)窃这、8578575(在線)、52508226(在線)

如果你有好的 ORM 實(shí)現(xiàn)想法征候,歡迎給作者留言討論杭攻,謝謝觀看!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末疤坝,一起剝皮案震驚了整個(gè)濱河市兆解,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌跑揉,老刑警劉巖锅睛,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡衣撬,警方通過查閱死者的電腦和手機(jī)乖订,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來具练,“玉大人乍构,你說我怎么就攤上這事】傅悖” “怎么了哥遮?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)陵究。 經(jīng)常有香客問我眠饮,道長(zhǎng),這世上最難降的妖魔是什么铜邮? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任仪召,我火速辦了婚禮,結(jié)果婚禮上松蒜,老公的妹妹穿的比我還像新娘扔茅。我一直安慰自己,他們只是感情好秸苗,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布召娜。 她就那樣靜靜地躺著,像睡著了一般惊楼。 火紅的嫁衣襯著肌膚如雪玖瘸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天檀咙,我揣著相機(jī)與錄音雅倒,去河邊找鬼。 笑死弧可,一個(gè)胖子當(dāng)著我的面吹牛蔑匣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播侣诺,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼殖演,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼氧秘!你這毒婦竟也來了年鸳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤丸相,失蹤者是張志新(化名)和其女友劉穎搔确,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膳算,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年座硕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涕蜂。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡华匾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出机隙,到底是詐尸還是另有隱情蜘拉,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布有鹿,位于F島的核電站旭旭,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏葱跋。R本人自食惡果不足惜持寄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娱俺。 院中可真熱鬧稍味,春花似錦、人聲如沸矢否。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽僵朗。三九已至赖欣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間验庙,已是汗流浹背顶吮。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留粪薛,地道東北人悴了。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像违寿,于是被迫代替她去往敵國(guó)和親湃交。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345