33

FreeSql v0.11 几个实用功能说明

 4 years ago
source link: http://www.cnblogs.com/kellynic/p/11881941.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

FreeSql 开源发布快一年了,立志成为 .Net 平台方便好用的 ORM,仓库地址: https://github.com/2881099/FreeSql

随着不断的迭代更新,越来越稳定,也越来越强大。预计在一周年的时候(2020年1月1日)发布 1.0 正式版本。

金九银十的日子过去了,在这个铜一般的月份里,鄙人做了几个重大功能,希望对使用者开发提供更大的便利。

  • 一、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%。

这个月发现还有一余孽未清,发现问题后及时解决了,并增加单元测试代码以绝后患。

RJbyyiJ.png!web

四、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/%e6%9b%b4%e6%96%b0%e6%97%a5%e5%bf%97


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK