Loading

.NET ORM 仓储层必备的功能介绍之 FreeSql Repository 实现篇

写在开头

2018年11月的某一天,头脑发热开启了 FreeSql 开源项目之旅,时间一晃已经四年多,当初从舒服区走向一个巨大的坑,回头一看后背一凉。四年时间从无到有,经历了数不清的日夜奋战(有人问我花了多长时间投入,答案:全职x2 + 前两年无休息,以及后面两年的持续投入)。今天 FreeSql 已经很强大,感谢第一期、第二期、第N期持续提出建议的网友。

FreeSql 现如今已经是一个稳定的版本,主要体现:

  • API 已经确定,不会轻易推翻重作调整,坚持十年不变的原则,让使用者真真正正的不再关心 ORM 使用问题;
  • 单元测试覆盖面广,6336+ 个单元测试,小版本更新升级无须考虑修东墙、补西墙的问题;
  • 经历四年时间的生产考验,nuget下载量已超过900K+,平均每日750+;

感叹:有些人说 .Net 陷入 orm 怪圈,动手的没几个,指点江山的一堆,.Net orm 真的如他们所讲的简单吗?


项目介绍

FreeSql 是 .Net ORM,能支持 .NetFramework4.0+、.NetCore、Xamarin、MAUI、Blazor、以及还有说不出来的运行平台,因为代码绿色无依赖,支持新平台非常简单。目前单元测试数量:6336+,Nuget下载数量:900K+。QQ群:4336577(已满)、8578575(在线)、52508226(在线)

温馨提醒:以下内容无商吹成份,FreeSql 不打诳语

为什么要重复造轮子?

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

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

5500+个单元测试作为基调,支持10多数数据库,我们提供了通用Odbc理论上支持所有数据库,目前已知有群友使用 FreeSql 操作华为高斯、mycat、tidb 等数据库。安装时只需要选择对应的数据库实现包:

dotnet add packages FreeSql.Provider.MySql

FreeSql.Repository 是 FreeSql 项目的延申扩展类库,支持 .NETFramework4.0+、.NETCore2.0+、.NET5+、Xamarin 平台。

FreeSql.Repository 除了 CRUD 还有很多实用性功能,不防耐下心花10分钟看完。


01 安装

环境1:.NET Core 或 .NET 5.0+

dotnet add package FreeSql.Repository

环境2、.NET Framework

Install-Package FreeSql.DbContext
static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.Sqlite, connectionString)
    .UseAutoSyncStructure(true) //自动迁移实体的结构到数据库
    .UseNoneCommandParameter(true)
    .UseMonitorCommand(cmd => Console.WriteLine(cmd.CommandText))
    .Build(); //请务必定义成 Singleton 单例模式

02 使用方法

方法1、IFreeSql 的扩展方法;

var curd = fsql.GetRepository<Topic>();

注意:Repository 对象多线程不安全,因此不应在多个线程上同时对其执行工作。

  • fsql.GetRepository 方法返回新仓储实例
  • 不支持从不同的线程同时使用同一仓储实例

以下为了方便测试代码演示,我们都使用方法1,fsql.GetRepository 创建新仓储实例


方法2、继承实现;

public class TopicRepository: BaseRepository<Topic, int> {
    public TopicRepository(IFreeSql fsql) : base(fsql, null, null) {}

    //在这里增加 CURD 以外的方法
}

方法3、依赖注入;

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IFreeSql>(Fsql);
    services.AddFreeRepository(null, this.GetType().Assembly);
}

//在控制器使用
public TopicController(IBaseRepository<Topic> repo) {
}

03 添加数据

repo.Insert 插入数据,适配了各数据库优化执行 ExecuteAffrows/ExecuteIdentity/ExecuteInserted

1、如果表有自增列,插入数据后应该要返回 id。

var repo = fsql.GetRepository<Topic>();
repo.Insert(topic);

内部会将插入后的自增值填充给 topic.Id

2、批量插入

var repo = fsql.GetRepository<Topic>();
var topics = new [] { new Topic { ... }, new Topic { ... } };
repo.Insert(topics);

3、插入数据库时间

使用 [Column(ServerTime = DateTimeKind.Utc)] 特性,插入数据时,使用适配好的每种数据库内容,如 getutcdate()

4、插入特殊类型

使用 [Column(RereadSql = "{0}.STAsText()", RewriteSql = "geography::STGeomFromText({0},4236)")] 特性,插入和读取时特别处理

5、插入时忽略

使用 [Column(CanInsert = false)] 特性


04 更新数据

1、只更新变化的属性

var repo = fsql.GetRepository<Topic>();
var item = repo.Where(a => a.Id == 1).First();  //此时快照 item

item.Name = "newtitle";
repo.Update(item); //对比快照时的变化
//UPDATE `Topic` SET `Title` = 'newtitle'
//WHERE (`Id` = 1)

2、手工管理状态

var repo = fsql.GetRepository<Topic>();
var item = new Topic { Id = 1 };
repo.Attach(item); //此时快照 item

item.Title = "newtitle";
repo.Update(item); //对比快照时的变化
//UPDATE `Topic` SET `Title` = 'newtitle'
//WHERE (`Id` = 1)

3、直接使用 repo.UpdateDiy,它是 IFreeSql 提供的原生 IUpdate 对象,功能更丰富


05 级联保存数据

实践发现,N对1 不适合做级联保存。保存 Topic 的时候把 Type 信息也保存?我个人认为自下向上保存的功能太不可控了,FreeSql 目前不支持自下向上保存。因此下面我们只讲 OneToOne/OneToMany/ManyToMany 级联保存。至于 ManyToOne 级联保存使用手工处理,更加安全可控。

功能1:SaveMany 手工保存

完整保存,对比表已存在的数据,计算出添加、修改、删除执行。

递归保存导航属性不安全,不可控,并非技术问题,而是出于安全考虑,提供了手工完整保存的方式。

var repo = fsql.GetRepository<Type>();
var type = new Type
{
    name = "c#",
    Topics = new List<Topic>(new[]
    {
        new Topic { ... }
    })
};
repo.Insert(type);
repo.SaveMany(type, "Topics"); //手工完整保存 Topics
  • SaveMany 仅支持 OneToMany、ManyToMany 导航属性
  • 只保存 Topics,不向下递归追朔
  • 当 Topics 为 Empty 时,删除 type 存在的 Topics 所有表数据,确认?
  • ManyToMany 机制为,完整对比保存中间表,外部表只追加不更新

如:

  • 本表 Topic
  • 外部表 Tag
  • 中间表 TopicTag

功能2:EnableCascadeSave 仓储级联保存

DbContext/Repository EnableCascadeSave 可实现保存对象的时候,递归追朔其 OneToOne/OneToMany/ManyToMany 导航属性也一并保存,本文档说明机制防止误用。

1、OneToOne 级联保存

v3.2.606+ 支持,并且支持级联删除功能(文档请向下浏览)

2、OneToMany 追加或更新子表,不删除子表已存在的数据

var repo = fsql.GetRepository<Type>();
repo.DbContextOptions.EnableCascadeSave = true; //需要手工开启
repo.Insert(type);
  • 不删除 Topics 子表已存在的数据,确认?
  • 当 Topics 属性为 Empty 时,不做任何操作,确认?
  • 保存 Topics 的时候,还会保存 Topics[0-..] 的下级集合属性,向下18层,确认?

向下18层的意思,比如【类型】表,下面有集合属性【文章】,【文章】下面有集合属性【评论】。

保存【类型】表对象的时候,他会向下检索出集合属性【文章】,然后如果【文章】被保存的时候,再继续向下检索出集合属性【评论】。一起做 InsertOrUpdate 操作。

3、ManyToMany 完整对比保存中间表,追加外部表

完整对比保存中间表,对比【多对多】中间表已存在的数据,计算出添加、修改、删除执行。

追加外部表,只追加不更新。

  • 本表 Topic
  • 外部表 Tag
  • 中间表 TopicTag

06 删除数据

var repo = fsql.GetRepository<Topic>();
repo.Delete(new Topic { Id = 1 }); //有重载方法 repo.Delete(Topic[])

var repo2 = fsql.GetRepository<Topic, int>(); //int 是主键类型,相比 repo 对象多了 Delete(int) 方法
repo2.Delete(1);

07 级联删除数据

第一种:基于【对象】级联删除

比如使用过 Include/IncludeMany 查询的对象,可以使用此方法级联删除它们。

var repo = fsql.GetRepository<Group>();
repo.DbContextOptions.EnableCascadeSave = true; //关键设置
repo.Insert(new UserGroup
{
    GroupName = "group01",
    Users = new List<User>
    {
        new User { Username = "admin01", Password = "pwd01", UserExt = new UserExt { Remark = "用户备注01" } },
        new User { Username = "admin02", Password = "pwd02", UserExt = new UserExt { Remark = "用户备注02" } },
        new User { Username = "admin03", Password = "pwd03", UserExt = new UserExt { Remark = "用户备注03" } },
    }
}); //级联添加测试数据
//INSERT INTO "usergroup"("groupname") VALUES('group01') RETURNING "id"
//INSERT INTO "user"("username", "password", "groupid") VALUES('admin01', 'pwd01', 1), ('admin02', 'pwd02', 1), ('admin03', 'pwd03', 1) RETURNING "id" as "Id", "username" as "Username", "password" as "Password", "groupid" as "GroupId"
//INSERT INTO "userext"("userid", "remark") VALUES(3, '用户备注01'), (4, '用户备注02'), (5, '用户备注03')

var groups = repo.Select
    .IncludeMany(a => a.Users, 
        then => then.Include(b => b.UserExt))
    .ToList();
repo.Delete(groups); //级联删除,递归向下遍历 group OneToOne/OneToMany/ManyToMany 导航属性
//DELETE FROM "userext" WHERE ("userid" IN (3,4,5))
//DELETE FROM "user" WHERE ("id" IN (3,4,5))
//DELETE FROM "usergroup" WHERE ("id" = 1)

第二种:基于【数据库】级联删除,不依赖数据库外键

根据设置的导航属性,递归删除 OneToOne/OneToMany/ManyToMany 对应数据,并返回已删除的数据。此功能不依赖数据库外键

var repo = fsql.GetRepository<Group>();
var ret = repo.DeleteCascadeByDatabase(a => a.Id == 1);
//SELECT a."id", a."username", a."password", a."groupid" FROM "user" a WHERE (a."groupid" = 1)
//SELECT a."userid", a."remark" FROM "userext" a WHERE (a."userid" IN (3,4,5))
//DELETE FROM "userext" WHERE ("userid" IN (3,4,5))
//DELETE FROM "user" WHERE ("id" IN (3,4,5))
//DELETE FROM "usergroup" WHERE ("id" = 1)

//ret   Count = 7	System.Collections.Generic.List<object>
//  [0]	{UserExt}	object {UserExt}
//  [1]	{UserExt}	object {UserExt}
//  [2]	{UserExt}	object {UserExt}
//  [3]	{User}	    object {User}
//  [4]	{User}	    object {User}
//  [5]	{User}  	object {User}
//  [6]	{UserGroup}	object {UserGroup}

public class Group
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public string GroupName { get; set; }

    [Navigate(nameof(User.GroupId))]
    public List<User> Users { get; set; }
}
public class User
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public int GroupId { get; set; }

    [Navigate(nameof(Id))]
    public UserExt UserExt { get; set; }
}
public class UserExt
{
    [Column(IsPrimary = true)]
    public int UserId { get; set; }
    public string Remark { get; set; }

    [Navigate(nameof(UserId))]
    public User User { get; set; }
}

08 添加或修改数据

var repo = fsql.GetRepository<Topic>();
repo.InsertOrUpdate(item);

如果内部的状态管理存在数据,则更新。

如果内部的状态管理不存在数据,则查询数据库,判断是否存在。

存在则更新,不存在则插入

缺点:不支持批量操作

提醒:IFreeSql 也定义了 InsertOrUpdate 方法,两者实现机制不同,它利用了数据库特性:

Database Features Database Features
MySql on duplicate key update 达梦 merge into
PostgreSQL on conflict do update 人大金仓 on conflict do update
SqlServer merge into 神通 merge into
Oracle merge into 南大通用 merge into
Sqlite replace into MsAccess 不支持
Firebird merge into
fsql.InsertOrUpdate<T>()
  .SetSource(items) //需要操作的数据
  //.IfExistsDoNothing() //如果数据存在,啥事也不干(相当于只有不存在数据时才插入)
  .ExecuteAffrows();

09 批量编辑数据

var repo = fsql.GetRepository<BeginEdit01>();
var cts = new[] {
    new BeginEdit01 { Name = "分类1" },
    new BeginEdit01 { Name = "分类1_1" },
    new BeginEdit01 { Name = "分类1_2" },
    new BeginEdit01 { Name = "分类1_3" },
    new BeginEdit01 { Name = "分类2" },
    new BeginEdit01 { Name = "分类2_1" },
    new BeginEdit01 { Name = "分类2_2" }
}.ToList();
repo.Insert(cts);

repo.BeginEdit(cts); //开始对 cts 进行编辑

cts.Add(new BeginEdit01 { Name = "分类2_3" });
cts[0].Name = "123123";
cts.RemoveAt(1);

var affrows = repo.EndEdit(); //完成编辑
Assert.Equal(3, affrows);
class BeginEdit01
{
    public Guid Id { get; set; }
    public string Name { get; set; }
}

上面的代码 EndEdit 方法执行的时候产生 3 条 SQL 如下:

INSERT INTO "BeginEdit01"("Id", "Name") VALUES('5f26bf07-6ac3-cbe8-00da-7dd74818c3a6', '分类2_3')

UPDATE "BeginEdit01" SET "Name" = '123123' 
WHERE ("Id" = '5f26bf00-6ac3-cbe8-00da-7dd01be76e26')

DELETE FROM "BeginEdit01" WHERE ("Id" = '5f26bf00-6ac3-cbe8-00da-7dd11bcf54dc')

场景:winform 加载表数据后,一顿添加、修改、删除操作之后,点击【保存】

提醒:该操作只对变量 cts 有效,不是针对全表对比更新。


10 弱类型 CRUD

var repo = fsql.GetRepository<object>();
repo.AsType(typeof(Topic));

var item = (object)new Topic { Title = "object title" };
repo.Insert(item);

11 无参数化命令

支持参数化、无参数化命令执行,有一些特定的数据库,使用无参数化命令执行效率更高哦,并且调试起来更直观。

var repo = fsql.GetRepository<object>();
repo.DbContextOptions.NoneParameter = true;

12 工作单元(事务)

UnitOfWork 可将多个仓储放在一个单元管理执行,最终通用 Commit 执行所有操作,内部采用了数据库事务。

方法1:随时创建使用

using (var uow = fsql.CreateUnitOfWork())
{
  var typeRepo = fsql.GetRepository<Type>();
  var topicRepo = fsql.GetRepository<Topic>();
  typeRepo.UnitOfWork = uow;
  topicRepo.UnitOfWork = uow;

  typeRepo.Insert(new Type());
  topicRepo.Insert(new Topic());

  uow.Orm.Insert(new Topic()).ExecuteAffrows();
  //uow.Orm 和 fsql 都是 IFreeSql
  //uow.Orm CRUD 与 uow 是一个事务
  //fsql CRUD 与 uow 不在一个事务

  uow.Commit();
}

方法2:使用 AOP + UnitOfWorkManager 实现多种事务传播

本段内容引导,如何在 asp.net core 项目中使用特性(注解) 的方式管理事务。

UnitOfWorkManager 支持六种传播方式(propagation),意味着跨方法的事务非常方便,并且支持同步异步:

  • Requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,默认的选择。
  • Supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
  • Mandatory:使用当前事务,如果没有当前事务,就抛出异常。
  • NotSupported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • Never:以非事务方式执行操作,如果当前事务存在则抛出异常。
  • Nested:以嵌套事务方式执行。

第一步:配置 Startup.cs 注入

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IFreeSql>(fsql);
    services.AddScoped<UnitOfWorkManager>();
    services.AddFreeRepository(null, typeof(Startup).Assembly);
}
UnitOfWorkManager 成员 说明
IUnitOfWork Current 返回当前的工作单元
void Binding(repository) 将仓储的事务交给它管理
IUnitOfWork Begin(propagation, isolationLevel) 创建工作单元

第二步:定义事务特性

[AttributeUsage(AttributeTargets.Method)]
public class TransactionalAttribute : Attribute
{
    /// <summary>
    /// 事务传播方式
    /// </summary>
    public Propagation Propagation { get; set; } = Propagation.Requierd;
    /// <summary>
    /// 事务隔离级别
    /// </summary>
    public IsolationLevel? IsolationLevel { get; set; }
}

第三步:引入动态代理库

在 Before 从容器中获取 UnitOfWorkManager,调用它的 var uow = Begin(attr.Propagation, attr.IsolationLevel) 方法

在 After 调用 Before 中的 uow.Commit 或者 Rollback 方法,最后调用 uow.Dispose

提醒:动态代理,一定注意处理好异步 await,否则会出现事务异常的问题

第四步:在 Controller 或者 Service 中使用事务特性

public class TopicService
{
    IBaseRepository<Topic> _repoTopic;
    IBaseRepository<Detail> _repoDetail;

    public TopicService(IBaseRepository<Topic> repoTopic, IBaseRepository<Detail> repoDetail)
    {
        _repoTopic = repoTopic;
        _repoDetail = repoDetail;
    }

    [Transactional]
    public virtual void Test1()
    {
        //这里 _repoTopic、_repoDetail 所有 CRUD 操作都是一个工作单元
        this.Test2();
    }

    [Transactional(Propagation = Propagation.Nested)]
    public virtual void Test2() //嵌套事务,新的(不使用 Test1 的事务)
    {
        //这里 _repoTopic、_repoDetail 所有 CRUD 操作都是一个工作单元
    }
}

是不是进方法就开事务呢?

不一定是真实事务,有可能是虚的,就是一个假的 unitofwork(不带事务)

也有可能是延用上一次的事务

也有可能是新开事务,具体要看传播模式

示例项目:https://github.com/dotnetcore/FreeSql/tree/master/Examples/aspnetcore_transaction

Autofac 动态代理参考项目:


13 手工分表

FreeSql 原生用法、FreeSql.Repository 仓储用法 都提供了 AsTable 方法对分表进行 CRUD 操作,例如:

var repo = fsql.GetRepository<Log>();
repo.AsTable(oldname => $"{oldname}_201903"); //对 Log_201903 表 CRUD

repo.Insert(new Log { ... });

跨库,但是在同一个数据库服务器下,也可以使用 AsTable(oldname => $"db2.dbo.{oldname}")

//跨表查询
var sql = fsql.Select<User>()
    .AsTable((type, oldname) => "table_1")
    .AsTable((type, oldname) => "table_2")
    .ToSql(a => a.Id);

//select * from (SELECT a."Id" as1 FROM "table_1" a) ftb 
//UNION ALL
//select * from (SELECT a."Id" as1 FROM "table_2" a) ftb 

分表总结:

  • 分表、相同服务器跨库 可以使用 AsTable 进行 CRUD;
  • AsTable CodeFirst 会自动创建不存在的分表;
  • 不可在分表分库的实体类型中使用《延时加载》;

v3.2.500 按时间自动分表方案:https://github.com/dotnetcore/FreeSql/discussions/1066


写在最后

FreeSql 他是免费自由的 ORM,也可以说是宝藏 ORM。更多文档请前往 github wiki 查看。

FreeSql 已经步入第四个年头,期待少年的你十年后还能归来在此贴回复,兑现当初吹过十年不变的承诺。

多的不说了,希望民间的开源力量越来越强大。

希望作者的努力能打动到你,请求正在使用的、善良的您能动一动小手指,把文章转发一下,让更多人知道 .NET 有这样一个好用的 ORM 存在。谢谢!!

FreeSql 使用最宽松的开源协议 MIT https://github.com/dotnetcore/FreeSql ,完全可以商用,文档齐全,甚至拿去卖钱也可以。QQ群:4336577(已满)、8578575(在线)、52508226(在线)

posted @ 2022-05-06 10:41  FreeSql  阅读(4417)  评论(46编辑  收藏  举报