.NET ORM 导航属性可以解决什么问题?

本文详细介绍了.NET ORM中的导航属性,包括ManyToOne、OneToMany、ManyToMany的关系设计和使用,阐述了它们在解决多表查询、级联保存等问题上的作用。以FreeSql为例,展示了如何简化多表操作,提高开发效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在开头

从最早期入门时的单表操作,

到后来接触了 left join、right join、inner join 查询,

因为经费有限,需要不断在多表查询中折腾解决实际需求,不知道是否有过这样的经历?

本文从实际开发需求讲解导航属性(ManyToOne、OneToMany、ManyToMany)的设计思路,和到底解决了什么问题。提示:以下示例代码使用了 FreeSql 语法,和一些伪代码。


入戏准备

FreeSql 是 .Net ORM,能支持 .NetFramework4.0+、.NetCore、Xamarin、XAUI、Blazor、以及还有说不出来的运行平台,因为代码绿色无依赖,支持新平台非常简单。目前单元测试数量:5000+,Nuget下载数量:180K+,源码几乎每天都有提交。值得高兴的是 FreeSql 加入了 ncc 开源社区:https://github.com/dotnetcore/FreeSql,加入组织之后社区责任感更大,需要更努力做好品质,为开源社区出一份力。

QQ群:4336577(已满)、8578575(在线)、52508226(在线)

为什么要重复造轮子?

FreeSql 主要优势在于易用性上,基本是开箱即用,在不同数据库之间切换兼容性比较好。作者花了大量的时间精力在这个项目,肯请您花半小时了解下项目,谢谢。功能特性如下:

  • 支持 CodeFirst 对比结构变化迁移;
  • 支持 DbFirst 从数据库导入实体类;
  • 支持 丰富的表达式函数,自定义解析;
  • 支持 批量添加、批量更新、BulkCopy;
  • 支持 导航属性,贪婪加载、延时加载、级联保存;
  • 支持 读写分离、分表分库,租户设计;
  • 支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/达梦/神通/人大金仓/MsAccess;

FreeSql 使用非常简单,只需要定义一个 IFreeSql 对象即可:

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.MySql, connectionString)
    .UseAutoSyncStructure(true) //自动同步实体结构到数据库
    .Build(); //请务必定义成 Singleton 单例模式

ManyToOne 多对一

left join、right join、inner join 从表的外键看来,主要是针对一对一、多对一的查询,比如 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().ToList(a => new { Topic = a, TypeName = a.Type.name }); 返回 topic.* 和 type.name


有了这些机制,各种复杂的 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 方便多了?


OneToOne 一对一

一对一 和 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; }
}

OneToMany 一对多

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 的时候可以把 topic 一起查询出来,并且还是用 Type 作为返回类型。
  • 添加 Type 的时候,把 Topics 一起添加
  • 更新 Type 的时候,把 Topics 一起更新
  • 删除 Type 的时候,没动作( ef 那边是用数据库外键功能删除子表记录的)

OneToMany 级联查询

把 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')

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值