关于.net:NET-ORM-仓储层必备的功能介绍之-FreeSql-Repository-实现篇

137次阅读

共计 11763 个字符,预计需要花费 30 分钟才能阅读完成。

写在结尾

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) // 主动迁徙实体的构造到数据库
    .Build(); // 请务必定义成 Singleton 单例模式 

02 应用办法

办法 1、IFreeSql 的扩大办法;

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

留神:Repository 对象多线程不平安,因而不应在多个线程上同时对其执行工作。

  • fsql.GetRepository 办法返回新仓储实例
  • 不反对从不同的线程同时应用同一仓储实例

以下为了不便测试代码演示,咱们都应用办法 1,fsql.GetRepository<T> 创立新仓储实例


办法 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 动静代理参考我的项目:

  • https://github.com/luoyunchong/lin-cms-dotnetcore
  • https://github.com/luoyunchong/dotnetcore-examples/tree/master/ORM/FreeSql/OvOv.FreeSql.AutoFac.DynamicProxy
  • AOP + FreeSql 跨办法异步事务 https://www.cnblogs.com/igeekfan/p/aop-freesql-autofac.html

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(在线)

正文完
 0