.NET ORM 开源项目 FreeSql 1.0 正式版发布

时间:2022-05-23 04:43:14

一、简介

FreeSql 是 .NET 平台下的对象关系映射技术(O/RM),支持 .NetCore 2.1 或 .NetFramework 4.0 或 Xamarin。

从 0.0.1 发布,历时整整一年的迭代更新,原计划元旦发布1.0,可能作者比较急提前了几天发布。其实是元旦有其他事……

本文内容从简,介绍项目的主要功能框架,以及暂时能想到的可能比较有说服力的特性。

二、项目统计

主仓库解决方案共计项目:29个

单元测试:3510个

Code Issues:168个

文档Wiki:43个

Stars:1140

Forks:236

Commits:690次

Nuget主包下载量:86,568次

开源地址:https://github.com/2881099/FreeSql

三、功能结构

.NET ORM 开源项目 FreeSql 1.0 正式版发布

  • 支持 CodeFirst 迁移,哪怕使用 Access 数据库也支持;
  • 支持 DbFirst 从数据库导入实体类;
  • 支持 深入的类型映射,比如pgsql的数组类型;
  • 支持 丰富的表达式函数,以及灵活的自定义解析;
  • 支持 导航属性一对多、多对多贪婪加载,以及延时加载;
  • 支持 读写分离、分表分库,租户设计,过滤器,乐观锁,悲观锁;
  • 支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/达梦数据库/Access;

四、CodeFirst/DbFirst

一切皆 CodeFirst,所有功能都是由实体类型,到表操作的过程。CodeFirst 【自动迁移】只需要一行代码:

using FreeSql;

static IFreeSql fsql = new FreeSqlBuilder()
    .UseConnectionString(DataType.Sqlite, 
        @"Data Source=|DataDirectory|document.db;Pooling=true;Max Pool Size=10")
    .UseAutoSyncStructure(true) //自动同步实体结构到数据库
    .Build();

在开发过程中,表结构会自动创建、或改变(不丢数据),取决于实体类的变化。

CodeFirst 提供功能丰富的特性ColumnAttribute,定义实体与表间的映射,并且支持 FluentApi 方式。如果不喜欢 ColumnAttribute 这个名字,还可以通过 AOP 设置换为 MyColumnAttribute。

using FreeSql.DataAnnotations;

class Song {
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public string Title { get; set; }
    public string Url { get; set; }
    public DateTime CreateTime { get; set; }
}

DbFirst 数据表先行,许多哥们使用动软、T4模板生成实体类代码。自已处理每种数据库的字段类型,和 csharp 类型对应,比较麻烦,各大 ORM 可能还不通用。

我们提供命令行工具生成实体类,dotnet-tools,对就是它。。非常好用的工具,没有之一。

C:Users28810>dotnet tool install -g freesql.generator
可使用以下命令调用工具: FreeSql.Generator
已成功安装工具“freesql.generator”(版本“1.0.0”)。

C:Users28810>FreeSql.Generator —help

它基于 Razor 模板生成,支持自定义模板生成,意味着它远不止可以生成实体类,甚至是 IRepository 或者。。。

五、导航属性

从一开始就着重导航对象的设计,支持一对多、多对多、父子关系、一对一、多对一,不夸张的说目前对导航属性处理最流弊,最容易上手的 ORM。多表查询的表达式使用非常便利,如下:

fsql.Select<Catetory>()
    .Where(a => a.Parent.Parent.Name == "粤语")

可以使用导航属性一直这样点下去。。。

级联保存,级联查询功能也必不可少,如下查询多对多:

fsql.Select<Song>()
    .IncludeMany(a => a.Tags)
    .ToList();

上面的代码,如果只返回 Tags 前 5条记录,也是支持的 .IncludeMany(a => a.Tags.Take(5))

对性能有追求,还可以指定 Tags 只查询部分字段

关于 IncludeMany 不便再这过多展开介绍。。。(其实还有黑科技!)

哦,还有 FreeSql.AdminLTE 扩展包,它不属于主仓库项目,最大化利用导航属性完成通用的 CURD 后台管理功能。

.NET ORM 开源项目 FreeSql 1.0 正式版发布

流弊哒哒~~~~

六、仓储模式

仓储工作单元目前是当下的流行风,在比较早的时候大约0.2版本发布了第一个仓储版本,当时参考了大量的项目设计,最终选用 abp vnext 的 IRepository 设计接口,实现通用仓储类功能。

也就是说,使用 FreeSql.Repository 你不必再自己写那些繁琐的 CURD 重复的仓储功能,不用再头疼仓储类的接口方法定义。定义标准比写代码难多了,abp vnext 的 IRepository 目前是见过最好的,木有之一!!

仓储模式都在操作实体对象,无论是更新还是删除,都是传对象。。。传传传。。。

问题1、传对象更新,意味着更新所有字段?

不会的,我们的仓储实现拥有状态管理机制,从对象查询出来的时候已经记录了拍照,当调用更新方法的时候会与之对比,计算出变化的字段,只更新变化的字段!

var repo = fsql.GetRepository<Song>();
var item = repo.Where(a => a.Id == 1).First();
item.Title = "原谅我今天";
repo.Update(item);

提示:支持乐观锁、悲观锁

问题2、状态管理是否影响性能?

不完全,因为状态管理设计在仓储实现之上,我们最原始的 IFreeSql 没有这个功能(仓储算是一种扩展包吧,但是仓储又非常有效)。仓储即用即销毁,擅用它的对比功能更新对象,不滥用没有性能问题。

有了仓储怎么会没有 UnitOfWork 呢,UnitOfWork 目前以事务的方式做了默认实现,并且它拥有实体变化跟踪记录。

七、性能

1、插入测试(52个字段)

18W 1W 5K 2K 1K 500 100 50
MySql 5.5 ExecuteAffrows 55,497 4,953 2,304 2,554 1,516 1,572 265 184
SqlServer Express ExecuteAffrows 402,355 24,847 11,465 4,971 2,437 915 138 88
SqlServer Express ExecuteSqlBulkCopy 21,065 578 326 139 105 79 60 48
PostgreSQL 10 ExecuteAffrows 46,756 3,294 2,269 1,019 374 209 51 37
PostgreSQL 10 ExecutePgCopy 10,090 583 337 136 88 61 30 25
Oracle XE ExecuteAffrows - - - - 24,528 10,648 571 200
Sqlite ExecuteAffrows 28,554 1,149 701 327 155 91 44 35

测试结果,是在相同操作系统下进行的,并且都有预热

18W 解释:插入18万行记录,表格中的数字是执行时间(单位ms)

Oracle 插入性能不用怀疑,可能安装学生版限制较大

提醒:开源数据库测试结果比较有意义,商业数据库版本之间性能可能有较大差距

2、插入测试(10个字段)

18W 1W 5K 2K 1K 500 100 50
MySql 5.5 ExecuteAffrows 15,380 1,813 1,457 1,254 563 246 55 21
SqlServer Express ExecuteAffrows 47,204 2,275 1,108 488 279 123 35 16
SqlServer Express ExecuteSqlBulkCopy 4,248 127 71 30 48 14 11 10
PostgreSQL 10 ExecuteAffrows 9,786 568 336 157 102 34 9 6
PostgreSQL 10 ExecutePgCopy 4,081 167 93 39 21 12 4 2
Oracle XE ExecuteAffrows - - - - 2,394 731 67 33
Sqlite ExecuteAffrows 4,524 246 137 94 35 19 14 11

提示:已经支持了 SqlServer 数据库的 SqlBulkCopy 功能、以及 PostgreSQL 数据库的 Copy 功能

八、拉姆达

非常特色的功能之一,深入细化函数解析,所支持的类型基本都可以使用对应的表达式函数,例如 日期、字符串、IN查询、数组(PostgreSQL的数组)、字典(PostgreSQL HStore)等等。

1、In查询

var t1 = fsql.Select<T>()
  .Where(a => new[] { 1, 2, 3 }.Contains(a.Id))
  .ToSql();
//SELECT .. FROM ..
//WHERE (a.`Id` in (1,2,3))

已优化,防止 where in 元素多过的 SQL 错误,如:

[Err] ORA-01795: maximum number of expressions in a list a 1000

原来:where id in (1..1333)

现在:where id in (1..500) or id in (501..1000) or id in (1001..1333)

2、In查询(多列)

//元组集合
vae lst = new List<(Guid, DateTime)>();

lst.Add((Guid.NewGuid(), DateTime.Now));
lst.Add((Guid.NewGuid(), DateTime.Now));
lst.Add((Guid.NewGuid(), DateTime.Now));
fsql.Select<T>()
  .Where(a => lst.Contains(a.Id, a.ct1))
  .ToSql();
//SELECT .. FROM ..
//WHERE (a."Id" = '685ee1f6-bdf6-4719-a291-c709b8a1378f' AND a."ct1" = '2019-12-07 23:55:27' OR 
//a."Id" = '5ecd838a-06a0-4c81-be43-1e77633b7404' AND a."ct1" = '2019-12-07 23:55:27' OR 
//a."Id" = 'b8b366f3-1c03-4547-9c96-d362dd5cae6a' AND a."ct1" = '2019-12-07 23:55:27')

3、自定义函数

默认已经支持了很丰富的函数解析,如果不够再自己定义:

[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.ParsedContent["that"]}, {up.ParsedContent["arg1"]})";
        return that;
    }
}

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

提示:SqlServer nvarchar/varchar 已兼容表达式解析,分别解析为:N‘‘ 和 ‘‘,优化索引执行计划

九、骚操作

1、代码注释 -> 迁移到数据库

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

  • 实体类所在程序集,需要开启 xml 文档功能;
  • xml 文件必须与程序集同目录,且文件名:xxx.dll -> xxx.xml;

2、NoneParameter

可以设置不使用 参数化 执行 SQL 命令,方便开发调试,区别如下:

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

在 new FreeSqlBuilder().UseNoneParameter(true) 全局设置

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

3、Dto 映射查询

用过 ProjectTo 功能吗?没用过当忽略此行。。。

有些朋友可能是先 ToList().Mapper<T>(),这样会先查询了所有字段。

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 }) 
//情况1:附加所有映射,再额外映射 ext,返回 List<DTO>

fsql.Select<Song>().ToList(a => new Song { id = a.id }) 
//情况2:只查询 id,返回 List<Song>

fsql.Select<Song>().ToList(a => new { id = a.id }) 
//情况3:只查询 id,返回 List<匿名对象>

fsql.Select<Song>().ToList(a => new DTO(a.id))
//情况4:只查询 id,返回 List<DTO>

fsql.Select<Song>().ToList(a => new DTO(a.id) { xxx = a.ext })
//情况5:查询 id, ext,返回 List<DTO>

fsql.Select<Song>().ToList(a => new Song(a.id))
//情况6:查询 id,返回 List<Song>

fsql.Select<Song>().ToList(a => new Song(a.id) { xxx = a.ext })
//情况7:查询 id, ext,返回 List<Song>

4、WhereCascade

FreeSql 擅长多表查询,遇到像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

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

5、审计 CURD

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

FreeSql 支持简单的类似功能:

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

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

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

6、审计属性值

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

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;

还有。。还有很多骚操作。。不便在此展开。。。

十、展望 2020

2019 年支持了主流的数据库:

  • SqlServer 2000-2019,支持 row_number/offset fetch next 分页自动版本选择适配,以及其他语法的差异适配,提供 ado.net 与 odbc 两种实现方式;

  • PostgreSQL 9.4-12,完成了版本间部分差异适配,提供 ado.net 与 odbc 两种实现方式;

  • MySql 5.5、Mariadb,提供 Oracle 官方驱动、与 MySqlConnector 社区驱动,还有 odbc 实现方式;

  • Oracle 11 ,提供 ado.net 与 odbc 两种实现方式;

  • Sqlite,兼容了 .net core / .net framework / xamarin 平台适配,支持 CodeFirst 开发模式,一个字爽!!!

  • MsAccess 2003-2007,提供 oledb 实现方式,支持 CodeFirst 开发模式;

  • 达梦,提供 odbc 的实现方式,并且支持 DbFirst 和 CodeFirst 两种开发模式;

2020 年支持国产是重点,重心,重要的工作内容,南大通用将是下一个目标,并且已经在进行中了。

开源地址:https://github.com/2881099/FreeSql

写到最后面,感谢这一年来与 FreeSql 一直陪伴的兄弟朋友们。