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