FreeSql 开源发布快一年了, 立志成为 .Net 平台方便好用的 ORM, 仓库地址: https://github.com/2881099/FreeSql
金九银十的日子过去了, 在这个铜一般的月份里, 鄙人做了几个重大功能, 希望对使用者开发提供更大的便利.
一, Dto 映射查询
二, IncludeMany 联级加载
三, Where(a => true) 逻辑表达式解析优化
四, SaveManyToMany 联级保存多对多集合属性
五, 迁移实体 - 到指定表名
六, MySQL 特有功能 On Duplicate Key Update, 和 Pgsql upsert
七, ISelect.ToDelete 高级删除
八, 全局过滤器
以下的代码, 先决定义代码如下 :
- IFreeSql fsql = new FreeSql.FreeSqlBuilder()
- .UseConnectionString(FreeSql.DataType.SQLite, @"Data Source=|DataDirectory|\db1.db;Max Pool Size=10";)
- .UseAutoSyncStructure(true) // 自动同步实体结构到数据库
- .Build();
- public class Blog
- {
- public Guid Id { get; set; }
- public string Url { get; set; }
- public int Rating { get; set; }
- }
一, Dto 映射查询
- class Dto
- {
- public Guid Id { get; set; }
- public string Url { get; set; }
- public int xxx { get; set; }
- }
- fsql.Select<Blog>().ToList<Dto>();
- //SELECT Id, Url FROM Blog
- fsql.Select<Blog>().ToList(a => new Dto { xxx = a.Rating} );
- //SELECT Id, Url, Rating as xxx FROM Blog
- // 这样写, 附加所有映射, 再额外映射 xxx
- fsql.Select<Blog>().ToList(a => new Blog { Id = a.Id })
- // 这样写, 只查询 id
- fsql.Select<Blog>().ToList(a => new { a.Id })
- // 这样写, 只查询 id, 返回匿名对象
映射支持单表 / 多表, 是在查询数据之前映射(不是先查询所有字段再到内存映射)
查找规则, 查找属性名, 会循环内部对象 _tables(join 查询后会增长), 以 主表优先查, 直到查到相同的字段.
如:
A, B, C 都有 id,Dto { id, a1, a2, b1, b2 },A.id 被映射. 也可以指定 id = C.id 映射.
友情提醒: 在 dto 可以直接映射一个导航属性
二, IncludeMany 联级加载
之前已经实现, 有设置关系, 和未设置关系 的导航集合属性联级加载.
有设置关系的(支持一对多, 多对多):
fsql.Select<Tag>().IncludeMany(a => a.Goods).ToList();
未设置关系的, 临时指定关系(只支持一对多):
fsql.Select<Goods>().IncludeMany(a => a.Comment.Where(b => b.TagId == a.Id));
只查询每项子集合的前几条数据, 避免像 EfCore 加载所有数据导致 IO 性能低下(比如某商品下有 2000 条评论):
fsql.Select<Goods>().IncludeMany(a => a.Comment.Take(10));
上面已有的 IncludeMany 功能还不够自由灵活.
新功能 1: 在 Dto 上做映射 IncludeMany
老的 IncludeMany 限制只能在 ISelect 内使用, 必须要先查上级数据, 解决这个问题我们做了直接在 Dto 上做映射:
查询 Goods 商品表, 分类 1, 分类 2, 分类 3 各 10 条数据
- // 定义临时类, 也可以是 Dto 类
- class Dto {
- public int TypeId { get; set; }
- public List<Goods> GoodsList { get; set; }
- }
- var dto = new [] { 1,2,3 }.Select(a => new Dto { TypeId = a }).ToList();
- dto.IncludeMany(d => d.GoodsList.Take(10).Where(gd => gd.TypeId == d.TypeId));
- // 执行后, dto 每个元素. Vods 将只有 10 条记录
现在 IncludeMany 不再是 ISelect 的专利, 普通的 List<T> 也可以用它来贪婪加载数据, 并准确填充到内部各元素中.
新功能 2: 查询子集合表的指定字段
老的 IncludeMany 限制只能查子表的所有字段, 子表过段多过的话比较浪费 IO 性能.
新功能可以设置子集合返回部分字段, 避免子集合字段过多的问题.
- fsql.Select<Tag>().IncludeMany(a => a.Goods.Select(b => new Goods {
- Id = b.Id, Title = b.Title
- }));
- // 只查询 goods 表 id, title 字段, 再作填充
三, Where(a => true) 逻辑表达式解析优化
相信很多 ORM 解析表达式的时候处理不了这个问题, 我们之前已经解决了 99%.
这个月发现还有一余孽未清, 发现问题后及时解决了, 并增加单元测试代码以绝后患.
四, SaveManyToMany 联级保存多对多集合属性
在此之前, FreeSql.DbContext 和 仓储实现, 已经实现了联级保存功能, 如下:
联级保存功能可实现保存对象的时候, 将其[OneToMany] ,[ManyToMany] 导航属性集合也一并保存.
全局关闭:
fsql.SetDbContextOptions(opt => opt.EnableAddOrUpdateNavigateList = false);
局部关闭:
- var repo = fsql.GetRepository<T>();
- repo.DbContextOptions.EnableAddOrUpdateNavigateList = false;
新功能:
保存实体的指定[多对多] 导航属性, SaveManyToMany 方法实现在 BaseRepository,DbContext.
解决问题: 当实体类导航数据过于复杂的时候, 选择关闭联级保存的功能是明智之选, 但是此时[多对多] 数据保存功能写起来非常繁琐麻烦(因为要与现有数据对比后保存).
- var song = new Song {
- Id = 1
- };
- song.Tags = new List<Tag>();
- song.Tags.Add(new Tag ...);
- song.Tags.Add(new Tag ...);
- song.Tags.Add(new Tag ...);
- repo.SaveManyToMany(song, "Tags");
- // 轻松保存 song 与 tag 表的关联
机制规则与联级保存的[多对多] 一样, 如下:
我们对中间表的保存是完整对比操作, 对外部实体的操作只作新增(注意不会更新)
属性集合为空时, 删除他们的所有关联数据(中间表)
属性集合不为空时, 与数据库存在的关联数据 (中间表) 完全对比, 计算出应该删除和添加的记录
五, 迁移实体 - 到指定表名
- fsql.CodeFirst.SyncStructure(typeof(Log), "Log_1"); // 迁移到 Log_1 表
- fsql.CodeFirst.SyncStructure(typeof(Log), "Log_2"); // 迁移到 Log_2 表
在此功能上, 我们对分表功能做了点升级, 以下动作都会做迁移动作:
- fsql.Select<Log>().AsTable((_, oldname) => $"{oldname}_1");
- fsql.GetRepository<Log>(null, oldname => $"{oldname}_1");
六, MySQL 特有功能 On Duplicate Key Update, 和 Pgsql upsert
FreeSql 提供了多种插入或更新方法, v0.11 之前主要使用 FreeSql.Repository/FreeSql.DbContext 库提供的方法实现.
FreeSql.Repository 之 InsertOrUpdate
此方法与 FreeSql.DbContext AddOrUpdate 方法功能一样.
- var repo = fsql.GetRepository<T>();
- repo.InsertOrUpdate(实体);
如果内部的状态管理存在数据, 则更新.
如果内部的状态管理不存在数据, 同查询数据库, 是否存在.
存在则更新, 不存在则插入
缺点: 不支持批量操作
新功能: MySQL 特有功能 On Duplicate Key Update
FreeSql.Provider.MySQL 和 FreeSql.Provider.MySqlConnector 在 v0.11.11 版本已支持 MySQL 特有的功能, On Duplicate Key Update.
这个功能也可以实现插入或更新数据, 并且支持批量操作.
- class TestOnDuplicateKeyUpdateInfo
- {
- [Column(IsIdentity = true)]
- public int id { get; set; }
- public string title { get; set; }
- public DateTime time { get; set; }
- }
- var item = new TestOnDuplicateKeyUpdateInfo { id = 100, title = "title-100", time = DateTime.Parse("2000-01-01") };
- fsql.Insert(item)
- .NoneParameter()
- .OnDuplicateKeyUpdate().ToSql();
- //INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`, `time`) VALUES(100, 'title-100', '2000-01-01 00:00:00.000')
- //ON DUPLICATE KEY UPDATE
- //`title` = VALUES(`title`),
- //`time` = VALUES(`time`)
OnDuplicateKeyUpdate() 之后可以调用的方法:
方法名 | 描述 |
---|---|
IgnoreColumns | 忽略更新的列,机制和 IUpdate.IgnoreColumns 一样 |
UpdateColumns | 指定更新的列,机制和 IUpdate.UpdateColumns 一样 |
Set | 手工指定更新的列,与 IUpdate.Set 功能一样 |
SetRaw | 作为 Set 方法的补充,可传入 SQL 字符串 |
ToSql | 返回即将执行的 SQL 语句 |
ExecuteAffrows | 执行,返回影响的行数 |
IInsert 与 OnDuplicateKeyUpdate 都有 IgnoreColumns,UpdateColumns 方法.
当插入实体 / 集合实体的时候, 忽略了 time 列, 代码如下:
- fsql.Insert(item)
- .IgnoreColumns(a => a.time)
- .NoneParameter()
- .OnDuplicateKeyUpdate().ToSql();
- //INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`) VALUES(200, 'title-200')
- //ON DUPLICATE KEY UPDATE
- //`title` = VALUES(`title`),
- //`time` = '2000-01-01 00:00:00.000'
我们发现, UPDATE time 部分变成了常量, 而不是 VALUES(`time`), 机制如下:
当 insert 部分中存在的列, 在 update 中将以 VALUES(` 字段 `) 的形式设置;
当 insert 部分中不存在的列, 在 update 中将为常量形式设置, 当操作实体数组的时候, 此常量为 case when ... end 执行(与 IUpdate 一样);
新功能 2:PostgreSQL 特有功能 On Conflict Do Update
使用方法 MySQL OnDuplicateKeyUpdate 大致相同.
七, ISelect.ToDelete 高级删除
默认 IDelete 不支持导航对象, 多表关联等. ISelect.ToDelete 可将查询转为删除对象, 以便支持导航对象或其他查询功能删除数据, 如下:
fsql.Select<T1>().Where(a => a.Options.xxx == 1).ToDelete().ExecuteAffrows();
注意: 此方法不是将数据查询到内存循环删除, 上面的代码产生如下 SQL 执行:
DELETE FROM `T1` WHERE id in (select a.id from T1 a left join Options b on b.t1id = a.id where b.xxx = 1)
复杂删除使用该方案的好处:
删除前可预览测试数据, 防止错误删除操作;
支持更加复杂的删除操作(IDelete 默认只支持简单的操作), 甚至在 ISelect 上使用 Limit(10) 将只删除附合条件的前 10 条记录;
还有 ISelect.ToUpdate 高级更新数据功能, 使用方法类似
八, 全局过滤器
FreeSql 基础层实现了 Select/Update/Delete 可设置的全局过滤器功能.
- public static AsyncLocal<Guid> TenantId { get; set; } = new AsyncLocal<Guid>();
- fsql.GlobalFilter
- .Apply<TestAddEnum>("test1", a => a.Id == TenantId.Value)
- .Apply<AuthorTest>("test2", a => a.Id == 111)
- .Apply<AuthorTest>("test3", a => a.Name == "11");
Apply 泛型参数可以设置为任何类型, 当使用 Select/Update/Delete 方法时会进行过滤器匹配尝试(try catch):
匹配成功的, 将附加 where 条件;
匹配失败的, 标记下次不再匹配, 避免性能损耗;
如何禁用?
- fsql.Select<TestAddEnum>().ToList(); // 所有生效
- fsql.Select<TestAddEnum>().DisableGlobalFilter("test1").ToList(); // 禁用 test1
- fsql.Select<TestAddEnum>().DisableGlobalFilter().ToList(); // 禁用所有
fsql.Update/Delete 方法效果同上.
注意: IFreeSql.GlobalFilter 与 仓储过滤器 不是一个功能, 可以同时生效
鸣谢
感谢反馈 bug 的朋友!
仓库地址: https://github.com/2881099/FreeSql
请移步更新日志: https://github.com/2881099/FreeSql/wiki/更新日志
来源: https://www.cnblogs.com/kellynic/p/11881941.html