Skip to content

骚操作

28810 edited this page Nov 23, 2019 · 9 revisions

1、备注 -> 迁移到数据库

FreeSql CodeFirst 支持将 c# 代码内的注释,迁移至数据库的备注。先决条件:

1、实体类所在程序集,需要开启 xml 文档功能;

2、xml 文件必须与程序集同目录,且文件名:xxx.dll -> xxx.xml;


2、NonoParameter

这个方法可以设置不使用 参数化 执行 SQL 命令,方便开发调试。

INSERT INTO `tb_topic`(`Title`) VALUES(?Title0)

INSERT INTO `tb_topic`(`Title`) VALUES('Title_1')

在 new FreeSqlBuilder().UseNonoParameter(true) 可以全局设置。

在 单次 ISelect、IInsert、IDelete、IUpdate 上使用 NoneParameter() 设置单次生效。


3、添加或修改

var repo = fsql.GetRepository<T>();
repo.InsertOrUpdate(实体);

更多资料移步:wiki


4、巧用AsTable

var sql = fsql.Select<User>()
    .AsTable((a, b) => "(select * from user where clicks > 10)")
    .Page(1, 10).ToList()

请注意 AsTable 里面的 SQL 两侧的括号


5、你不知道的,指定字段返回

fsql.Select<t1>()
.ToList(a => new {
        a.Id,
        a.Title,
        cstitle = "substr(a.title, 0, 2)", //将 substr(a.title, 0, 2) 作为查询字段
        csnow = Convert.ToDateTime("now()"), //将 now() 作为查询字段
        //奇思妙想:怎么查询开窗函数的结果

        count = fsql.Select<T2>().Count(),
        max = fsql.Select<T2>().Max(b => b.Id),
        min = fsql.Select<T2>().Min(b => b.Id),
        name = fsql.Select<T2>().First(b => b.name)

        //可以直接映射一个导航属性
    });

6、Dto 映射查询

映射查询支持单表/多表,在查询数据之前映射(不是先查询所有字段再到内存映射)

规则:查找属性名,会循环内部对象 _tables(多表会增长),以 主表优先查,直到查到相同的字段。

如:

A, B, C 都有 id,Dto { id, a1, a2, b1, b2 },A.id 被映射。也可以指定 id = C.id 映射。

fsql.Select<Song>().ToList(a => new DTO { xxx = a.ext }) 
//这样写,附加所有映射,再额外映射 xxx

fsql.Select<Song>().ToList(a => new Song { id = a.id }) 
//这样写,只查询 id

fsql.Select<Song>().ToList(a => new { id = a.id }) 
//这样写,只查询 id,返回匿名对象

7、级联加载

有设置导航属性关系的(支持一对多、多对多):

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));

在 Dto 上做映射 IncludeMany:

//定义临时类,也可以是 Dto 类
class Dto {
    public int TypeId { get; set; }
    public List<Goods > GoodsList { get; set; }
}

//查询 Goods 商品表,分类1、分类2、分类3 各10条数据
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条记录

查询子集合表的部分字段,避免子集合字段过多的问题:

fsql.Select<Tag>().IncludeMany(a => a.Goods.Select(b => new Goods { Id = b.Id, Title = b.Title }));
//只查询 goods 表 id, title 字段,再作填充

8、WhereCascade

多表查询时,像isdeleted每个表都给条件,挺麻烦的。WhereCascade使用后生成sql时,所有表都附上这个条件。

如:

fsql.Select<t1>()
    .LeftJoin<t2>(...)
    .WhereCascade(x => x.IsDeleted == false)
    .ToList();

得到的 SQL:

SELECT ...
FROM t1
LEFT JOIN t2 on ... AND (t2.IsDeleted = 0) 
WHERE t1.IsDeleted = 0

其中的实体可附加表达式时才生效,支持子表查询。单次查询使用的表数目越多收益越大。


9、ISelect.ToDelete、ISelect.ToUpdate

默认 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 操作类似


10、保存多对多数据 SaveMany

之前:

FreeSql.DbContext 和 仓储实现,已经实现了联级保存功能,联级保存功能可实现保存对象的时候,将其【OneToMany】、【ManyToMany】导航属性集合也一并保存。

全局关闭:

fsql.SetDbContextOptions(opt => opt.EnableAddOrUpdateNavigateList = false);

局部关闭:

var repo = fsql.GetRepository<T>();
repo.DbContextOptions.EnableAddOrUpdateNavigateList = false;

本功能:

保存实体的指定【一对多】、【多对多】导航属性,SaveMany 方法实现在 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.SaveMany(song, "Tags");
//轻松保存 song 与 tag 表的关联

SaveMany【一对多】的机制是完整对比保存。

SaveMany【多对多】的机制规则与联级保存的一样,如下:

我们对中间表的保存是完整对比操作,对外部实体的操作只作新增(注意不会更新)

  • 属性集合为空时,删除他们的所有关联数据(中间表)
  • 属性集合不为空时,与数据库存在的关联数据(中间表)完全对比,计算出应该删除和添加的记录

11、自定义表达式函数

[ExpressionCall]
public static class DbFunc
{
    //必要定义 static + ThreadLocal
    static ThreadLocal<ExpressionCallContext> context = new ThreadLocal<ExpressionCallContext>();

    public static DateTime FormatDateTime(this DateTime that, string arg1)
    {
        var up = context.Value;
        if (up.DataType == FreeSql.DataType.Sqlite) //重写内容
            context.Value.Result = $"date_format({up.Values["that"]}, {up.Values["arg1"]})";
        return that;
    }
    
    public static string SetDbParameter(this string that, int size)
    {
        if (context.Value.DbParameter != null)
        {
            //已经参数化了,直接修改长度
            //提示:此条件可能开启了全局表达式参数化功能 UseGenerateCommandParameterWithLambda(true)
            context.Value.DbParameter.Size = size;
            return that;
        }
        var guid = Guid.NewGuid().ToString("N").ToLower();
        context.Value.UserParameters.Add(new SqlParameter
        {
            ParameterName = guid,
            SqlDbType = System.Data.SqlDbType.VarChar,
            Size = size,
            Value = that
        });
        return $"@{guid}"; //重写内容
    }
}

var sql1 = fsql.Select<SysModule>()
    .ToSql(a => a.CreateTime.FormatDateTime("yyyy-MM-dd"));
//SELECT date_format(a."CreateTime", 'yyyy-MM-dd') as1 
//FROM "SysModule" a

var sql2 = fsql.Select<SysModule>()
    .Where(a => a.Title == "123123".SetDbParameter(20))
    .ToSql();
//SELECT ...
//FROM [SysModule] a 
//WHERE (a.[Title] = @6a8b79d7001540369a2f52ecbba15679)

[ExpressionCall] 特性可在静态扩展类上标记,也可以在单个静态方法上标记;

ExpressionCallContext 属性 类型 描述
DataType FreeSql.DataType 用于实现不同数据库的适配判断条件
ParsedContent Dictionary<string, string> 函数的各参数解析结果
DbParameter DbParameter that 被参数化的对象(有可能为 null)
UserParameters List<DbParameter> 可附加参数化对象
Result string 返回表达式函数表示的 SQL 字符串

当扩展方法返回值为 string 时,其返回值也可以当作 context.Value.Result 功能


12、自定义实体特性、与其他 ORM 共用特性

抛砖引玉,以下的示例代码,FreeSql 使用 EFCore 的实体特性:

fsql.Aop.ConfigEntity = (s, e) => {
  var attr = e.EntityType.GetCustomAttributes(typeof(System.ComponentModel.DataAnnotations.Schema.TableAttribute), false).FirstOrDefault() as System.ComponentModel.DataAnnotations.Schema.TableAttribute;
  if (attr != null)
    e.ModifyResult.Name = attr.Name; //表名
};
fsql.Aop.ConfigEntityProperty = (s, e) => {
  if (e.Property.GetCustomAttributes(typeof(System.ComponentModel.DataAnnotations.KeyAttribute), false).Any())
    e.ModifyResult.IsPrimary = true; //主键
};

[System.ComponentModel.DataAnnotations.Schema.Table("xxx")]
class ModelAopConfigEntity {
  [System.ComponentModel.DataAnnotations.Key]
  [Column(IsPrimary = false)]
  public int pkid { get; set; }
}

13、审计 CURD

如果因为某个 sql 骚操作耗时很高,没有一个相关的审计功能,排查起来可以说无从下手。

FreeSql 支持简单的类似功能:

fsql.Aop.CurdAfter = (s, e) => {
	if (e.ElapsedMilliseconds > 200) {
		//记录日志
		//发送短信给负责人
	}
};

只需要一个事件,就可以对全局起到作用。

还有一个 CurdBefore 在执行 sql 之前触发,常用于记录日志或开发调试。


14、审计属性值

实现插入/更新时统一处理某些值,比如某属性的雪花算法值、创建时间值、甚至是业务值。

fsql.Aop.AuditValue += (s, e) => {
    if (e.Column.CsType == typeof(long) 
        && e.Property.GetCustomAttribute<SnowflakeAttribute>(false) != null
        && e.Value?.ToString() == 0)
        e.Value = new Snowflake().GetId();
};

class Order {
    [Snowflake]
    public long Id { get; set; }
    //...
}

当属性的类型是 long,并且标记了 [Snowflake],并且当前值是 0,那么在插入/更新时它的值将设置为雪花id值。

说明:SnowflakeAttribute 是使用者您来定义,new Snowflake().GetId() 也是由使用者您来实现

如果命名规范,可以在 aop 里判断,if (e.Property.Name == "createtime") e.Value = DateTime.Now;

You can’t perform that action at this time.