FreeSql https://freesql.net/ FreeSql 文档 zh-CN Fri, 06 Mar 2026 16:19:25 GMT Fri, 06 Mar 2026 16:19:25 GMT @vuepress/plugin-feed https://validator.w3.org/feed/docs/rss2.html TDengine https://freesql.net/guide/freesql-provider-tdengine.html https://freesql.net/guide/freesql-provider-tdengine.html TDengine 介绍 TDengine 是一款开源、高性能、云原生的时序数据库, 它专为物联网、车联网、工业互联网、金融、IT 运维等场景优化设计 TDengine 文档 | TDengine 文档 | 涛思数据 安装包 FreeSql.Provider.TDengine .NET CLI Package Manager 安装客户端驱动 如果选择原生连接,而且应用程序... Sat, 30 Nov 2024 03:50:04 GMT 介绍

TDengine 是一款开源、高性能、云原生的时序数据库, 它专为物联网、车联网、工业互联网、金融、IT 运维等场景优化设计

TDengine 文档 | TDengine 文档 | 涛思数据

安装包

FreeSql.Provider.TDengine

.NET CLI

dotnet add package FreeSql.Provider.TDengine

Package Manager

Install-Package FreeSql.Provider.TDengine

安装客户端驱动

如果选择原生连接,而且应用程序不在 TDengine 同一台服务器上运行,你需要先安装客户端驱动,否则可以跳过此一步。为避免客户端驱动和服务端不兼容,请使用一致的版本。

安装客户端驱动 taosc

声明

建议尽量使用无参数化

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.TDengine,
        "host=localhost;port=6030;username=root;password=taosdata;protocol=Native;db=test;")
    .UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}\r\n"))
    .UseNoneCommandParameter(true) //无参数化
    .Build();

特有功能

超级表

采用“一个数据采集点一张表”的设计虽然有助于针对性地管理每个采集点,但随着设备数量不断增加表的数量也会急剧增加,这给数据库管理和数据分析带来了挑战。在进行跨数据采集点的聚合操作时,用户需要面对大量的表,工作变得异常繁重。

为了解决这个问题,TDengine 引入超级表(Super Table,简称为 STable)的概念。超级表是一种数据结构,它能够将某一特定类型的数据采集点聚集在一起,形成一张逻辑上的统一表。这些数据采集点具有相同的表结构,但各自的静态属性(如标签)可能不同。创建超级表时,除了定义采集量以外,还需定义超级表的标签。一张超级表至少包含一个时间戳列、一个或多个采集量列以及一个或多个标签列。此外,超级表的标签可以灵活地进行增加、修改或删除操作。

在 TDengine 中,表代表具体的数据采集点,而超级表则代表一组具有相同属性的数据采集点集合。以智能电表为例,我们可以为该类型的电表创建一张超级表,其中包含了所有智能电表的共有属性和采集量。这种设计不仅简化了表的管理,还便于进行跨数据采集点的聚合操作,从而提高数据处理的效率。

子表

子表是数据采集点在逻辑上的一种抽象表示,它是隶属于某张超级表的具体表。用户可以将超级表的定义作为模板,并通过指定子表的标签值来创建子表。这样,通过超级表生成的表便被称为子表。超级表与子表之间的关系主要体现在以下几个方面。

  • 一张超级表包含多张子表,这些子表具有相同的表结构,但标签值各异。
  • 子表的表结构不能直接修改,但可以修改超级表的列和标签,且修改对所有子表立即生效。
  • 超级表定义了一个模板,自身并不存储任何数据或标签信息。

在 TDengine 中,查询操作既可以在子表上进行,也可以在超级表上进行。针对超级表的查询,TDengine 将所有子表中的数据视为一个整体,首先通过标签筛选出满足查询条件的表,然后在这些子表上分别查询时序数据,最终将各张子表的查询结果合并。本质上,TDengine 通过对超级表查询的支持,实现了多个同类数据采集点的高效聚合。为了更好地理解采集量、标签、超级表与子表之间的关系,这里以智能电表的数据模型为例进行说明。可以参考图 3-1 的数据模型,以便更直观地了解这些概念。

为了更好地理解采集量、标签、超级与子表的关系,以智能电表为例,可以参考下图

数据模型示意图
数据模型示意图

例子

   [TDengineSuperTable(Name = "meters")]
   class Meters
   {
       [Column(Name = "ts")]
       public DateTime Ts { get; set; }

       [Column(Name = "current")]
       public float Current { get; set; }

       [Column(Name = "voltage")]
       public int Voltage { get; set; }

       [Column(Name = "describe", StringLength = 50)]
       public string? Describe { get; set; }

       [TDengineTag(Name = "location")]
       public virtual string? Location { get; set; }

       [TDengineTag(Name = "group_id")]
       public virtual int GroupId { get; set; }
   }

   [TDengineSubTable(SuperTableName = "meters", Name = "d1001")]
   class D1001 : Meters
   {
       [TDengineTag(Name = "location")]
       public override string Location { get; set; } = "BeiJIng.ChaoYang";

       [TDengineTag(Name = "group_id")]
       public override int GroupId { get; set; } = 1;
   }


   [TDengineSubTable(SuperTableName = "meters", Name = "d1002")]
   class D1002 : Meters
   {
       [TDengineTag(Name = "location")]
       public new string Location { get; set; } = "California.SanFrancisco";

       [TDengineTag(Name = "group_id")]
       public new int GroupId { get; set; } = 2;
   }

CodeFirst创建表

//尽量确定表结构
//同步子表会自动创建超表
fsql.CodeFirst.SyncStructure<D1001>();
fsql.CodeFirst.SyncStructure<D1002>();

插入数据

//向子表插入数据
var affrows = fsql.Insert(new D1002()
{
    Ts = DateTime.Now,
    Current = 1,
    Voltage = 1,
    Describe = "D10021"
}).ExecuteAffrows();

//批量向子表插入数据,注意 一定要关闭参数化
var batchRes = fsql.Insert(new List<D1002>()
{
    new D1002()
    {
        Ts = DateTime.Now,
        Current = 6,
        Voltage = 6,
        Describe = "D10026"
    },
    new D1002()
    {
        Ts = DateTime.Now,
        Current = 3,
        Voltage = 3,
        Describe = "D10023"
    },
    new D1002()
    {
        Ts = DateTime.Now,
        Current = 4,
        Voltage = 4,
        Describe = "D10024"
    }
}).ExecuteAffrows();

查询

//查询子表
var list = fsql.Select<D1001>().ToList();
//查询超表
var metersList = fsql.Select<Meters>().Where(d => d.GroupId == 2).ToList();

删除

var startTime = DateTime.Parse("2024-11-30T02:33:52.308+00:00");
var endTime = DateTime.Parse("2024-11-30T02:40:58.961+00:00");
//必须包含时间戳
var executeAffrows = fsql.Delete<Meters>().Where(meters => meters.Ts >= startTime && meters.Ts <= endTime && meters.GroupId == 1).ExecuteAffrows();
]]>
DuckDB(嵌入式 OLAP) https://freesql.net/guide/freesql-provider-duckdb.html https://freesql.net/guide/freesql-provider-duckdb.html DuckDB(嵌入式 OLAP) 介绍 DuckDB 是一款进程内分析数据库,它可以在无需维护分布式多服务器系统的情况下处理出人意料的大型数据集。 DuckDB has two configurable options for concurrency: One process can both read and write to the database. Multiple proce... Sat, 17 Aug 2024 09:27:08 GMT 介绍

DuckDB 是一款进程内分析数据库,它可以在无需维护分布式多服务器系统的情况下处理出人意料的大型数据集。

DuckDB has two configurable options for concurrency:

  • One process can both read and write to the database.
  • Multiple processes can read from the database, but no processes can write (access_mode = 'READ_ONLY').

DuckDB | 官网

安装包

FreeSql.Provider.Duckdb

.NET CLI

dotnet add package FreeSql.Provider.Duckdb

Package Manager

Install-Package FreeSql.Provider.Duckdb

声明

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.DuckDB, "DataSource = train_services.db")
    .UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}"))
    .UseAutoSyncStructure(true) //自动创建表
    .Build();

依赖的第三方 ado.net 驱动源代码:https://github.com/Giorgi/DuckDB.NET

| Connection String | Description | |

]]>
ClickHouse https://freesql.net/guide/freesql-provider-clickhouse.html https://freesql.net/guide/freesql-provider-clickhouse.html ClickHouse 介绍 ClickHouse 是一个高性能的开源列式数据库,专为实时大数据分析设计。它以列存储数据,支持快速查询和聚合操作,适合处理大规模数据集。ClickHouse 具备分布式架构、数据压缩和高可用性特性,使其在处理复杂查询和实时数据分析时表现出色。 ClickHouse | 官网 安装包 FreeSql.Provider.ClickHouse .NE... Fri, 26 Jul 2024 09:14:45 GMT 介绍

ClickHouse 是一个高性能的开源列式数据库,专为实时大数据分析设计。它以列存储数据,支持快速查询和聚合操作,适合处理大规模数据集。ClickHouse 具备分布式架构、数据压缩和高可用性特性,使其在处理复杂查询和实时数据分析时表现出色。

ClickHouse | 官网

安装包

FreeSql.Provider.ClickHouse

.NET CLI

dotnet add package FreeSql.Provider.ClickHouse

Package Manager

Install-Package FreeSql.Provider.ClickHouse

声明

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.ClickHouse,
        "DataCompress=False;BufferSize=32768;SocketTimeout=10000;CheckCompressedHash=False;Encrypt=False;Compressor=lz4;" +
        "Host=192.168.0.121;Port=8125;Database=PersonnelLocation;Username=root;Password=123")
    .UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}"))
    .Build();
]]>
Firebird(嵌入式) https://freesql.net/guide/freesql-provider-firebird.html https://freesql.net/guide/freesql-provider-firebird.html Firebird(嵌入式) 介绍 Firebird 是一个开源的关系型数据库管理系统,它支持嵌入式部署。Firebird 嵌入式数据库适用于需要在本地应用程序中直接集成数据库的场景,无需单独的数据库服务器。它提供了强大的事务处理、数据完整性和并发控制功能,同时保持轻量级和高效。适合小型到中型应用程序,特别是那些需要在不依赖外部数据库服务器的情况下进行本地数据存储和操作的情况。 F... Fri, 26 Jul 2024 09:14:45 GMT 介绍

Firebird 是一个开源的关系型数据库管理系统,它支持嵌入式部署。Firebird 嵌入式数据库适用于需要在本地应用程序中直接集成数据库的场景,无需单独的数据库服务器。它提供了强大的事务处理、数据完整性和并发控制功能,同时保持轻量级和高效。适合小型到中型应用程序,特别是那些需要在不依赖外部数据库服务器的情况下进行本地数据存储和操作的情况。

FirebirdSqlite 都是本地数据库,Firebird 支持并发读写,Sqlite 不支持并发写。

Firebird | 官网

安装包

FreeSql.Provider.Firebird

.NET CLI

dotnet add package FreeSql.Provider.Firebird

Package Manager

Install-Package FreeSql.Provider.Firebird

声明

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.Firebird, "database=localhost:D:\fbdata\EXAMPLES.fdb;user=sysdba;password=123456")
    .UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}"))
    .UseAutoSyncStructure(true) //自动创建表
    .Build();

嵌入式例子

freesql_firebird_embed.zip

]]>
SqlServer https://freesql.net/guide/freesql-provider-sqlserver.html https://freesql.net/guide/freesql-provider-sqlserver.html SqlServer FreeSql 最多支持 SqlServer2000,根据不同的需求选择驱动包,微软提供了两个 SqlClient 访问包,因此我们也发布了两个,分别是: FreeSql.Provider.SqlServer FreeSql.Provider.SqlServerForSystem (强制使用 System.Data.SqlClient.dll,对 .N... Thu, 25 Jul 2024 07:47:58 GMT FreeSql 最多支持 SqlServer2000,根据不同的需求选择驱动包,微软提供了两个 SqlClient 访问包,因此我们也发布了两个,分别是:

  • FreeSql.Provider.SqlServer
  • FreeSql.Provider.SqlServerForSystem (强制使用 System.Data.SqlClient.dll,对 .NET Framework 更友好)

WithLock/WithIndex

var list = fsql.Select<Region>()
    .WithLock()
    .Limit(1).ToList();
//SELECT TOP 1 ... FROM [Region] a With(NoLock)

var list = fsql.Select<Region>()
    .WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
    .Limit(1).ToList();
//SELECT TOP 1 ... FROM [Region] a With(NoLock, NoWait)

var list = fsql.Select<Region>()
    .WithLock()
    .WithIndex("idx_01")
    .Limit(1).ToList();
//SELECT TOP 1 ... FROM [Region] a With(index=idx_01, NoLock)

多表:

var list = Select<Region, T2>()
    .InnerJoin((a, b) => a.x == b.xx)
    .WithLock(SqlServerLock.NoLock, new Dictionary<Type, bool>
    {
        [typeof(T2)] = false
    })
    .WithIndex("idx_01", new Dictionary<Type, string>
    {
        [typeof(T2)] = "idx_02"
    })
    .Limit(1).ToList();
//SELECT TOP 1 ..
//FROM [Region] a With(index=idx_01, NoLock)
//INNER JOIN [T2] b With(index=idx_02) ON a.[x] = b.[xx]

全局设置 NoLock:

//所有实体类生效
fsql.SetGlobalSelectWithLock(SqlServerLock.NoLock, null);

//【指定】实体类生效
fsql.SetGlobalSelectWithLock(SqlServerLock.NoLock, new Dictionary<Type, bool>
{
    [typeof(Region)] = true,
    [typeof(T2)] = true
});

增删改 SqlBulkCopy

fsql.Insert(items)
    .ExecuteSqlBulkCopy();

fsql.Update<T>.SetSource(items)
    .ExecuteSqlBulkCopy(); //临时表 + MERGE INTO

fsql.InsertOrUpdate<T>.SetSource(items)
    .ExecuteSqlBulkCopy(); //临时表 + MERGE INTO

访问 SqlServer2000

使用自定义适配更多的数据库,比如 mssql2000、db2,自定义适配将牺牲一些功能:

  • 不支持 CodeFirst 自动迁移
  • 不支持 DbFirst 接口方法的实现
  • 不支持 原来的分页方法,需要自行判断 id 进行分页
  • 只支持较少的基础类型:bool,sbyte,short,int,long,byte,ushort,uint,ulong,double,float,decimal,DateTime,byte[],string,Guid

使用者只需求重写类 FreeSql.Custom.CustomAdapter 就可以自定义访问不同的数据库。

默认做了一套 sqlserver 的语法和映射适配,代码在 CustomAdapter.cs,请查看代码了解。

class Mssql2000Adapter : FreeSql.Custom.CustomAdapter
{
    public override string InsertAfterGetIdentitySql => "SELECT SCOPE_IDENTITY()";
    //可以重写更多的设置
}

public class DB
{
   static Lazy<IFreeSql> sqliteLazy = new Lazy<IFreeSql>(() =>
   {
        var fsql = new FreeSql.FreeSqlBuilder()
            .UseConnectionString(DataType.Custom, () => new OdbcConnection(...))
            .UseMonitorCommand(cmd => Trace.WriteLine($"Sql:{cmd.CommandText}"))
            .Build();
        fsql.SetCustomAdapter(new Mssql2000Adapter());
        return fsql;
    });
    public static IFreeSql Sqlite => sqliteLazy.Value;
}

适配好新的 CustomAdapter 后,请在 FreeSqlBuilder.Build 之后调用 IFreeSql.SetCustomAdapter 方法生效。

使用 ODBC 访问古董数据库。

]]>
AdminBlazor https://freesql.net/guide/AdminBlazor.html https://freesql.net/guide/AdminBlazor.html AdminBlazor AdminBlazor 项目介绍 AdminBlazor 是一款 Blazor Server SaaS 中台项目,支持 RABC 权限菜单/按钮,支持快速代码生成一对一、一对多、多对多导航属性的 .razor 界面。 集成功能:菜单、角色、用户、公司组织、定时任务、数据字典、参数配置、租户、审批、审计、OSS文件管理 依赖组件:BootstrapBla... Wed, 31 Jan 2024 07:25:26 GMT 项目介绍

AdminBlazor 是一款 Blazor Server SaaS 中台项目,支持 RABC 权限菜单/按钮,支持快速代码生成一对一、一对多、多对多导航属性的 .razor 界面。

集成功能:菜单、角色、用户、公司组织、定时任务、数据字典、参数配置、租户、审批、审计、OSS文件管理

依赖组件:BootstrapBlazor、FreeSql、FreeScheduler、Rougamo

  • 老版本: https://gitee.com/FreeSql/AdminBlazor
  • 新版本:暂不开源(商业技术支持)
image
image
image
image

快速开始

  1. 安装模板

dotnet new install AdminBlazor.Template

  1. 新建项目

dotnet new admin

项目结构简洁(没有过多的分层)

  1. 运行访问

http://localhost:5231

用户名:admin 密码:admin

  1. 新建菜单,类型选择增删改查
image
image
  1. 生成代码,在实体类型维护注释、导航属性
  • 实体上的注释,会生成 HTML Label
  • 实体上的导航属性,会生成丰富的 UI
  • 创建实体类型,建议继承 Entity/EntityCreated/EntityModifed/EntityAudited
image
image

权限

  • SysUser 多对多 SysRole
  • SysRole 多对多 SysMenu

提示:AdminContext 类型已注入为 Scoped

class AdminContext
{
    public IServiceProvider Service { get; }
    public SysUser User { get; set; }
    public List<SysRole> Roles { get; }
    public List<SysMenus> RoleMenus { get; }

    //路由、按钮权限验证
    public bool AuthPath(string path);
    public bool AuthButton(string path)
}

按钮权限,在 razor 中设置特性:

[AdminButton("name")]
void ButtonClick()
{
}

//或者 webapi
[Route("api/菜单路径")]
class XxxController : ControllerBase
{
    [HttpGet("@name")]
    public JsonResult ButtonClick()
    {
    }
}

之后菜单管理,会出现对应的按钮项,勾选设置角色是否有按钮的权限。

image
image

审批

  • 实体类继承 EntityAudited
  • 菜单编辑选择 【审批】
image
image
  • 待提交/反审,可修改/删除
  • 审核中/通过,已锁定业务数据,不可修改/删除
  • EntityAudited 带版本功能、审计历史变化,且多端无法同时编辑(提示正在被[admin]编辑)
image
image

租户

提示:AdminContext 类型已注入为 Scoped

class AdminContext
{
    public IServiceProvider Service { get; }
    public SysTenant Tenant { get; }
}

每个租户独立数据库,注入方式:

  • 访问租户:IFreeSql/IAggregateRootRepository<T>
  • 访问主库:FreeSqlCloud

FreeSqlCloud API 访问方式与 IFreeSql 一样 IAggregateRootRepository 是级联操作友好的仓储模式

image
image

定时任务

[Scheduler("任务1", "0/30 * * * * *")]
static void Scheduler001()
{
    System.Console.WriteLine("任务1 被触发...");
}

[Scheduler("任务2", Interval = TaskInterval.SEC, Argument = "10")]
static void Scheduler002(IServiceProvider service, TaskInfo task)
{
    System.Console.WriteLine("任务2 被触发...");
}

//运行时 scheduler.AddTask("任务3"...)
[Scheduler("任务3")]
static void Scheduler003()
{
    System.Console.WriteLine("任务3 被触发...");
}

组件

以下几个是 AdminBlazor 封装的组件,更多丰富的 UI 组件可以看:BootstrapBlazor

1. 增删改查 AdminTable2<TItem>

| 名称 | 说明 | |

]]>
低代码 https://freesql.net/guide/lowcode.html https://freesql.net/guide/lowcode.html 低代码 低代码 本篇是继文档之后的大功能,专门为低代码设计。 FreeSql 默认依赖实体类型,虽然运行时可以动态创建 Type,但不断的动态编译导致会内存无法释放,对 Type 的版本也难以管理。 本功能是独立的,使用纯字典(无实体类型)进行 CRUD,支持导航属性,级联操作等功能。 注意:本功能是独立的,请勿与其他文档的级联机制搞混。 字典 CUD(单表)... Thu, 07 Dec 2023 05:53:21 GMT 本篇是继《动态操作》文档之后的大功能,专门为低代码设计。

FreeSql 默认依赖实体类型,虽然运行时可以动态创建 Type,但不断的动态编译导致会内存无法释放,对 Type 的版本也难以管理。

本功能是独立的,使用纯字典(无实体类型)进行 CRUD,支持导航属性,级联操作等功能。

注意:本功能是独立的,请勿与其他文档的级联机制搞混。

字典 CUD(单表)

var dic = new Dictionary<string, object>();
dic.Add("id", 1);
dic.Add("name", "xxxx");

fsql.InsertDict(dic).AsTable("table1").ExecuteAffrows();
fsql.UpdateDict(dic).AsTable("table1").WherePrimary("id").ExecuteAffrows();
fsql.DeleteDict(dic).AsTable("table1").ExecuteAffrows();
fsql.InsertOrUpdateDict(dic).AsTable("table1").WherePrimary("id").ExecuteAffrows();

InsertDict/UpdateDict/DeleteDict/InsertOrUpdateDict 都支持批量操作,对应类型 List<Dictionary<string, object>>

无类型 CRUD(更高级)

不依赖实体类型,不需要动态编译,纯字典操作,支持导航属性,级联保存,AOT 编译福音。

nuget 安装:

dotnet add package FreeSql.Extensions.ZeroEntity

var ctx = new ZeroDbContext(fsql, JsonConvert.DeserializeObject<TableDescriptor[]>(json)); //在文档后面

var item = JsonConvert.DeserializeObject<Dictionary<string, object>>(@"
{
  ""Name"":""user1"",
  ""Ext"":
  {
    ""Remarks"":[{""Remark"":""remark1""},{""Remark"":""remark2""}]
  },
  ""Claims"":[{""ClaimName"":""claim1""},{""ClaimName"":""claim2""},{""ClaimName"":""claim3""}],
  ""Roles"":[{""Name"":""role1""},{""Name"":""role2""}]
}");
ctx.Insert(item);

var item2 = JsonConvert.DeserializeObject<Dictionary<string, object>>(@"
{
  ""Id"":1,
  ""Name"":""user1111"",
  ""Ext"":{},
  ""Claims"":[{""Id"":1,""ClaimName"":""claim1111""},{""Id"":""3"",""ClaimName"":""claim3222222""},{""ClaimName"":""claim0000""}],
  ""Roles"":[{""Name"":""role111100001""},{""Id"":2,""Name"":""role2""}]
}");
ctx.Update(item2);
ctx.Delete(item2);
INSERT INTO [Role]([Name]) OUTPUT INSERTED.[Id] as [Id], INSERTED.[Name] as [Name] VALUES(N'role1'), (N'role2')
INSERT INTO [User]([Name]) OUTPUT INSERTED.[Id] as [Id], INSERTED.[Name] as [Name] VALUES(N'user1')
INSERT INTO [UserExt]([UserId]) VALUES(1)
INSERT INTO [UserExtRemarks]([RemarkId], [UserId], [Remark]) VALUES('6570e3f8-a226-c3ac-00d1-a3dd18b30339', 1, N'remark1'), ('6570e3f8-a226-c3ac-00d1-a3de16d9aa68', 1, N'remark2')
INSERT INTO [UserClaim]([UserId], [ClaimName]) OUTPUT INSERTED.[Id] as [Id], INSERTED.[UserId] as [UserId], INSERTED.[ClaimName] as [ClaimName] VALUES(1, N'claim1'), (1, N'claim2'), (1, N'claim3')
INSERT INTO [UserRole]([UserId], [RoleId]) VALUES(1, 5), (1, 6)

INSERT INTO [Role]([Name]) OUTPUT INSERTED.[Id] as [Id], INSERTED.[Name] as [Name] VALUES(N'role111100001'), (N'role2')
INSERT INTO [UserClaim]([UserId], [ClaimName]) OUTPUT INSERTED.[Id] as [Id], INSERTED.[UserId] as [UserId], INSERTED.[ClaimName] as [ClaimName] VALUES(1, N'claim0000')
INSERT INTO [UserRole]([UserId], [RoleId]) VALUES(1, 7), (1, 8)
DELETE FROM [UserRole] WHERE ([UserId] = 1 AND [RoleId] = 6)
DELETE FROM [UserRole] WHERE ([UserId] = 1 AND [RoleId] = 5)
DELETE FROM [UserClaim] WHERE ([Id] = 2)
UPDATE [User] SET [Name] = N'user1111'
WHERE ([Id] = 1)
UPDATE [UserClaim] SET [ClaimName] = CASE [Id]
WHEN 1 THEN N'claim1111'
WHEN 3 THEN N'claim3222222' END
WHERE ([Id] IN (1,3))

DELETE FROM [UserRole] WHERE (([UserId] = 1 AND [RoleId] = 7) OR ([UserId] = 1 AND [RoleId] = 8))
DELETE FROM [UserClaim] WHERE ([Id] IN (1,3,4))
DELETE FROM [UserExt] WHERE ([UserId] = 1)
DELETE FROM [User] WHERE ([Id] = 1)

查询,返回结果是字典 Dictionary<string, object>:

//自动级联
ctx.Select.Where("id", 1).ToList();

//单独查询
ctx.SelectNoTracking("User")
  //.IncludeAll()
  .Include("Ext.Remarks", then => then.Where("remark", "like", "error"))
  .Include("Roles", then => then.Include("Users",
    then => then.Include("Ext.Remarks")))
  .ToList();

//普通多表查询
ctx.SelectNoTracking("User")
  .LeftJoin("UserExt", "UserId", "User.Id")
  .ToList();
//[{id:1, UserExt:{} },..]

上述 ctx 对象依赖 json 配置如下:

提示:ctx.LoadSchemaFromDatabase("User") 从数据库动态加载描述(可能不完善)

[
  {
    "Name": "User",
    "Comment": "用户表",
    "Columns": [
      {
        "Name": "Id",
        "IsPrimary": true,
        "IsIdentity": true,
        "MapType": "System.Int32"
      },
      { "Name": "Name", "MapType": "System.String" }
    ],
    "Navigates": [
      { "Name": "Ext", "Type": "OneToOne", "RelTable": "UserExt" },
      {
        "Name": "Claims",
        "Type": "OneToMany",
        "RelTable": "UserClaim",
        "Bind": "UserId"
      },
      {
        "Name": "Roles",
        "Type": "ManyToMany",
        "RelTable": "Role",
        "ManyToMany": "UserRole"
      }
    ],
    "Indexes": []
  },
  {
    "Name": "UserExt",
    "Comment": "用户扩展信息表",
    "Columns": [
      { "Name": "UserId", "IsPrimary": true, "MapType": "System.Int32" }
    ],
    "Navigates": [
      {
        "Name": "Remarks",
        "Type": "OneToMany",
        "RelTable": "UserExtRemarks",
        "Bind": "UserId"
      }
    ]
  },
  {
    "Name": "UserExtRemarks",
    "Comment": "用户扩展信息表-子表",
    "Columns": [
      { "Name": "RemarkId", "IsPrimary": true, "MapType": "System.Guid" },
      { "Name": "UserId", "MapType": "System.Int32" },
      { "Name": "Remark", "MapType": "System.String" }
    ]
  },
  {
    "Name": "UserClaim",
    "Comment": "一对多测试表",
    "Columns": [
      {
        "Name": "Id",
        "IsPrimary": true,
        "IsIdentity": true,
        "MapType": "System.Int32"
      },
      { "Name": "UserId", "MapType": "System.Int32" },
      { "Name": "ClaimName", "MapType": "System.String" }
    ]
  },
  {
    "Name": "Role",
    "Comment": "权限表",
    "Columns": [
      {
        "Name": "Id",
        "IsPrimary": true,
        "IsIdentity": true,
        "MapType": "System.Int32"
      },
      { "Name": "Name", "MapType": "System.String" }
    ],
    "Navigates": [
      {
        "Name": "Users",
        "Type": "ManyToMany",
        "RelTable": "User",
        "ManyToMany": "UserRole"
      }
    ],
    "Indexes": []
  },
  {
    "Name": "UserRole",
    "Comment": "多对多中间表",
    "Columns": [
      { "Name": "UserId", "IsPrimary": true, "MapType": "System.Int32" },
      { "Name": "RoleId", "IsPrimary": true, "MapType": "System.Int32" }
    ],
    "Navigates": [
      {
        "Name": "User",
        "Type": "ManyToOne",
        "RelTable": "User",
        "Bind": "UserId"
      },
      {
        "Name": "Role",
        "Type": "ManyToOne",
        "RelTable": "Role",
        "Bind": "RoleId"
      }
    ]
  }
]

级联机制

理解本机制之前,请先忘记 Repository/DbContext 等之前的级联机制,他们没有关联。

schemas[] 是一组表映射信息定义,包含表名、列名、导航属性、索引等信息

  • 导航属性:OneToOne/OneToMany/ManyToOne/ManyToMany
  • 聚合根:OneToOne/OneToMany/多对多中间表,作为一个整体看待
  • 外部根:ManyToOne/ManyToMany外部表,作为外部看待,它有自己的聚合根整体

举例:

  • User 为聚合根
  • UserExt/UserClaim/UserRole 这三个表是子成员,一起存储/删除
  • Role 为外部根(相对 User 而言,它自己是独立的聚合根)

CURD 都是基于 schemas[0] 聚合根进行操作

  • 查询:贪婪加载所有子成员,以及外部根,以及外部根的外部根(递归)
  • 状态管理:快照聚合根副本(由于外部根也是聚合根,即外部根与聚合根是并行存储关系)

对比保存:

将当前操作的聚合根与状态管理的副本进行对比,计算出发生变化的列

| 导航属性 | 副本 | 最新 | 动作 | |

]]>
QuestDB https://freesql.net/guide/freesql-provider-questdb.html https://freesql.net/guide/freesql-provider-questdb.html QuestDB 介绍 QuestDB 是一款针对时序数据实时处理优化的关系型列存数据库, 支持 Rest API 方式访问,同时兼容 PostgreSQL 访问协议,以及 InfluxDB 写入的访问协议。自带 Web Console,方便数据库的基本访问 QuestDB | 官网 安装包 FreeSql.Provider.QuestDb .NET CLI Packa... Wed, 22 Feb 2023 05:25:18 GMT 介绍

QuestDB 是一款针对时序数据实时处理优化的关系型列存数据库, 支持 Rest API 方式访问,同时兼容 PostgreSQL 访问协议,以及 InfluxDB 写入的访问协议。自带 Web Console,方便数据库的基本访问

QuestDB | 官网

安装包

FreeSql.Provider.QuestDb

.NET CLI

dotnet add package FreeSql.Provider.QuestDb

Package Manager

Install-Package FreeSql.Provider.QuestDb

声明

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.QuestDb,
       @"host=localhost;port=8812;username=admin;password=quest;database=qdb;ServerCompatibilityMode=NoTypeLoading;")  //连接字符串
    .UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}"))
    .UseQuestDbRestAPI("localhost:9000", "username", "password")  //RestAPI,建议开启
    .Build();

特有功能

QuestFunc

  • QuestFunc 实现了 QuestDB 官方文档的函数
  • SelectLongSequence 对应 long_sequence
fsql.SelectLongSequence(10, () => new
    {
        rndstr = QuestFunc.rnd_str(10, 5, 10, 0),
        rnddate = QuestFunc.rnd_date(QuestFunc.to_date("2020", "yyyy"), QuestFunc.to_date("2023", "yyyy"))
    })
    .From<User1>()
    .InnerJoin((a, b) => a.rndstr == b.Username)
    .ToList();
//SELECT *
//FROM (
//    SELECT rnd_str(10,5,10,0) "rndstr", rnd_date(to_date('2020','yyyy'),to_date('2023','yyyy'),0) "rnddate"
//    FROM long_sequence(10)
//) a
//INNER JOIN "user1" b ON a."rndstr" = b."username"

Sample By

SAMPLE BY用于时间序列数据,将大型数据集汇总为同质时间块的聚合,作为SELECT语句的一部分

SAMPLE BY keyword | QuestDB

fsql.Select<Table>()
    .SampleBy(1, SampleUnit.day)
    .WithTempQuery(q => new { q.Id, q.Activos, count = SqlExt.Count(q.Id).ToValue() })
    .Where(q => q.Id != "xxx")
    .ToSql();

生成SQL

SELECT *
FROM (
    SELECT a."Id", a."Activos", count(a."Id") "count"
    FROM "Table" a SAMPLE BY 1d
) a
WHERE (a."Id" <> '1')

GroupBy

需要注意的是 QuestDB 的 GroupBy 与其他关系型 数据库不同

官网说明:SQL extensions | QuestDB

//这里通过WithTempQuery实现
fsql.Select<Table>()
    .WithTempQuery(q => new { q.Id, q.xxx, count = SqlExt.Count(q.Id).ToValue() })
    .Where(q => q.Id != "1" && q.count > 1)
    .ToSql();

生成SQL

SELECT *
FROM (
    SELECT a."Id", a."xxx", count(a."Id") "count"
    FROM "Table" a
) a
WHERE (a."Id" <> '1' AND a."count" > 1)

Latest On

对于多个时间序列存储在同一个表中的场景,根据时间戳检索给定键或键组合的最新项

LATEST ON keyword | QuestDB

fsql.Select<Table>()
    .LatestOn(q => q.CreateTime, q => new { q.xxx, q.xxx })
    .ToSql();

生成SQL

SELECT  a."xxx", a."xxx", a."xxx", a."xxx", a."xxx", a."xxx", a."xxx", a."xxx", a."xxx"
FROM "Table" a
LATEST ON a.xxxx  PARTITION BY a.xxxx

BulkCopy

实测七列10W数据预热后只需1.5秒,100W数据14秒左右

//需要启用RestAPI
fsql.Insert(list).ExecuteBulkCopyAsync();

注意:RestAPI 不经过 ado.net,因为不触发 UseMonitorCommand/Aop.CommandBefore/After 等事件

自动分表、索引

QuestDB 支持自动分表

[Index("Id_Index", nameof(Id), false)]
class Table
{
    //索引类型必须是symbol
    [Column(DbType = "symbol")]
    public string Id { get; set; }
    public string Name { get; set; }
    public double? Activos { get; set; }
    //按天分表
    [AutoSubtable(SubtableType.Day)]
    //特性标记类型必须是DateTime
    public DateTime? CreateTime { get; set; }
    public bool? IsCompra { get; set; }
}

常见问题

table busy

多线程并发查询时会出现 table busy [reason=insert] 异常

官网说明 | table busy

解决方案,启用RestAPI后 Insert/Update就会默认使用HTTP方式

//在FreeSqlBuilder增加UseQuestDbRestAPI()
new FreeSql.FreeSqlBuilder()
    .UseQuestDbRestAPI("localhost:9000", "username", "password")

注意:RestAPI 不经过 ado.net,因为不触发 UseMonitorCommand/Aop.CommandBefore/After 等事件

RestAPI设置账号密码

QuestDb WebConsole并不支持设置账号密码,但是官网给出解决方案 使用Nginx代理

Setting up Basic Authentication for QuestDB open source using Nginx | QuestDB

QuestDb不支持删除?

FAQ | How do I delete a row?

在线测试

QuestDB | 在线测试 提供了最新的QuestDB发行版和示例数据集:

  • Trips: 10 years of NYC taxi trips with 1.6 billion rows
  • Trades: live crytocurrency market data with 30M+ rows per month
  • Pos: geolocations of 250k unique ships over time

| Query | Execution time | |

]]>
聚合根(实验室) https://freesql.net/guide/aggregateroot.html https://freesql.net/guide/aggregateroot.html 聚合根(实验室) 聚合根(实验室) FreeSql.DbContext 定义了 IBaseRepository&lt;T&gt; 仓储接口,(虽然)支持了级联保存、级联删除功能,(但是)使用时需要人工自己判断何时开启、何时使用。 本文看上去像 EF,实则有区别,主要区别在级联边界的规则设定,例如我们允许 OneToMany 从下层向上递归级联,但是仅限查询,不能增删改。研究目的希望... Sun, 04 Sep 2022 08:40:48 GMT FreeSql.DbContext 定义了 IBaseRepository<T> 仓储接口,(虽然)支持了级联保存、级联删除功能,(但是)使用时需要人工自己判断何时开启、何时使用。

本文看上去像 EF,实则有区别,主要区别在级联边界的规则设定,例如我们允许 OneToMany 从下层向上递归级联,但是仅限查询,不能增删改。研究目的希望从机制上杜绝痛点,让操作变得更可控。

AggregateRootRepository 是 IBaseRepository<T> 一种新的尝试实现,根据聚合根特点,实现可控的级联添加、级联更新、级联删除、级联查询(查询时自动 Include/IncludeMany)操作。

var repository = fsql.GetAggregateRootRepository<Order>();

dotnet add package FreeSql.Extensions.AggregateRoot

意见征集、讨论区:https://github.com/dotnetcore/FreeSql/discussions/1235

接下来的内容,严重依赖【导航属性】的正确配置,请先学会再继续向下!

设定边界

将一个主要的实体类认定为聚合根,设定好安全的管辖范围(边界),CRUD 时会把边界之内的所有内容看作一个整体。

边界之外的导航属性,增删改 递归时会忽略:

  • ManyToOne
  • ManyToMany(外部表)
  • PgArrayToMany

边界之内的导航属性,增删改 递归时会级联操作:

  • OneToOne
  • OneToMany
  • ManyToMany(中间表)

示例1:在聚合根内递归所有 OneToOne/OneToMany 导航属性

  • OneToOne: Order <-> OrderExt
  • OneToMany: Order <== OrderDetail
  • OneToOne: OrderDetail <-> OrderDetailExt
  • 聚合根 Order 的管辖范围:Extdata、Details、Details[?].Extdata
class Order
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public string Field2 { get; set; }

    public OrderExt Extdata { get; set; }

    [Navigate(nameof(OrderDetail.OrderId))]
    public List<OrderDetail> Details { get; set; }
}
class OrderExt
{
    [Key]
    public int OrderId { get; set; }
    public string Field3 { get; set; }

    public Order Order { get; set; }
}
class OrderDetail
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public int OrderId { get; set; }
    public string Field4 { get; set; }

    public OrderDetailExt Extdata { get; set; }
}
class OrderDetailExt
{
    [Key]
    public int OrderDetailId { get; set; }
    public string Field5 { get; set; }

    public OrderDetail OrderDetail { get; set; }
}

示例2:在聚合根内递归所有 ManyToMany 导航属性对应的中间表

  • ManyToMany: Order <=> Tag
  • 聚合根 Order 会根据 Tags 生成 OrderTag 中间表数据,进行管理
  • 聚合根 Order 不会管理 Tag 实体类,以及 Tag 向下延申的导航属性(外部表不属于管辖范围)
class Order
{
    // ..
    [Navigate(ManyToMany = typeof(OrderTag))]
    public List<Tag> Tags { get; set; }
}
class OrderTag
{
    [Key]
    public int OrderId { get; set; }
    [Key]
    public int TagId { get; set; }

    [Navigate(nameof(OrderId))]
    public Order Order { get; set; }
    [Navigate(nameof(TagId))]
    public Tag Tag { get; set; }
}
class Tag
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public string Name { get; set; }

    [Navigate(ManyToMany = typeof(OrderTag))]
    public List<Order> Orders { get; set; }
}

插入数据

根据上面设定的边界,插入时会自动 级联插入 边界以内的内容。

var order = new Order
{
    Field2 = "field2",
    Extdata = new OrderExt { Field3 = "field3" },
    Details = new List<OrderDetail>
    {
        new OrderDetail { Field4 = "field4_01", Extdata = new OrderDetailExt { Field5 = "field5_01" } },
        new OrderDetail { Field4 = "field4_02", Extdata = new OrderDetailExt { Field5 = "field5_02" } },
        new OrderDetail { Field4 = "field4_03", Extdata = new OrderDetailExt { Field5 = "field5_03" } },
    },
    Tags = fsql.Select<Tag>().Where(a => new [] { 1,2,3 }.Contains(a.Id)).ToList()
};
repository.Insert(order); //级联插入
  • 插入 Order 表记录;
  • 插入 OrderExt 表记录;
  • 插入 OrderDetail 表记录;
  • 插入 OrderDetailExt 表记录;
  • 插入 OrderTag 表记录;(不会插入 Tag 表记录)

注意:即使 order.Tags 在数据库不存在,也不会插入 Tag 表记录

查询数据

根据上面设定的边界,查询时会自动 Include/IncludeMany 边界以内的内容。

var list = repository.Select
    .Where(a => a.Id < 10)
    .ToList();

效果等同于:

var list = fsql.Select<Order>()
    .Include(a => a.Extdata)
    .IncludeMany(a => a.Details,
        then => then.Include(b => b.Extdata))
    .IncludeMany(a => a.Tags)
    .Where(a => a.Id < 10)
    .ToList();

扩展查询边界:

提示:[AggregateRootBoundary("name", Break = true)] 设置边界范围,请往后面看。。

class OrderRepository : AggregateRootRepository<Order>
{
    public OrderRepository(IFreeSql fsql, UnitOfWorkManager uowManager) : base(uowManager?.Orm ?? fsql)
    {
        Console.WriteLine(AggregateRootUtils.GetAutoIncludeQueryStaicCode(null, fsql, typeof(Order)));
        //控制台输出一块 Include/IncludeMany 字符串,内容与下方 SelectDiy 代码块相同
    }

    public override ISelect<IFreeSql> Select => this.SelectDiy
        //.TrackToList(this.SelectAggregateRootTracking) 状态跟踪
        .Include(a => a.Extdata)
        .IncludeMany(a => a.Details,
            then => then.Include(b => b.Extdata))
        .IncludeMany(a => a.Tags);
}

重写 Select 可以把边界以外的数据一起查询出来(例如 ManyToOne 导航属性),但是 添加/修改/删除 仍然采用默认边界规则

手工使用 SelectDiy Include/IncludeMany 包含内容,如果小于默认边界规则,则建议不要开启 状态跟踪 (保存数据可能造成不一致),反之则应该开启。(详细请往后看 更新数据

删除数据

根据上面设定的边界,删除时会自动 级联删除 边界以内的内容。

repository.Delete(order);
  • 删除 OrderExt 表对应的记录;
  • 删除 OrderDetailExt 表对应的记录;
  • 删除 OrderDetail 表对应的记录;
  • 删除 OrderTag 表对应的记录;(不会删除 Tag 表记录)
  • 删除 Order 表对应的记录;

删除数据是在内存递归 order 实例进行的,因此需要使用 repository 提前查询,内容庞大时有性能缺陷。

如果设置了数据库表外键的级联删除功能,则只需删除 Order 表对应的记录,并且不需要提前查询。

更新数据

根据上面设定的边界,更新时会自动 级联保存 边界以内的内容。

repository.Attach 存储更新前的数据快照(查询会自动快照),称为副本,repository.Update 的时候和副本进行级联对比保存。

var order = repository.Select.Where(a => a.Id == 1).First(); //此时已自动 Attach
order.Tags.Add(new Tag { Id = 4 });
order.Details.RemoveAt(1);
order.Details[0].Extdata.Field5 = "field5_01_01";
order.Field2 = "field2_02";
repository.Update(order);
  • 添加 OrderTag 表记录;(不会管理 Tag 表记录)
  • 删除 OrderDetail 表记录;
  • 删除 OrderDetailExt 表记录;
  • 更新 OrderDetailExt 表记录;
  • 更新 Order 表记录;

完整保存 先查询再更新,机制容易理解,数据一致性也更有保障。但是如果聚合根下内容较庞大,将会造成性能问题。

例如 Order 下面的评论数据大约有 1000 条,每天还不断有新的记录,每次 Load 内存再保存代价就太大了。

利用对比保存的特点,可以变向实现 追加记录

class Order
{
    // ..
    [Navigate(nameof(OrderComment.OrderId))]
    public List<OrderComment> Comments { get; set; }
}
class OrderComment
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public int OrderId { get; set; }
    public string Field6 { get; set; }
}

var order = fsql.Select<Order>()
    .Where(a => a.Id == 1)
    .First(); //单表数据
repository.Attach(order); //快照时 Comments 是 NULL/EMPTY
order.Comments = new List<OrderComment>();
order.Comments.Add(new OrderComment { Field6 = "field6_01" });
order.Comments.Add(new OrderComment { Field6 = "field6_02" });
repository.Update(order);
  • 使用 fsql 只查询了单表数据;
  • order 本身没发生变化,所以不更新 Order 表记录;
  • 添加 OrderComment 表记录2条;

我为什么不直接对 OrderComment 进行单表操作啊???

答案你们回答!!!

对比保存 规则说明:

| 导航属性 | 副本 | 最新 | 结果 | |

]]>
FreeSql+CAP事务 https://freesql.net/extra/freesql-cap.html https://freesql.net/extra/freesql-cap.html FreeSql+CAP事务 FreeSql+CAP事务 背景描述 在CAP中,事务对象需要交给CAP进行提交从而在事务实现提交后对缓存消息到 Broker 的 Flush 动作,而目前的Orm大部分都有自己的事务管理对象进行事务的提交。CAP官方直接原生支持使用 ADO.NET 和 EntityFrameworkCore 进行事务集成,而对于第三方ORM本文提供了一种扩展用以集成... Wed, 31 Aug 2022 14:37:49 GMT 背景描述

在CAP中,事务对象需要交给CAP进行提交从而在事务实现提交后对缓存消息到 Broker 的 Flush 动作,而目前的Orm大部分都有自己的事务管理对象进行事务的提交。CAP官方直接原生支持使用 ADO.NET 和 EntityFrameworkCore 进行事务集成,而对于第三方ORM本文提供了一种扩展用以集成的示例。

接入有二种方式

  • 安装FreeSql
dotnet add package FreeSql
dotnet add package FreeSql.DbContext
dotnet add package FreeSql.Provider.MySqlConnector
  • 安装CAP相关包
dotnet add package Savorboard.CAP.InMemoryMessageQueue
dotnet add package DotNetCore.CAP.MySql
dotnet add package DotNetCore.CAP.Dashboard

服务

  • appsetttings.json
{
  "ConnectionStrings": {
    "MySql": "Data Source=localhost;Port=3306;User ID=root;Password=root;Initial Catalog=lincms;Charset=utf8mb4;SslMode=none;Max pool size=1;Connection LifeTime=20"
  }
}
  • 配置相关服务
 IConfigurationSection mysqlSelection = configuration.GetSection($"ConnectionStrings:MySql");
    IFreeSql fsql = new FreeSqlBuilder()
                .UseConnectionString(DataType.MySql,mysqlSelection.Value)
                .UseNameConvert(NameConvertType.PascalCaseToUnderscoreWithLower)
                .UseAutoSyncStructure(true)
                .UseMonitorCommand(cmd => { Trace.WriteLine(cmd.CommandText + ";"); })
                .Build()
    services.AddSingleton(fsql);
    services.AddFreeRepository();
    services.AddScoped<UnitOfWorkManager>();

至少你要配置一个消息队列和一个事件存储(UseMySql)

  services.AddCap(x =>
    {
        x.UseInMemoryMessageQueue();
        x.UseMySql(opt=>{
            //MySqlOptions
            opt.ConnectionString = mysqlSelection.Value
        });
        // x.UseXXX ...
         x.UseDashboard();
    });

默认情况下,你可以访问 http://localhost:xxx/cap 这个地址打开Dashboard。

方式一

    public static class CapUnitOfWorkExtensions
    {
        public static void Flush(this ICapTransaction capTransaction)
        {
            capTransaction?.GetType().GetMethod("Flush", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(capTransaction, null);
        }
        public static ICapTransaction BeginTransaction(this IUnitOfWork unitOfWork, ICapPublisher publisher, bool autoCommit = false)
        {
            var dbTransaction = unitOfWork.GetOrBeginTransaction();
            publisher.Transaction = ActivatorUtilities.CreateInstance<MySqlCapTransaction>(publisher.ServiceProvider);
            publisher.Transaction.DbTransaction = dbTransaction;
            publisher.Transaction.AutoCommit = autoCommit;
            return publisher.Transaction;
        }

        public static void Commit(this ICapTransaction capTransaction, IUnitOfWork unitOfWork)
        {
            unitOfWork.Commit();
            capTransaction.Flush();
        }
 }
  • 使用方式
public class BookController : Controller
{
    [HttpGet("~/freesql/unitofwork")]
    public DateTime FreeSqlUnitOfWorkManagerTransaction([FromServices] IBaseRepository<Book> repo,
    [FromServices] UnitOfWorkManager unitOfWorkManager,
    [FromServices] ICapPublisher capBus
    )
    {
        DateTime now = DateTime.Now;
        using (IUnitOfWork uow = unitOfWorkManager.Begin())
        {
            ICapTransaction trans = uow.BeginTransaction(capBus, false);

            repo.Insert(new Book()
            {
                Author = "叶老板",
                Title = "FreeSql源码解析与实战",
                Summary = "带你了解FreeSql源码细节,掌握FreeSql的实战操作,扩展FreeSql的功能,提升你的开发效率。"
            });

            capBus.Publish("freesql.time", now);
            trans.Commit(uow);
        }
        return now;
    }
    [NonAction]
    [CapSubscribe("freesql.time")]
    public void GetTime(DateTime time)
    {
        Console.WriteLine($"time:{time}");
    }
}

[Table(Name = "book")]
public class Book
{
   [Column(IsIdentity = true, IsPrimary = true)]
    public long Id { get; set; }

    [Column(StringLength = 30)]
    public string Author { get; set; }

    [Column(StringLength = 1000)]
    public string Summary { get; set; }

    [Column(StringLength = 50)]
    public string Title { get; set; }

}

方式二

public class FreeSqlRepositoryPatternTransaction : CapTransactionBase
{
    public FreeSqlRepositoryPatternTransaction(IDispatcher dispatcher, IUnitOfWork uow) : base(dispatcher)
    {
        Uow = uow;
    }

    public IUnitOfWork Uow { get; }

    public override object? DbTransaction => Uow.GetOrBeginTransaction();

    public override void Commit()
    {
        Uow.Commit();
        Flush();
    }

    public override Task CommitAsync(CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }

    public override void Rollback()
    {
        Uow.Rollback();
    }

    public override Task RollbackAsync(CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }

    public override void Dispose()
    {
        Uow.Dispose();
    }
}

public static class Extensions
{
      // 注意:你可以酌情修改此扩展以支持你的使用习惯,参考下方讨论内容
      public static ICapTransaction BeginTransaction(this IUnitOfWork unitOfWork, ICapPublisher publisher, bool autoCommit = false)
      {
            var dispatcher = publisher.ServiceProvider.GetRequiredService<IDispatcher>();
            var transaction = new FreeSqlRepositoryPatternTransaction(dispatcher, unitOfWork)
            {
                AutoCommit = autoCommit
            };
            return publisher.Transaction.Value = transaction;
      }

使用方式:

[HttpGet("~/freesql/Withtransaction")]
public DateTime WithTransaction([FromServices] IBaseRepository<Book> repo,
[FromServices] UnitOfWorkManager unitOfWorkManager,
[FromServices] ICapPublisher capBus
)
{
    DateTime now = DateTime.Now;
    using (IUnitOfWork uow = unitOfWorkManager.Begin())
    {
        ICapTransaction trans = uow.BeginTransaction(capBus, false);

        repo.Insert(new Book()
        {
            Author = "叶老板",
            Title = "FreeSql源码解析与实战",
            Summary = "带你了解FreeSql源码细节,掌握FreeSql的实战操作,扩展FreeSql的功能,提升你的开发效率。"
        });

        capBus.Publish("freesql.time", now);
        trans.Commit();
    }
    return now;
}

二者区别在于后者,trans.Commit()不需要传递``IUnitOfWork`参数

原文参考

]]>
FreeSql.AdminLTE https://freesql.net/guide/freesqladminlte.html https://freesql.net/guide/freesqladminlte.html FreeSql.AdminLTE FreeSql.AdminLTE 它是 FreeSql 衍生出来的 .NETCore MVC 中间件扩展包 FreeSql.AdminLTE.Preview.dll,基于 AdminLTE 前端框架动态产生实体的增删查改界面。 推荐使用:AdminBlazor 集成了菜单、角色、用户、公司组织、定时任务、数据字典、参数配置、租户、审批、审计等功能。 d... Tue, 30 Aug 2022 23:01:53 GMT 它是 FreeSql 衍生出来的 .NETCore MVC 中间件扩展包 FreeSql.AdminLTE.Preview.dll,基于 AdminLTE 前端框架动态产生实体的增删查改界面。

推荐使用:AdminBlazor 集成了菜单、角色、用户、公司组织、定时任务、数据字典、参数配置、租户、审批、审计等功能。

dotnet add packages FreeSql.AdminLTE.Preview

输入:实体1、实体2、实体3

输出:后台管理的功能

app.UseFreeAdminLtePreview("/",
    typeof(Config),
    typeof(Role),
    typeof(Menu),
    typeof(User),

    typeof(Department),
    typeof(Employee),
    typeof(Position),

    typeof(AppLog),
    typeof(LoginLog),
    typeof(OprationLog),

    typeof(FreeScheduler.TaskInfo),
    typeof(FreeScheduler.TaskLog)
);

只需要传入实体,就可以生产 curd 的管理功能,是不是有些骚啊~~~

image
image
image
image

对于通用后台管理系统的生成,除了单纯的对单表 crud 操作外,我还喜欢利用导航属性的操作,比如:

1、Song、Tag 多对多场景,添加/更新 Song 时可以把 Tag 一起保存;

2、列表页,希望外键、多对多出现在过滤筛选条件;

3、列表页,希望枚举出现在过滤筛选条件;

4、删除时,级联删除所有相关数据;

等等诸如此类的繁琐操作,之所以说繁琐,是因为这些工作技术不难,属于严重的重复劳动。

机制设定

1、添加、修改数据

中件间产生的界面包括添加、修改数据的功能,普通实体的根据属性的类型与 Html5 UI 一一映射;

比较特殊的映射规则:

| c# 类型 | Html5 | |

]]>
FreeIM https://freesql.net/guide/freeim.html https://freesql.net/guide/freeim.html FreeIM FreeIM FreeIM v2.0.0 调整:已将 ClientId Guid 改为 long; 修复:JoinChan/LeaveChan 数量统计问题; 增加:SendBroadcastMessage 广播消息; 优化:SendChanMessage 性能; FreeIM 使用 websocket 协议实现简易、高性能(单机支持5万+连接)、集群... Tue, 30 Aug 2022 05:43:50 GMT FreeIM v2.0.0
  • 调整:已将 ClientId Guid 改为 long;
  • 修复:JoinChan/LeaveChan 数量统计问题;
  • 增加:SendBroadcastMessage 广播消息;
  • 优化:SendChanMessage 性能;

FreeIM 使用 websocket 协议实现简易、高性能(单机支持5万+连接)、集群即时通讯组件,支持点对点通讯、群聊通讯、上线下线事件消息等众多实用性功能。

使用场景:好友聊天、群聊天、直播间、实时评论区、游戏。

接受定制项目开发,详细请联系作者

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

扩展资料:《C#.NET im 聊天通讯架构设计 -- FreeIM 支持集群、职责分明、高性能》

dotnet add package FreeIM

定制服务

  • 前端:Flutter、uniapp
  • 后端:.NET10 + Redis + MySql + Clickhouse + Kafka (高性能、万人群、群直播)
  • 功能:好友、群、签到、红包、直播、语聊、后台管理系统、系统推送
  • 配置:会员等级、谁不能删除谁、谁能领红包等等
  • 价格:根据功能谈价
image image
image image
image image
image
image

ImServer 服务端

public void Configure(IApplicationBuilder app)
{
    app.UseFreeImServer(new ImServerOptions
    {
        Redis = new FreeRedis.RedisClient("127.0.0.1:6379,poolsize=5"),
        Servers = new[] { "127.0.0.1:6001" }, //集群配置
        Server = "127.0.0.1:6001"
    });
}
//dotnet run --urls=http://127.0.0.1:6001

一套永远不需要迭代更新的 ImServer 服务端,支持 .NET6.0、.NETCore2.1+、NETStandard2.0

WebApi 业务端

public void Configure(IApplicationBuilder app)
{
    //...

    ImHelper.Initialization(new ImClientOptions
    {
        Redis = new FreeRedis.RedisClient("127.0.0.1:6379,poolsize=5"),
        Servers = new[] { "127.0.0.1:6001" }
    });

    ImHelper.EventBus(
        t => Console.WriteLine(t.clientId + "上线了"),
        t => Console.WriteLine(t.clientId + "下线了"));
}

| ImHelper方法 | 参数 | 描述 | |

]]>
FreeScheduler https://freesql.net/guide/freescheduler.html https://freesql.net/guide/freescheduler.html FreeScheduler FreeScheduler FreeScheduler 是利用 IdleBus 实现的轻量化定时任务调度,支持集群、临时的延时任务和重复循环任务(可持久化),可按秒,每天/每周/每月固定时间,自定义间隔执行,支持 .NET Core 2.1+、.NET Framework 4.0+ 运行环境。 开源地址:https://github.com/28810... Tue, 30 Aug 2022 05:41:38 GMT FreeScheduler 是利用 IdleBus 实现的轻量化定时任务调度,支持集群、临时的延时任务和重复循环任务(可持久化),可按秒,每天/每周/每月固定时间,自定义间隔执行,支持 .NET Core 2.1+、.NET Framework 4.0+ 运行环境。

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

扩展资料:《.NET 定时任务 -- FreeScheduler 支持 cron、持久化、可变定时设置》

快速开始

dotnet add package FreeScheduler

Install-Package FreeScheduler

static Scheduler scheduler = new FreeSchedulerBuilder()
    .UseTimeZone(TimeSpan.FromHours(8)) //默认为UTC时间,国内可指定时区+8,任务将按本地时间执行
    .OnExecuting(task =>
    {
        Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss.fff")}] {task.Topic} 被执行");
        switch (task.Topic)
        {
            case "武林大会": Wulin(task.Body); break;
            case "攻城活动": AttackCity(task.Body); break;
        }
    })
    .Build();

| Method | 说明 | |

]]>
FreeRedis https://freesql.net/guide/freeredis.html https://freesql.net/guide/freeredis.html FreeRedis FreeRedis CSRedisCore 是 .NETFramework 4.0 及以上 访问 redis-server 的客户端组件,也是 FreeSql 作者早年发布的 nuget 版本。 后来重构了更简易的 FreeRedis,目前推荐大家使用 FreeRedis,支持几乎所有 .NET 平台和 AOT。 开源地址:https://github... Tue, 30 Aug 2022 05:39:40 GMT CSRedisCore 是 .NETFramework 4.0 及以上 访问 redis-server 的客户端组件,也是 FreeSql 作者早年发布的 nuget 版本。

后来重构了更简易的 FreeRedis,目前推荐大家使用 FreeRedis,支持几乎所有 .NET 平台和 AOT。

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

  • 🌈 所有方法名与 redis-cli 保持一致
  • 🌌 支持 Redis 集群(服务端要求 3.2 及以上版本)
  • ⛳ 支持 Redis 哨兵模式
  • 🎣 支持主从分离(Master-Slave)
  • 📡 支持发布订阅(Pub-Sub)
  • 📃 支持 Redis Lua 脚本
  • 💻 支持管道(Pipeline)、支持事务、延迟队列、RediSearch
  • 🌴 支持 GEO 命令(服务端要求 3.2 及以上版本)
  • 🌲 支持 STREAM 类型命令(服务端要求 5.0 及以上版本)
  • ⚡ 支持本地缓存(Client-side-cahing,服务端要求 6.0 及以上版本)
  • 🌳 支持 Redis 6 的 RESP3 协议

🚀 快速入门

public static RedisClient cli = new RedisClient("127.0.0.1:6379,password=123,defaultDatabase=13");
//cli.Serialize = obj => JsonConvert.SerializeObject(obj);
//cli.Deserialize = (json, type) => JsonConvert.DeserializeObject(json, type);
cli.Notice += (s, e) => Console.WriteLine(e.Log); //打印命令日志

cli.Set("key1", "value1");
cli.MSet("key1", "value1", "key2", "value2");

string value1 = cli.Get("key1");
string[] vals = cli.MGet("key1", "key2");

支持 STRING、HASH、LIST、SET、ZSET、BITMAP、HyperLogLog、GEO、Stream 以及布隆过滤器等。

| 参数 | 默认值 | 说明 | | :

]]>
其他作品 https://freesql.net/guide/otherworks.html https://freesql.net/guide/otherworks.html 其他作品 其他作品 FreeSql 作者是一个入行 18 年的老批,他目前写的开源项目还有: 更早的作品可以直接访问 https://github.com/2881099 查看。 AdminBlazor AdminBlazor 是一款 Blazor Server SaaS 中台项目,支持 RABC 权限菜单/按钮,支持快速代码生成一对一、一对多、多对多导航属性的... Tue, 30 Aug 2022 05:35:49 GMT FreeSql 作者是一个入行 18 年的老批,他目前写的开源项目还有:

| 开源项目 | 描述 | 开源地址 | 开源协议 | |

]]>
联合查询 https://freesql.net/guide/unionall.html https://freesql.net/guide/unionall.html 联合查询 联合查询 在之前都是推荐使用 ToSql + WithSql 完成联合查询操作,v3.2.666 新增功能直接使用 UnionAll 方法。 GroupBy + WithTempQuery(嵌套查询) + FromQuery + UnionAll 组合使用,会让查询功能更加强大、灵活。 单表 UNION ALL 多表 UNION ALL 注意:如上 S... Fri, 26 Aug 2022 09:40:43 GMT 在之前都是推荐使用 ToSql + WithSql 完成联合查询操作,v3.2.666 新增功能直接使用 UnionAll 方法。

GroupBy + WithTempQuery(嵌套查询) + FromQuery + UnionAll 组合使用,会让查询功能更加强大、灵活。

单表 UNION ALL

var sql = fsql.Select<User>().Where(a => a.Id == 1)
    .UnionAll(
        fsql.Select<User>().Where(a => a.Id == 2),
        fsql.Select<User>().Where(a => a.Id == 3)
    )
    .Where(a => a.Id == 1 || a.Id == 2)
    .ToSql();
SELECT a."Id", a."GroupId", a."Username"
FROM ( SELECT a."Id", a."GroupId", a."Username"
    FROM "User" a
    WHERE (a."Id" = 1)
    UNION ALL
    SELECT a."Id", a."GroupId", a."Username"
    FROM "User" a
    WHERE (a."Id" = 2)
    UNION ALL
    SELECT a."Id", a."GroupId", a."Username"
    FROM "User" a
    WHERE (a."Id" = 3) ) a
WHERE ((a."Id" = 1 OR a."Id" = 2))

多表 UNION ALL

var sql = fsql.Select<User, Group>()
    .InnerJoin((a, b) => a.GroupId == b.Id)
    .Where((a, b) => a.Id == 1)
    .WithTempQuery((a, b) => new { user = a, group = b }) //匿名类型

    .UnionAll(
        fsql.Select<User, Group>()
            .InnerJoin((a, b) => a.GroupId == b.Id)
            .Where((a, b) => a.Id == 2)
            .WithTempQuery((a, b) => new { user = a, group = b }) //匿名类型
    )
    .Where(a => a.user.Id == 1 || a.user.Id == 2)
    .ToSql();
SELECT *
FROM ( SELECT *
    FROM (
        SELECT a."Id", a."GroupId", a."Username", b."Id", b."GroupName"
        FROM "User" a
        INNER JOIN "UserGroup" b ON a."GroupId" = b."Id"
        WHERE (a."Id" = 1) ) a
    UNION ALL
    SELECT *
    FROM (
        SELECT a."Id", a."GroupId", a."Username", b."Id", b."GroupName"
        FROM "User" a
        INNER JOIN "UserGroup" b ON a."GroupId" = b."Id"
        WHERE (a."Id" = 2) ) a ) a
WHERE ((a."Id" = 1 OR a."Id" = 2))

注意:如上 SQL 会执行报错,因为 User、UserGroup 都存在相同的 Id 字段名称,暂时的解决办法需要指定字段

    .WithTempQuery((a, b) => new
    {
        user = a,
        group = new
        {
            GroupId = b.Id,
            GroupName = b.GroupName
        }
    })

WithParameters 参数化共享

开启参数化查询功能后,使用 WithParameters 共享参数化,可避免产生相同的参数名称:

var dbpars = new List<DbParameter>();

var id1 = 1;
var id2 = 2;
var sql = fsql.Select<User>()
    .WithParameters(dbpars)
    .Where(a => a.Id == id1)

    .UnionAll(
        fsql.Select<User>()
            .WithParameters(dbpars)
            .Where(a => a.Id == id2)
    )
    .Where(a => a.Id == 1 || a.Id == 2)
    .ToSql();
SELECT a."Id", a."GroupId", a."Username"
FROM ( SELECT a."Id", a."GroupId", a."Username"
    FROM "User1" a
    WHERE (a."Id" = @exp_0)
    UNION ALL
    SELECT a."Id", a."GroupId", a."Username"
    FROM "User1" a
    WHERE (a."Id" = @exp_1) ) a
WHERE ((a."Id" = 1 OR a."Id" = 2))
]]>
动态操作 https://freesql.net/guide/dynamic.html https://freesql.net/guide/dynamic.html 动态操作 动态操作 弱类型 CRUD v3.2.695 emit 动态创建实体类型 字典 CUD InsertDict/UpdateDict/DeleteDict/InsertOrUpdateDict 都支持批量操作,对应类型 List&lt;Dictionary&lt;string, object&gt;&gt; 无类型 CRUD(更高级) 不依赖实体类型,不需要动态编译,纯字典操作... Mon, 15 Aug 2022 16:01:12 GMT 弱类型 CRUD
fsql.Insert<object>().AsType(实体类型)
  .AppendData(data).ExecuteAffrows();

fsql.Update<object>().AsType(实体类型)
  .SetSource(data).ExecuteAffrows();

fsql.Delete<object>().AsType(实体类型)
  .Where(a => (a as BaseEntity).Id == 1).ExecuteAffrows();

//fsql.Select<object>()...

//或者仓储
var repo = fsql.GetRepository<object>();
repo.AsType(实体类型);

repo.Insert(..);
repo.Update(..);
repo.Delete(..);
repo.InsertOrUpdate(..);

v3.2.695 emit 动态创建实体类型

var table = fsql.CodeFirst.DynamicEntity("user", new TableAttribute { Name = "t_user" })
  .Property("id", typeof(int), new ColumnAttribute { IsIdentity = true, IsPrimary = rue })
  .Property("username", typeof(string), new ColumnAttribute { StringLength = 32 })
  .Build();

//如果有必要,请将 table 缓存起来
if (fsql.DbFirst.ExistsTable(table.DbName) == false)
    fsql.CodeFirst.SyncStructure(table.Type); //创建表

var dict = new Dictionary<string, object>();
dict["id"] = 1;
dict["username"] = "xxx";

//将字典转化成 type 对应的 object
//也可以直接使用 InsertDict/UpdateDict/DeleteDict 等字典 CUD 功能
object obj = table.CreateInstance(dict);

fsql.Insert<object>().AsType(table.Type).AppendData(obj).ExecuteAffrows();
fsql.Update<object>().AsType(table.Type).SetSource(obj).ExecuteAffrows();
fsql.InsertOrUpdate<object>().AsType(table.Type).SetSource(obj).ExecuteAffrows();
fsql.Delete<object>().AsType(table.Type).WhereDynamic(obj).ExecuteAffrows();
List<object> objs = fsql.Select<object>().AsType(table.Type).ToList();

字典 CUD

var dic = new Dictionary<string, object>();
dic.Add("id", 1);
dic.Add("name", "xxxx");

fsql.InsertDict(dic).AsTable("table1").ExecuteAffrows();
fsql.UpdateDict(dic).AsTable("table1").WherePrimary("id").ExecuteAffrows();
fsql.DeleteDict(dic).AsTable("table1").ExecuteAffrows();
fsql.InsertOrUpdateDict(dic).AsTable("table1").WherePrimary("id").ExecuteAffrows();

InsertDict/UpdateDict/DeleteDict/InsertOrUpdateDict 都支持批量操作,对应类型 List<Dictionary<string, object>>

无类型 CRUD(更高级)

不依赖实体类型,不需要动态编译,纯字典操作,支持导航属性,级联保存,AOT 编译福音。

内容较长,请移步《低代码》

动态表名

1、仓储 Repository

var repo = fsql.GetRepository<Log>();
repo.AsTable(old => $"{old}_201903"); //对 Log_201903 表 CRUD
//repo.AsTable((t, old) => $"{old}_201903"); //对 Log_201903 表 CRUD(级联有关表也增加该后辍)
repo.Insert(new Log { ... });

2、原生 IFreeSql

fsql.Select<Log>().AsTable((t, old) => $"{old}_201903").ToList(); //对 Log_201903 表查询
fsql.Insert(new Log { ... }).AsTable("Log_201903").ExecuteAffrows(); //对 Log_201903 表插入
fsql.Update<Log>().AsTable("Log_201903").SetSource(item).ExecuteAffrows(); //对 Log_201903 表更新
fsql.Delete<Log>().AsTable("Log_201903").Where(a => a.Id == 1).ExecuteAffrows(); //对 Log_201903 表删除
fsql.InsertOrUpdate<Log>().AsTable("Log_201903").SetSource(item).ExecuteAffrows(); //对 Log_201903 表插入或更新

动态条件

1、ISelect.Where(string sql) 使用原生条件:

fsql.Select<Region>().Where("a.id > 0") //提示:存在SQL注入安全问题

2、动态 Lambda 表达式

Expression<Func<Region, bool>> where = null;
where = where.And(b => b.Id > 10);
where = where.Or(b => b.Id == 1);
fsql.Select<Region>().Where(where).ToList();
//WHERE id > 10 OR id = 1

3、ISelect.WhereDynamicFilter 方法实现动态过滤条件(与前端交互),支持的操作符:

  • Contains/StartsWith/EndsWith/NotContains/NotStartsWith/NotEndsWith:包含/不包含,like '%xx%',或者 like 'xx%',或者 like '%xx'
  • Equal/NotEqual:等于/不等于
  • GreaterThan/GreaterThanOrEqual:大于/大于等于
  • LessThan/LessThanOrEqual:小于/小于等于
  • Range:范围查询
  • DateRange:日期范围,有特殊处理 value[1] + 1
  • Any/NotAny:是否符合 value 中任何一项(直白的说是 SQL IN)
  • Custom:自定义解析
DynamicFilterInfo dyfilter = JsonConvert.DeserializeObject<DynamicFilterInfo>(@"
{
  ""Logic"": ""And"",
  ""Filters"":
  [
    { ""Field"": ""id"", ""Operator"": ""Equals"", ""Value"": 1 },
    {
      ""Logic"": ""Or"",
      ""Filters"":
      [
        { ""Field"": ""id"", ""Operator"": ""Equals"", ""Value"": 2 },
        { ""Field"": ""id"", ""Operator"": ""Equals"", ""Value"": 3 }
      ]
    }
  ]
}");
fsql.Select<Region>().WhereDynamicFilter(dyfilter).ToList();
//WHERE id = 1 AND (id = 2 OR id = 3)

《高效理解 FreeSql WhereDynamicFilter,深入了解设计初衷》

实现 Custom 的例子:

{
  "Logic": "And",
  "Filters": [
    { "Field": "id", "Operator": "Equals", "Value": 1 },
    {
      "Logic": "Or",
      "Filters": [
        { "Field": "id", "Operator": "Equals", "Value": 2 },
        {
          "Field": "{{ DynamicFilterCustomImpl.CustomLinq }}",
          "Operator": "Custom",
          "Value": "Title.StartsWith('new topic 1')"
        }
      ]
    }
  ]
}
var dyfilter = JsonConvert.DeserializeObject<DynamicFilterInfo>(json);
fsql.Select<Topic>().WhereDynamicFilter(dyfilter).ToList();
// WHERE id = 1 AND (id = 2 OR title like 'new topic 1%')

//nuget 安装 System.Linq.Dynamic.Core
public class DynamicFilterCustomImpl
{
    //JSON Field 对应这个值
    public static string CustomLinq = $"{nameof(DynamicFilterCustomImpl.DynamicLinq)} {typeof(DynamicFilterCustomImpl).FullName},{typeof(DynamicFilterCustomImpl).Assembly.FullName}";

    [DynamicFilterCustom]
    public static LambdaExpression DynamicLinq(object sender, string value)
    {
        if (string.IsNullOrWhiteSpace(value)) value = "1==2";
        ParameterExpression t = Expression.Parameter(sender.GetType().GetGenericArguments()[0], "t");
        var exp = DynamicExpressionParser.ParseLambda(new ParameterExpression[] { t }, typeof(bool), value);
        return exp;
    }
}

动态排序

1、ISelect.OrderBy(string sql) 使用原生排序:

fsql.Select<Region>().OrderBy("a.id desc") //提示:存在SQL注入安全问题

2、ISelect.OrderByPropertyName 使用属性名排序:

  • 支持导航属性,比如 OrderByPropertyName("Parent.Code")
  • 支持多表查询,比如 OrderByPropertyName("b.Code")

动态贪婪加载

1、ISelect.IncludeByPropertyName 方法实现动态贪婪加载,对应 Include/IncludeMany:

fsql.Select<Region>()
    .IncludeByPropertyName("Parent.Parent.Parent")
    .IncludeByPropertyName("Childs")

    .IncludeByPropertyName("Childs", then => then
        .IncludeByPropertyName("Parent.Parent")
        .IncludeByPropertyName("Parent.Childs"))
    .ToList();

2、List<TDto>.IncludeByPropertyName 扩展方法也实现了 OneToMany 动态贪婪加载:

非实体类型,也可以级联加载,他们不需要配置导航属性关系。

var dtos = fsql.Select<Region>().ToList<Dto>();

dtos.IncludeByPropertyName(
    orm: fsql,
    property: "Childs",
    where: "ParentId=Id", //临时关系
    take: 5,
    select: "id,name",
    then => then.IncludeByPropertyName("Parent")
);

动态返回数据

1、ISelect.ToList 使用原生SQL返回数据:

List<(int, string)> list = fsql.Select<Region>()
    .ToList<(int, string)>("a.id,a.name") //提示:存在SQL注入安全问题

2、ISelect.ToDataTableByPropertyName 使用属性名返回数据:

DataTable dt = fsql.Select<Region>()
    .ToDataTableByPropertyName(new [] {
        "Parent.Code",
        "b.Id"
    });

动态片段

FreeSql 提供 Where(sql)、GroupBy(sql)、OrderBy(sql)、ToList(sql) 等直接使用 SQL 片段的 API。

使用这些 API 时请务必注意SQL注入安全问题。

不建议前端直接 POST SQL 到后端使用它们,而应该在后端做一层映射,例如:

var whereMapping = new Dictionary<string, string>
{
    ["where1"] = "a.id > {0}",
    ["where2"] = "len(a.name) > {0}"
};
var orderByMapping = new Dictionary<string, string>
{
    ["order1"] = "a.id asc, a.name desc",
    ["order2"] = "len(a.name) desc"
};

//假设前端 POST 内容是 postWhere=where1&postWhereValue=100&postOrder=order1
fsql.Select<Region>()
    .WhereIf(
        whereMapping.TryGetValue(postWhere, out var whereSql),
        string.Format(whereSql, postWhereValue)
    )
    .OrderBy(
        orderByMapping.TryGetValue(postOrder, out var orderSql),
        orderSql
    )
]]>
BaseEntity https://freesql.net/extra/freesql-extensions-baseentity.html https://freesql.net/extra/freesql-extensions-baseentity.html BaseEntity 前言 尝试过 ado.net、dapper、ef,以及Repository仓储,甚至自己还写过生成器工具,以便做常规CRUD操作。 它们日常操作不方便之处: 每次使用前需要声明,再操作; 很多人一个实体类,对应一个操作类(或DAL、DbContext、Repository); BaseEntity 是一种极简单的 CodeFirst 开发方式,特别对单... Thu, 11 Aug 2022 15:34:05 GMT 前言

尝试过 ado.net、dapper、ef,以及Repository仓储,甚至自己还写过生成器工具,以便做常规CRUD操作。

它们日常操作不方便之处:

  • 每次使用前需要声明,再操作;

  • 很多人一个实体类,对应一个操作类(或DAL、DbContext、Repository);

BaseEntity 是一种极简单的 CodeFirst 开发方式,特别对单表或多表CRUD,利用继承节省了每个实体类的重复属性(创建时间、ID等字段),软删除等功能,进行 crud 操作时不必时常考虑仓储的使用;

本文介绍 BaseEntity 一种极简约的 CRUD 操作方法。

功能特点

  • 自动迁移实体结构(CodeFirst),到数据库;

  • 直接操作实体的方法,进行 CRUD 操作;

  • 简化用户定义实体类型,省去主键、常用字段的配置(如CreateTime、UpdateTime);

  • 实现单表、多表查询的软删除逻辑;

声明

dotnet add package FreeSql.Extensions.BaseEntity
dotnet add package FreeSql.Provider.Sqlite
BaseEntity.Initialization(fsql, null);

1、定义一个主键 int 并且自增的实体类型,BaseEntity TKey 指定为 int/long 时,会认为主键是自增;

public class UserGroup : BaseEntity<UserGroup, int>
{
    public string GroupName { get; set; }
}

如果不想主键是自增键,可以重写属性:

public class UserGroup : BaseEntity<UserGroup, int>
{
    [Column(IsIdentity = false)]
    public override int Id { get; set; }
    public string GroupName { get; set; }
}

有关更多实体的特性配置,请参考资料:实体特性

2、定义一个主键 Guid 的实体类型,保存数据时会自动产生有序不重复的 Guid 值(不用自己指定 Guid.NewGuid());

public class User : BaseEntity<UserGroup, Guid>
{
    public string UserName { get; set; }
}

CRUD 使用

//添加
var item = new UserGroup { GroupName = "组一" };
item.Insert();

//更新
item.GroupName = "组二";
item.Update();

//添加或更新
item.Save();

//软删除
item.Delete();

//恢复软删除
item.Restore();

//根据主键获取对象
var item = UserGroup.Find(1);

//查询数据
var items = UserGroup.Where(a => a.Id > 10).ToList();

实体类型.Select 是一个查询对象,使用方法和 FreeSql.ISelect 一样;

支持多表查询时,软删除条件会附加在每个表中;

有关更多查询方法,请参考资料:查询

事务建议

1、同线程事务,不支持异步:

fsql.Transaction(() =>
{
    //todo ...
})

2、如果你是异步控

由于 AsyncLocal 平台兼容不好,所以交给外部管理事务。

static AsyncLocal<IUnitOfWork> _asyncUow = new AsyncLocal<IUnitOfWork>();

BaseEntity.Initialization(fsql, () => _asyncUow.Value);

在 Scoped 开始时:_asyncUow.Value = fsql.CreateUnitOfWork(); (也可以使用 UnitOfWorkManager 对象获取 uow)

在 Scoped 结束时:_asyncUow.Value = null;

如下:

using (var uow = fsql.CreateUnitOfWork())
{
    _asyncUow.Value = uow;

    try
    {
        //todo ... BaseEntity 内部 curd 方法保持使用 uow 事务
    }
    finally
    {
        _asyncUow.Value = null;
    }

    uow.Commit();
}
]]>
级联删除 https://freesql.net/guide/cascade-delete.html https://freesql.net/guide/cascade-delete.html 级联删除 级联删除 接下来的内容,严重依赖的正确配置,请先学会再继续向下! 级联只针对 OneToOne/OneToMany/ManyToMany 三种导航属性,文档已经解释过。 基于【对象】级联删除 比如 查询的对象,可以使用此方法级联删除它们。 基于【数据库】级联删除 根据设置的导航属性,递归删除 OneToOne/OneToMany/ManyToMany ... Sat, 30 Jul 2022 09:07:52 GMT 接下来的内容,严重依赖【导航属性】的正确配置,请先学会再继续向下!

级联只针对 OneToOne/OneToMany/ManyToMany 三种导航属性,级联保存文档已经解释过。

基于【对象】级联删除

比如 Include/IncludeMany 查询的对象,可以使用此方法级联删除它们。

var repo = fsql.GetRepository<UserGroup>();
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<UserGroup>();
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 UserGroup
{
    [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; }
}
]]>
嵌套查询 ✨ https://freesql.net/guide/withtempquery.html https://freesql.net/guide/withtempquery.html 嵌套查询 ✨ 嵌套查询 ✨ WithTempQuery 需求版本:v3.2.666+ GroupBy + WithTempQuery(嵌套查询) + FromQuery + UnionAll 组合使用,会让查询功能更加强大、灵活。 注意:FromQuery 多个 WithTempQuery 匿名类型时,确保不是同一个类型(可使用任意属性区分) #1620 场景1:查... Sat, 23 Jul 2022 05:30:46 GMT WithTempQuery

需求版本:v3.2.666+

GroupBy + WithTempQuery(嵌套查询) + FromQuery + UnionAll 组合使用,会让查询功能更加强大、灵活。

注意:FromQuery 多个 WithTempQuery 匿名类型时,确保不是同一个类型(可使用任意属性区分) #1620

场景1:查询分组第一条记录

fsql.Select<User1>()
    .Where(a => a.Id < 1000)
    .WithTempQuery(a => new
    {
        item = a,
        rownum = SqlExt.RowNumber().Over().PartitionBy(a.Nickname).OrderBy(a.Id).ToValue()
    })
    .Where(a => a.rownum == 1)
    .ToList();

提示:支持多表嵌套查询,fsql.Select<User1, UserGroup1>()

SELECT *
FROM (
    SELECT a.[Id], a.[Nickname], row_number() over( partition by a.[Nickname] order by a.[Id]) [rownum]
    FROM [User1] a
    WHERE a.[Id] < 1000
) a
WHERE (a.[rownum] = 1)

如果数据库不支持开窗函数,可以使用分组嵌套查询解决:

fsql.Select<User1>()
    .Where(a => a.Id < 1000)
    .GroupBy(a => a.Nickname)
    .WithTempQuery(g => new { min = g.Min(g.Value.Id) })
    .From<User1>()
    .InnerJoin((a, b) => a.min == b.Id)
    .ToList((a, b) => b);
SELECT b.[Id], b.[Nickname]
FROM (
    SELECT min(a.[Id]) [min]
    FROM [User1] a
    WHERE a.[Id] < 1000
    GROUP BY a.[Nickname] ) a
INNER JOIN [User1] b ON a.[min] = b.[Id]

场景2:嵌套查询 + Join

WithTempQuery + From<T2> 或 FromQuery(ISelect<T2>) 可实现无限联表

fsql.Select<User1>()
    .Where(a => a.Id < 1000)
    .WithTempQuery(a => new
    {
        item = a,
        rownum = SqlExt.RowNumber().Over().PartitionBy(a.Nickname).OrderBy(a.Id).ToValue()
    })
    //.From<UserExt>() //普通联表
    .FromQuery(fsql.Select<UserExt>().Where(b => b.Id > 0)) //子查询联表
    //.FromQuery(fsql.Select<UserExt, UserGroup, xxx>() //子多表查询联表
    //    .WithTempQuery((a,b,c) => new { ... }))
    .InnerJoin((a, b) => a.item.Id == b.UserId)
    .Where((a, b) => a.rownum == 1)
    .ToList((a, b) => new
    {
        user = a.item,
        rownum = a.rownum,
        userext = b
    });
SELECT ...
FROM (
    SELECT a.[Id], a.[Nickname], row_number() over( partition by a.[Nickname] order by a.[Id]) [rownum]
    FROM [User1] a
    WHERE a.[Id] < 1000 ) a
INNER JOIN (
    SELECT a.[UserId], a.[Remark]
    FROM [UserExt] a
    WHERE (a.[Id] > 0) ) b ON a.[Id] = b.[UserId]
WHERE (a.[rownum] = 1)

场景3:分组查询嵌套

fsql.Select<User1>()
    .WithTempQuery(a => new
    {
        user = a,
        rownum = SqlExt.RowNumber().Over().PartitionBy(a.Nickname).OrderBy(a.Id).ToValue()
    })
    .Where(a => a.rownum == 1)
    .FromQuery(fsql.Select<UserExt>().Where(b => b.UserId > 0)
        .GroupBy(b => new { b.UserId, b.Remark })
        .WithTempQuery(b => new { b.Key, sum1 = b.Sum(b.Value.UserId) }))
    .InnerJoin((a, b) => a.user.Id == b.Key.UserId)
    .Where((a, b) => a.user.Nickname == "name03" || a.user.Nickname == "name02")
    .ToList((a, b) => new
    {
        user = a.user,
        rownum = a.rownum,
        groupby = b
    });
SELECT ...
FROM (
    SELECT a.[Id], a.[Nickname], row_number() over( partition by a.[Nickname] order by a.[Id]) [rownum]
    FROM [User1] a ) a
INNER JOIN (
    SELECT a.[UserId], a.[Remark], sum(a.[UserId]) [sum1]
    FROM [UserExt] a
    WHERE (a.[UserId] > 0)
    GROUP BY a.[UserId], a.[Remark] ) b ON a.[Id] = b.[UserId]
WHERE (a.[rownum] = 1) AND ((a.[Nickname] = N'name03' OR a.[Nickname] = N'name02'))

场景4:内存数据嵌套

假设跨数据库服务器,或者数据表被缓存过,WithMemory 便可以实现数据表与内存关联查询。

var list = new List<User1>();
list.Add(new User1 { Id = Guid.NewGuid() });
list.Add(new User1 { Id = Guid.NewGuid() });
list.Add(new User1 { Id = Guid.NewGuid() });

var listSql2 = fsql.Select<UserGroup>()
    .FromQuery(fsql.Select<User1>().WithMemory(list))
    .InnerJoin((a, b) => a.Id == b.GroupId)
    .ToSql();
SELECT ...
FROM [UserGroup] a
INNER JOIN (
    SELECT ...
    UNION ALL
    SELECT ...
    UNION ALL
    SELECT ...
) b ON a.[Id] = b.[GroupId]

场景5:自动分表后分页 分组聚合

自动分表后,如果有分页的需求 或者分组聚合的需求可以参考以下代码

var result = fsql.Select<Statistics>()
    .Where(a => a.createtime.BetweenEnd(startTime, endTime))  //时间字段定位表
    .WithTempQuery(a => new { item = a })
    .GroupBy(a => a.item.shareId)
    .Count(out var total)
    .Page(dto.page, dto.limit)
    .ToSql(g => new {
        Sid = a.Key,
        Sum1 = g.Sum(g.Value.item.field1),
        Sum2 = g.Sum(g.Value.item.field2),
    });
SELECT a.`shareId` as1, sum( a.`field1` ) as3, sum( a.`field2` ) as5
FROM (
    SELECT ...
    FROM (
        SELECT ...
        FROM `Statistics_2023` a
        WHERE (a.`createtime` >= '2022-01-01 00:00:00' AND a.`createtime` < '2023-01-14 00:00:00')
    ) ftb
    UNION ALL
    SELECT ...
    FROM (
        SELECT ...
        FROM `Statistics_2022` a
        WHERE (a.`createtime` >= '2022-01-01 00:00:00' AND a.`createtime` < '2023-01-14 00:00:00')
    ) ftb
) a
GROUP BY a.`shareId`
LIMIT 0,30

场景6:FromQuery 多个查询,最后映射查询

var query2 = fsql.Select<UnitLog, LoadPlan, Instruction>()
    .InnerJoin((a, b, c) => a.LoadNo == b.LoadNo && a.UnitTransactionType == "TO")
    .InnerJoin((a, b, c) => b.InstructionNo == c.InstructionNo)
    .WithTempQuery((a, b, c) => new
    {
        a.LoadNo,
        a.SeqNoLog,
        c.DeliveryInstractionStatus,
        c.UpTime,
        RN = SqlExt.RowNumber().Over().PartitionBy(a.UnitId).OrderByDescending(a.SeqNoLog).ToValue()
    });
var query3 = fsql.Select<Unit>();

fsql.Select<UnitLog>()
    .FromQuery(query2, query3)
    .InnerJoin((a,b,c) => a.SeqNoLog == b.SeqNoLog)
    .InnerJoin((a,b,c) => a.UnitId == c.UnitId)
    .Where((a,b,c) => b.RN < 2)
    .ToSql((a,b,c) => new MB51_View
    {
        //CkassIfCation = a.CkassIfCation,
        PGI = b.DeliveryInstractionStatus,
        PGITime = b.UpTime,
        IsDelayPGI = true,
        RunNo = c.RunNo
    });
SELECT a.[CkassIfCation] as1, b.[DeliveryInstractionStatus] as2, b.[UpTime] as3, 1 as4, c.[RunNo] as5
FROM [UnitLog] a
INNER JOIN (SELECT a.[LoadNo], a.[SeqNoLog], c.[DeliveryInstractionStatus], c.[UpTime], row_number() over( partition by a.[UnitId] order by a.[SeqNoLog] desc) [RN]
    FROM [UnitLog] a
    INNER JOIN [LoadPlan] b ON a.[LoadNo] = b.[LoadNo] AND a.[UnitTransactionType] = N'TO'
    INNER JOIN [Instruction] c ON b.[InstructionNo] = c.[InstructionNo] ) b ON a.[SeqNoLog] = b.[SeqNoLog]
INNER JOIN [Unit] c ON a.[UnitId] = c.[UnitId]
WHERE (b.[RN] < 2)

场景7:报表(每日)

  1. 从内存创建连续的日期 List
  2. 使用 FromQuery 与多个 ISelect 横向 LeftJoin
var startDate = DateTime.Parse("2024-11-1");
var endDate = DateTime.Parse("2024-12-1");
fsql.Select<object>()
    .WithMemory(
        Enumerable.Range(0, (int)endDate.Subtract(startDate).TotalDays)
            .Select(a => new { Date = startDate.AddDays(a).ToString("yyyy-MM-dd") })
            .ToList()
    )
    .FromQuery(
        fsql.Select<T1, T2>().InnerJoin((a,b) => ...)
            .Where((a,b) => a.CreateDate.BetweenEnd(startDate, endDate)
            .GroupBy((a,b) => a.CreateDate.Date.ToString("yyyy-MM-dd"))
            .WithTempQuery(g => new { Date = g.Key, Type1Total = g.Sum(g.Value.Item2.Qty1) }),
        fsql.Select<T3>()
            .Where(a => a.CreateDate.BetweenEnd(startDate, endDate)
            .GroupBy(a => a.CreateDate.Date.ToString("yyyy-MM-dd"))
            .WithTempQuery(g => new { Date = g.Key, Type2Total = g.Sum(g.Value.Qty2) }),
        //... 最多支持 16 个 ISelect 合并
    )
    .LeftJoin(t => t.t2.Date = t.t1.Date)
    .LeftJoin(t => t.t3.Date = t.t1.Date)
    .OrderByDescending(t => t.t1.Date)
    .ToList(t => new
    {
        t.t1.Date,
        Sum1 = t.t2.Type1Total,
        Sum2 t.t3.Type2Total
    });

WithParameters 参数化共享

开启参数化查询功能后,使用 WithParameters 共享参数化,可避免产生相同的参数名称:

var dbpars = new List<DbParameter>();

var id1 = 1;
var id2 = 2;
var sql = fsql.Select<User1>()
    .WithParameters(dbpars)
    .Where(a => a.Id == id1)

    .FromQuery(
        fsql.Select<User1>()
            .WithParameters(dbpars)
            .Where(a => a.Id == id2)
    )
    .InnerJoin((a,b) => a.Id == b.Id)
    .ToSql();
SELECT a."Id", a."GroupId", a."Username"
FROM (
    SELECT a."Id", a."GroupId", a."Username"
    FROM "User1" a
    WHERE (a."Id" = @exp_0)
) a
INNER JOIN (
    SELECT a."Id", a."GroupId", a."Username"
    FROM "User1" a
    WHERE (a."Id" = @exp_1) ) b ON b."Id" = a."Id"
]]>
Castle AOP + FreeSql 跨方法异步事务 https://freesql.net/extra/aop-freesql-autofac.html https://freesql.net/extra/aop-freesql-autofac.html Castle AOP + FreeSql 跨方法异步事务 Castle AOP + FreeSql 跨方法异步事务 使用Autofac基于特性标签,实现跨方法的异步事务处理 Autofac.Extensions.DependencyInjection Autofac.Extras.DynamicProxy Castle.Core.AsyncInterceptor(异步方法AOP拦截) FreeSql基础服务 ... Mon, 18 Jul 2022 15:41:00 GMT 使用Autofac基于特性标签,实现跨方法的异步事务处理

  • Autofac.Extensions.DependencyInjection
  • Autofac.Extras.DynamicProxy
  • Castle.Core.AsyncInterceptor(异步方法AOP拦截)

FreeSql基础服务

安装FreeSql包

dotnet add package FreeSql
dotnet add package FreeSql.DbContext
dotnet add package FreeSql.Provider.MySqlConnector

手动创建一个MySql/MariaDB数据库,名为ovov_freesql_repository

appsettings.json

{
  "Default": "Data Source=127.0.0.1;Port=3306;User ID=root;Password=root;Initial Catalog=ovov_freesql_repository;Charset=utf8;SslMode=none;Max pool size=10"
}

配置FreeSql服务

public void ConfigureServices(IServiceCollection services)
{
  IConfigurationSection Default  = Configuration.GetSection("Default");
  var fsql = new FreeSqlBuilder()
            .UseConnectionString(DataType.MySql, Default.Value)
            .UseAutoSyncStructure(true)
            .UseNameConvert(NameConvertType.PascalCaseToUnderscoreWithLower)
            .UseMonitorCommand(cmd => Trace.WriteLine(cmd.CommandText))
            .Build();
    services.AddSingleton<IFreeSql>(fsql);
    services.AddScoped<UnitOfWorkManager>();
    services.AddFreeRepository(null, typeof(Startup).Assembly);
}

Autofac+AOP实现异步事务

csproj

<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="5.0.0" />
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="1.7.0" />

dotnet add package Autofac.Extensions.DependencyInjection
dotnet add package Autofac.Extras.DynamicProxy
dotnet add package Castle.Core.AsyncInterceptor

创建一个标识事务的特性标签

[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class TransactionalAttribute : Attribute
{
    /// <summary>
    /// 事务传播方式
    /// </summary>
    public Propagation Propagation { get; set; } = Propagation.Required;

    /// <summary>
    /// 事务隔离级别
    /// </summary>
    public IsolationLevel? IsolationLevel { get; set; }

    public TransactionalAttribute(){}

    public TransactionalAttribute(Propagation propagation, IsolationLevel isolationLevel)
    {
        Propagation = propagation;
        IsolationLevel = isolationLevel;
    }
}

Autofac集成

Program.CS 替换默认的DI CreateHostBuilder方法

 Host.CreateDefaultBuilder(args).UseServiceProviderFactory(new AutofacServiceProviderFactory())

Startup.cs配置服务

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterModule(new AutofacModule());
}

.NET6 这样注册

builder.Host
    .UseServiceProviderFactory(new AutofacServiceProviderFactory())
    .ConfigureContainer<ContainerBuilder>((webBuilder, containerBuilder) =>
    {
          containerBuilder.RegisterModule(new AutofacModule());
    });

这里给BlogService方法注入UnitOfWorkInterceptor拦截处理。直接注入类。

public class AutofacModule : Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<UnitOfWorkInterceptor>();
        builder.RegisterType<UnitOfWorkAsyncInterceptor>();

        builder.RegisterType<BlogService>()
            .InterceptedBy(typeof(UnitOfWorkInterceptor))
            .EnableClassInterceptors();

}

AOP

    public class UnitOfWorkInterceptor : IInterceptor
    {
        private readonly UnitOfWorkAsyncInterceptor asyncInterceptor;

        public UnitOfWorkInterceptor(UnitOfWorkAsyncInterceptor interceptor)
        {
            asyncInterceptor = interceptor;
        }

        public void Intercept(IInvocation invocation)
        {
            asyncInterceptor.ToInterceptor().Intercept(invocation);
        }
    }

    public class UnitOfWorkAsyncInterceptor : IAsyncInterceptor
    {
        private readonly UnitOfWorkManager _unitOfWorkManager;
        private readonly ILogger<UnitOfWorkAsyncInterceptor> _logger;
        IUnitOfWork _unitOfWork;

        public UnitOfWorkAsyncInterceptor(UnitOfWorkManager unitOfWorkManager, ILogger<UnitOfWorkAsyncInterceptor> logger)
        {
            _unitOfWorkManager = unitOfWorkManager;
            _logger = logger;
        }

        private bool TryBegin(IInvocation invocation)
        {
            var method = invocation.MethodInvocationTarget ?? invocation.Method;
            var attribute = method.GetCustomAttributes(typeof(TransactionalAttribute), false).FirstOrDefault();
            if (attribute is TransactionalAttribute transaction)
            {
                _unitOfWork = _unitOfWorkManager.Begin(transaction.Propagation, transaction.IsolationLevel);
                return true;
            }

            return false;
        }

        /// <summary>
        /// 拦截同步执行的方法
        /// </summary>
        /// <param name="invocation"></param>
        public void InterceptSynchronous(IInvocation invocation)
        {
            if (TryBegin(invocation))
            {
                try
                {
                    invocation.Proceed();
                    _unitOfWork.Commit();
                }
                catch
                {
                    _unitOfWork.Rollback();
                    throw;
                }
                finally
                {
                    _unitOfWork.Dispose();
                }
            }
            else
            {
                invocation.Proceed();
            }
        }

        /// <summary>
        /// 拦截返回结果为Task的方法
        /// </summary>
        /// <param name="invocation"></param>
        public void InterceptAsynchronous(IInvocation invocation)
        {
             invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
        }

        private async Task InternalInterceptAsynchronous(IInvocation invocation)
        {
            if (TryBegin(invocation))
            {
                try
                {
                    invocation.Proceed();
                    if (invocation.ReturnValue != null)
                    {
                        await (Task)invocation.ReturnValue;
                    }
                    _unitOfWork.Commit();
                }
                catch (Exception)
                {
                    _unitOfWork.Rollback();
                    throw;
                }
                finally
                {
                    _unitOfWork.Dispose();
                }
            }
            else
            {
                invocation.Proceed();
                if (invocation.ReturnValue != null)
                {
                    await (Task)invocation.ReturnValue;
                }
            }
        }


        /// <summary>
        /// 拦截返回结果为Task<TResult>的方法
        /// </summary>
        /// <param name="invocation"></param>
        /// <typeparam name="TResult"></typeparam>
        public void InterceptAsynchronous<TResult>(IInvocation invocation)
        {
            invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
        }

        private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
        {
            TResult result;
            if (TryBegin(invocation))
            {
                try
                {
                    invocation.Proceed();
                    result = await (Task<TResult>)invocation.ReturnValue;
                    _unitOfWork.Commit();
                }
                catch (System.Exception)
                {
                    _unitOfWork.Rollback();
                    throw;
                }
                finally
                {
                    _unitOfWork.Dispose();
                }
            }
            else
            {
                invocation.Proceed();
                result = await (Task<TResult>)invocation.ReturnValue;
            }
            return result;
        }
    }

当Service层没有接口,则必须使用virtual虚方法。

    public class BlogService
    {
        /// <summary>
        /// 当出现异常时,不会插入数据
        /// </summary>
        /// <param name="createBlogDto"></param>
        [Transactional]
        public virtual void CreateBlogTransactional(CreateBlogDto createBlogDto)
        {
            Blog blog = _mapper.Map<Blog>(createBlogDto);
            blog.CreateTime = DateTime.Now;
            _blogRepository.Insert(blog);

            List<Tag> tags = new List<Tag>();
            createBlogDto.Tags.ForEach(r =>
            {
                tags.Add(new Tag { TagName = r });
            });
            if (createBlogDto.Title == "abc")
            {
                throw new Exception("test exception");
            }
            _tagRepository.Insert(tags);
        }
    }

当传入的参数,title为abc时,会出现异常,throw new Exception("test exception");,前面插入的数据并没有成功,会自动回滚。

Autofac批量注册

public class ServiceModule : Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<UnitOfWorkInterceptor>();
        builder.RegisterType<UnitOfWorkAsyncInterceptor>();

        List<Type> interceptorServiceTypes = new List<Type>()
        {
            typeof(UnitOfWorkInterceptor)
        };
        //service所在dll,LinCms.Application为程序集名称,也可以通过typeof(程序集中的某个类即可).Assembly获取
        Assembly servicesDllFile = Assembly.Load("LinCms.Application");

        builder.RegisterAssemblyTypes(servicesDllFile)
                .Where(a => a.Name.EndsWith("Service") && !a.IsAbstract && !a.IsInterface && a.IsPublic)
                .AsImplementedInterfaces()
                .InstancePerLifetimeScope()
                .PropertiesAutowired()// 属性注入
                .InterceptedBy(interceptorServiceTypes.ToArray())
                .EnableInterfaceInterceptors();
     }
}

当我们使用Autofac批量注册服务后,可以直接使用Service层的接口,不需要再使用注入。

public interface IBlogService
{
  Task CreateBlogTransactionalAsync(CreateBlogDto createBlogDto);
}

public class BlogService:IBlogService
{
    private readonly IBlogRepository _blogRepository;
    private readonly ITagRepository _tagRepository;
    private readonly IMapper _mapper;

    public BlogService(IBlogRepository blogRepository, ITagRepository tagRepository, IMapper mapper)
    {
        _blogRepository = blogRepository ?? throw new ArgumentNullException(nameof(blogRepository));
        _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository));
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }
    [Transactional]
    public virtual async Task CreateBlogTransactionalAsync(CreateBlogDto createBlogDto)
    {
        Blog blog = _mapper.Map<Blog>(createBlogDto);
        blog.CreateTime = DateTime.Now;
        await _blogRepository.InsertAsync(blog);

        List<Tag> tags = new List<Tag>();
        createBlogDto.Tags.ForEach(r =>
        {
            tags.Add(new Tag { TagName = r });
        });
        if (createBlogDto.Title == "abc")
        {
            throw new Exception("test exception CreateBlogTransactionalAsync");
        }
        await _tagRepository.InsertAsync(tags);
    }
}
  • 使用
[Route("api/[controller]")]
[ApiController]
public class BlogController : ControllerBase
{
    private readonly IBlogService _blogService;
    public BlogController(IBlogService blogService)
    {
        _blogService=blogService;
    }

    [HttpPost("CreateBlogTransactionalAsync")]
    public async Task CreateBlogTransactionalAsync([FromBody] CreateBlogDto createBlogDto)
    {
        await _blogService.CreateBlogTransactionalAsync(createBlogDto);
    }
}
]]>
国产数据库 https://freesql.net/guide/freesql-provider-custom.html https://freesql.net/guide/freesql-provider-custom.html 国产数据库 国产数据库 由于太多,在此不一一列举,它们大多数语法兼容 MySql、Oracle、SqlServer、PostgreSQL 四种常用数据库之一。 FreeSql.Provider.Custom 提供了这四种数据库适配,并且支持 CodeFirst/DbFirst 以及完整的 FreeSql 功能。 FreeSql.Provider.Custom 不依... Sat, 09 Jul 2022 09:12:12 GMT | 数据库名称 | 提供者 | 系列风格 | |

]]>
ODBC https://freesql.net/guide/freesql-provider-odbc.html https://freesql.net/guide/freesql-provider-odbc.html ODBC FreeSql.Provider.Odbc 实现 ODBC 访问数据库,ODBC 属于比较原始的技术,更新慢,各大数据库厂支持得标准不一,不到万不得已最好别用 odbc,坑比较多。 FreeSql.Provider.Odbc 做了多种数据库的专用实现:SqlServer、PostgreSQL、Oracle、MySql、达梦、人大金仓,和一种通用实现。 ... Sat, 09 Jul 2022 09:12:12 GMT FreeSql.Provider.Odbc 实现 ODBC 访问数据库,ODBC 属于比较原始的技术,更新慢,各大数据库厂支持得标准不一,不到万不得已最好别用 odbc,坑比较多。

FreeSql.Provider.Odbc 做了多种数据库的专用实现:SqlServer、PostgreSQL、Oracle、MySql、达梦、人大金仓,和一种通用实现。

和原来的 FreeSql.Provider.SqlServer 等 ado.net 相比,只支持较少的基础类型,其他功能几乎都有,包括 CodeFirst 自动迁移。

国产数据库大多数都兼容 SqlServer、PostgreSQL、Oracle、MySql 这四种数据库,所以它们也可以用来访问国产数据库。

自定义适配

通用实现为了让用户自己适配更多的数据库,比如连接 mssql 2000、db2 等数据库,牺牲了一些功能:

  • 不支持 CodeFirst 自动迁移
  • 不支持 DbFirst 接口方法的实现
  • 不支持 原来的分页方法,需要自行判断 id 进行分页
  • 只支持较少的基础类型:bool,sbyte,short,int,long,byte,ushort,uint,ulong,double,float,decimal,DateTime,byte[],string,Guid

使用者只需求重写类 FreeSql.Odbc.Default.OdbcAdapter 就可以自定义访问不同的数据库。

我们默认做了一套 sqlserver 的语法和映射适配,代码在 Default/OdbcAdapter.cs,请查看代码了解。

class Mssql2000Adapter : FreeSql.Odbc.Default.OdbcAdapter
{
    public override string InsertAfterGetIdentitySql => "SELECT SCOPE_IDENTITY()";
    //可以重写更多的设置
}

fsql.SetOdbcAdapter(new Mssql2000Adapter());

适配好新的 OdbcAdapter 后,请在 FreeSqlBuilder.Build 之后调用 IFreeSql.SetOdbcAdapter 方法生效。

]]>
FreeSql 实现审计日志 https://freesql.net/extra/freesql-auditlog.html https://freesql.net/extra/freesql-auditlog.html FreeSql 实现审计日志 FreeSql 实现审计日志 有两种情况,如果都是针对实体操作,确实很好做这个功能。 IFreeSql 更新/删除,都可以不传实体进行操作,所以这个 old_values, new_values 实现起来比较麻烦(可能需要查询一次?性能?)。另外还有批量操作。 1、fsql.Aop.CurdAfter 事件是 CRUD 之后触发,提供以下参数 2、Fr... Fri, 24 Jun 2022 16:45:08 GMT 有两种情况,如果都是针对实体操作,确实很好做这个功能。

IFreeSql 更新/删除,都可以不传实体进行操作,所以这个 old_values, new_values 实现起来比较麻烦(可能需要查询一次?性能?)。另外还有批量操作。

1、fsql.Aop.CurdAfter 事件是 CRUD 之后触发,提供以下参数

/// <summary>
/// 标识符,可将 CurdBefore 与 CurdAfter 进行匹配
/// </summary>
public Guid Identifier { get; protected set; }
protected Stopwatch Stopwatch { get; }
/// <summary>
/// 操作类型
/// </summary>
public CurdType CurdType { get; } //Select, Delete, Update, Insert, InsertOrUpdate
/// <summary>
/// 实体类型
/// </summary>
public Type EntityType { get; }
/// <summary>
/// 实体类型的元数据
/// </summary>
public TableInfo Table { get; }
/// <summary>
/// 执行的 SQL
/// </summary>
public string Sql { get; }
/// <summary>
/// 参数化命令
/// </summary>
public DbParameter[] DbParms { get; }

/// <summary>
/// 发生的错误
/// </summary>
public Exception Exception { get; }
/// <summary>
/// 执行SQL命令,返回的结果
/// </summary>
public object ExecuteResult { get; }
/// <summary>
/// 耗时(单位:Ticks)
/// </summary>
public long ElapsedTicks => this.Stopwatch.ElapsedTicks;
/// <summary>
/// 耗时(单位:毫秒)
/// </summary>
public long ElapsedMilliseconds => this.Stopwatch.ElapsedMilliseconds;

2、FreeSql.DbContext 或者 FreeSql.UnitOfWork 提供对象变化跟踪

全局设置:

fsql.SetDbContextOptions(opt => {
  opt.OnEntityChange = report => {
    Console.WriteLine(report);
  };
});

单独设置 DbContext 或者 UnitOfWork:

var ctx = fsql.CreateDbContext();
ctx.Options.OnEntityChange = report => {
  Console.WriteLine(report);
};

var uow = fsql.CreateUnitOfWork();
uow.OnEntityChange = report => {
  Console.WriteLine(report);
};

参数 report 是一个 List 集合,集合元素的类型定义如下:

public class ChangeInfo
{
    public object Object { get; set; }
    public EntityChangeType Type { get; set; }
    /// <summary>
    /// Type = Update 的时候,获取更新之前的对象
    /// </summary>
    public object BeforeObject { get; set; }
    /// <summary>
    /// 实体类型
    /// </summary>
    public Type EntityType { get; set; }
}
public enum EntityChangeType { Insert, Update, Delete, SqlRaw }

| 变化类型 | 说明 | |

]]>
Docker+ FreeSql https://freesql.net/extra/freesql-docker.html https://freesql.net/extra/freesql-docker.html Docker+ FreeSql Docker+ FreeSql .net5+Docker+ Encryption(ssl/tls) handshake failed .net5 网站使用Sqlserver数据库部署在docker容器内运行报主库链接失败 环境 数据库:Sqlserver2014 网站程序:.Net5 Docker版本:Docker version 19.03.13, ... Fri, 24 Jun 2022 16:45:08 GMT .net5+Docker+ Encryption(ssl/tls) handshake failed

.net5 网站使用Sqlserver数据库部署在docker容器内运行报主库链接失败

环境

数据库:Sqlserver2014 网站程序:.Net5 Docker版本:Docker version 19.03.13, .net5环境镜像源:mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim Centos版本:CentOS Linux release 7.9.2009 (Core)

使用原生方式进行测试

using (SqlConnection connection = new SqlConnection("Data Source=xxxxx;User Id=sa;Password=xxxxxx;Initial Catalog=xxxxxxxx;Pooling=true;Min Pool Size=1;"))
{
    connection.Open();
    connection.Close();
    connection.Dispose();
}

报错

A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: SSL Provider, error: 31 - Encryption(ssl/tls) handshake failed)

最终解决方案

在dockerfile里面加上这么两句

RUN sed -i 's/MinProtocol = TLSv1.2/MinProtocol = TLSv1/g' /etc/ssl/openssl.cnf
RUN sed -i 's/MinProtocol = TLSv1.2/MinProtocol = TLSv1/g' /usr/lib/ssl/openssl.cnf

将TLSv1.2设为TLSv1,只能是设为TLSv1而不是设为TLSv1.0

原文链接

]]>
ISelect 拷贝克隆(copy/clone) https://freesql.net/extra/iselect-depcopy.html https://freesql.net/extra/iselect-depcopy.html ISelect 拷贝克隆(copy/clone) ISelect 拷贝克隆(copy/clone) 当一个 ISelect 构造到一定复杂程序之后,比如: 利用语法糖解决: 科普: csharp 7.0 支持本地函数,方法内再定义临时方法,这个特性向大家推荐,在很多时候都非常有效。 方法内还可以定义方法,那就称它:本地函数/嵌套方法。 原文链接 技巧:ISelect 如何拷贝(copy)复用,克隆(c... Fri, 24 Jun 2022 16:45:08 GMT 当一个 ISelect 构造到一定复杂程序之后,比如:

public void Test()
{
    var select1 = fsql.Select<AdmRoute>().Include(a => a.Parent)
        .WhereIf(!string.IsNullOrEmpty(key), a => a.Name.Contains(key) || a.Extdata.Contains(key) || a.Remark.Contains(key) || a.TenantId.Contains(key) || a.Parent.Name.Contains(key) || a.Parent.Extdata.Contains(key) || a.Parent.Remark.Contains(key) || a.Parent.TenantId.Contains(key))
        .WhereIf(Parent_Id?.Any() == true, a => Parent_Id.Contains(a.ParentId))
        .WhereIf(mn_Roles_Id?.Any() == true, a => a.Roles.AsSelect().Any(b => mn_Roles_Id.Contains(b.Id)));

    var select2 = select1;
    select1.Where(a => a.Status == 0);
    //此时 select2 也附加了 a.Status == 0 条件
}

利用语法糖解决:

public void Test()
{
    ISelect<AdmRoute> GetSelect() => fsql.Select<AdmRoute>().Include(a => a.Parent)
        .WhereIf(!string.IsNullOrEmpty(key), a => a.Name.Contains(key) || a.Extdata.Contains(key) || a.Remark.Contains(key) || a.TenantId.Contains(key) || a.Parent.Name.Contains(key) || a.Parent.Extdata.Contains(key) || a.Parent.Remark.Contains(key) || a.Parent.TenantId.Contains(key))
        .WhereIf(Parent_Id?.Any() == true, a => Parent_Id.Contains(a.ParentId))
        .WhereIf(mn_Roles_Id?.Any() == true, a => a.Roles.AsSelect().Any(b => mn_Roles_Id.Contains(b.Id)));

    var select1 = GetSelect();
    var select2 = GetSelect();
    select1.Where(a => a.Status == 0);
    //此时 select2 不会附加 a.Status == 0 条件
}

科普:

csharp 7.0 支持本地函数,方法内再定义临时方法,这个特性向大家推荐,在很多时候都非常有效。

方法内还可以定义方法,那就称它:本地函数/嵌套方法。

原文链接

]]>
动态聚合列 sum(case when ...) https://freesql.net/extra/issues-expression-groupbysum.html https://freesql.net/extra/issues-expression-groupbysum.html 动态聚合列 sum(case when ...) 动态聚合列 sum(case when ...) 如上 v1,v2,v3 是动态聚合值,如果 where IN (1,2,3,4) 那就会产生 v1-v4 正常情况下,静态的 lambda 查询没办法处理这种动态列查询。 变通一下,这样查询: 如此便可以使用 FreeSql 实现: 自定义解析表达式树,实现如下: Fri, 24 Jun 2022 16:45:08 GMT
SELECT
a."Time",
v1 = sum(case when a."Id" == 1 then 1 else 0 end),
v2 = sum(case when a."Id" == 2 then 1 else 0 end),
v3 = sum(case when a."Id" == 3 then 1 else 0 end)
FROM "table" a
WHERE a."Id" IN (1,2,3)
GROUP BY a."Time"

如上 v1,v2,v3 是动态聚合值,如果 where IN (1,2,3,4) 那就会产生 v1-v4

正常情况下,静态的 lambda 查询没办法处理这种动态列查询。

]]>
In多列查询,表达式自定义实现 https://freesql.net/extra/issues-in-valuetype.html https://freesql.net/extra/issues-in-valuetype.html In多列查询,表达式自定义实现 In多列查询,表达式自定义实现 FreeSql 基础库为了不依赖 System.ValueType.dll,所以将以下代码抽离了出来。 Oracle 产生如下SQL: 非 ORACLE 产生如下 SQL: v3.2.650 使用 .Where(a =&gt; list.Any(b =&gt; b.Item1 == a. Id && b.Item2 == a. ct... Fri, 24 Jun 2022 16:45:08 GMT FreeSql 基础库为了不依赖 System.ValueType.dll,所以将以下代码抽离了出来。

//元组集合
var lst = new List<(Guid, DateTime)>();
lst.Add((Guid.NewGuid(), DateTime.Now));
lst.Add((Guid.NewGuid(), DateTime.Now));

var t2 = fsql.Select<T>()
  .Where(a => lst.Contains(a.Id, a.ct1))
  .ToList();

Oracle 产生如下SQL:

SELECT .. FROM ..
WHERE (a."Id", a."ct1") in (
('685ee1f6-bdf6-4719-a291-c709b8a1378f','2019-12-07 23:55:27'),
('5ecd838a-06a0-4c81-be43-1e77633b7404', '2019-12-07 23:55:27'))

非 ORACLE 产生如下 SQL:

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

v3.2.650 使用 .Where(a => list.Any(b => b.Item1 == a. Id && b.Item2 == a. ct1))

代码实现:

using FreeSql.DataAnnotations;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

[ExpressionCall]
public static class MyFreeSqlExpressionCall
{
    public static ThreadLocal<ExpressionCallContext> expContext = new ThreadLocal<ExpressionCallContext>();
    /// <summary>
    /// C#:从元组集合中查找 exp1, exp2 是否存在<para></para>
    /// SQL: <para></para>
    /// exp1 = that[0].Item1 and exp2 = that[0].Item2 OR <para></para>
    /// exp1 = that[1].Item1 and exp2 = that[1].Item2 OR <para></para>
    /// ... <para></para>
    /// 注意:当 that 为 null 或 empty 时,返回 1=0 <para></para>
    /// Oracle: (Id, Name) IN ((1, "name1"), (2, "name2"))
    /// </summary>
    /// <typeparam name="T1"></typeparam>
    /// <typeparam name="T2"></typeparam>
    /// <param name="that"></param>
    /// <param name="exp1"></param>
    /// <param name="exp2"></param>
    /// <returns></returns>
    public static bool Contains<T1, T2>([RawValue] this IEnumerable<(T1, T2)> that, T1 exp1, T2 exp2)
    {
        if (expContext.IsValueCreated == false || expContext.Value == null || expContext.Value.ParsedContent == null)
            return that?.Any(a => a.Item1.Equals(exp1) && a.Item2.Equals(exp2)) == true;
        if (that?.Any() != true)
        {
            expContext.Value.Result = "1=0";
            return false;
        }
        var sb = new StringBuilder();
        var idx = 0;

        switch (expContext.Value.DataType )
        {
            case FreeSql.DataType.Oracle:
            case FreeSql.DataType.OdbcOracle:
                sb.Append("(")
                    .Append(expContext.Value.ParsedContent["exp1"]).Append(", ")
                    .Append(expContext.Value.ParsedContent["exp2"])
                    .Append(") IN (");
                foreach (var item in that)
                {
                    if (idx++ > 0) sb.Append(", ");
                    sb.Append("(")
                        .Append(expContext.Value.FormatSql(FreeSql.Internal.Utils.GetDataReaderValue(typeof(T1), item.Item1))).Append(", ")
                        .Append(expContext.Value.FormatSql(FreeSql.Internal.Utils.GetDataReaderValue(typeof(T2), item.Item2)))
                        .Append(")");
                }
                sb.Append(")");

                expContext.Value.Result = sb.ToString();
                return false;
        }
        foreach (var item in that)
        {
            if (idx++ > 0) sb.Append(" OR \r\n");
            sb
                .Append(expContext.Value.ParsedContent["exp1"]).Append(" = ").Append(expContext.Value.FormatSql(FreeSql.Internal.Utils.GetDataReaderValue(typeof(T1), item.Item1)))
                .Append(" AND ")
                .Append(expContext.Value.ParsedContent["exp2"]).Append(" = ").Append(expContext.Value.FormatSql(FreeSql.Internal.Utils.GetDataReaderValue(typeof(T2), item.Item2)));
        }
        expContext.Value.Result = sb.ToString();
        return true;
    }
}
]]>
Mysql 5.5 兼容性 https://freesql.net/extra/issues-mysql5_5.html https://freesql.net/extra/issues-mysql5_5.html Mysql 5.5 兼容性 Mysql 5.5 兼容性 Mysql 5.5 不支持 datetime(3),导致 codefirst 报错 二种解决办法: 1、[Column(DbType = &quot;DATETIME&quot;)] 2、统一处理的话写个AOP Fri, 24 Jun 2022 16:45:08 GMT Mysql 5.5 不支持 datetime(3),导致 codefirst 报错

二种解决办法:

1、[Column(DbType = "DATETIME")]

2、统一处理的话写个AOP

fsql.Aop.ConfigEntityProperty += (s, e) =>
{
    if (e.Property.PropertyType == typeof(DateTime) || e.Property.PropertyType == typeof(DateTime?))
         e.ModifyResult.DbType = "DATETIME";
};
]]>
MySql 系列数据库 https://freesql.net/guide/freesql-provider-mysqlconnector.html https://freesql.net/guide/freesql-provider-mysqlconnector.html MySql 系列数据库 FreeSql.Provider.MySqlConnector是FreeSql基于社区提供的最新的MySqlConnector驱动的实现,兼容性、性能都比FreeSql.Provider.MySql好,且支持多种数据库,如:MySQL, MariaDB, Percona, Amazon Aurora, Azure Database for MySQL,... Fri, 24 Jun 2022 16:45:08 GMT

FreeSql.Provider.MySqlConnectorFreeSql基于社区提供的最新的MySqlConnector驱动的实现,兼容性、性能都比FreeSql.Provider.MySql好,且支持多种数据库,如:MySQL, MariaDB, Percona, Amazon Aurora, Azure Database for MySQL, Google Cloud SQL for MySQL, OceanBase, Doris, Tidb

并且支持BulkCopy,推荐使用

如果你使用 FreeSql.Provider.MySql 发生了以下错误,请替换到 FreeSql.Provider.MySqlConnector:

  • The given key '0' was not present in the dictionary.
  • The given key '25653' was not present in the dictionary.
  • The given key '26995' was not present in the dictionary.
  • The given key '28261 was not present in the dictionary.
  • The given key '65535' was not present in the dictionary.
  • The type initializer for 'MySql.Data.MySqlClient.Replication.ReplicationManager' threw an exception.
  • Parameter '@xxx' must be defined.
  • Object cannot be cast from DBNull to other types.

MySql Enum 映射

默认情况 c# 枚举会映射为 MySql Enum 类型,如果想映射为 int 在 FreeSqlBuilder Build 之后执行以下 Aop 统一处理:

fsql.Aop.ConfigEntityProperty += (s, e) => {
    if (e.Property.PropertyType.IsEnum)
        e.ModifyResult.MapType = typeof(int);
};

增删改 BulkCopy

  • 主键无值
public class Department
{
    [Column(IsPrimary = true, IsIdentity = true)]
    public long Id { get; set; }
    public string Name { get; set; }
}
List<Department> departments = new List<Department>()
{
    new Department() { Name ="部门1"},
    new Department() { Name ="部门2"},
    new Department() { Name ="部门3"}
};
fsql.Insert(departments)
    .InsertIdentity() //这行
    .ExecuteMySqlBulkCopy();
  • Id主键有值时
List<Department> departments = new List<Department>()
{
    new Department() { Id=1, Name ="部门1"},
    new Department() { Id=2, Name ="部门2"},
    new Department() { Id=3, Name ="部门3"}
};
fsql.Insert(departments)
    .ExecuteMySqlBulkCopy();
fsql.Update<T>.SetSource(items)
    .ExecuteSqlBulkCopy(); //临时表 + MERGE INTO

fsql.InsertOrUpdate<T>.SetSource(items)
    .ExecuteSqlBulkCopy(); //临时表 + MERGE INTO
]]>
Oracle https://freesql.net/guide/freesql-provider-oracle.html https://freesql.net/guide/freesql-provider-oracle.html Oracle FreeSql 对 Oracle 支持非常友好,是 c#.net ORM 不二之选,提供了 Ado.net/Odbc/Oledb 三种实现包,他们都支持 .NETCore2.1+、.NET4.0+ 等最新或较低的 .NETFramework 版本。 若想以 Ado.net 驱动访问数据库,请安装: dotnet add package FreeSql.... Fri, 24 Jun 2022 16:45:08 GMT FreeSql 对 Oracle 支持非常友好,是 c#.net ORM 不二之选,提供了 Ado.net/Odbc/Oledb 三种实现包,他们都支持 .NETCore2.1+、.NET4.0+ 等最新或较低的 .NETFramework 版本。

若想以 Ado.net 驱动访问数据库,请安装:

dotnet add package FreeSql.Provider.Oracle

若想以 ODBC 驱动访问数据库,请安装:

dotnet add package FreeSql.Provider.Odbc

若想以 Oledb 驱动访问数据库,请安装:

dotnet add package FreeSql.Provider.OracleOledb

]]>
PostgreSQL https://freesql.net/guide/freesql-provider-postgresql.html https://freesql.net/guide/freesql-provider-postgresql.html PostgreSQL nuget 安装:FreeSql.Provider.PostgreSQL 数组、字典 PostgreSQL 数据类型丰富,支持 int[]、string[] 数组类型,Dictionary&lt;string, string&gt; 字典类型。 参考资料:《PostgreSQL Array 数组类型与 FreeSql 打出一套【组合拳】》 JSONB Postgr... Fri, 24 Jun 2022 16:45:08 GMT

nuget 安装:FreeSql.Provider.PostgreSQL

数组、字典

PostgreSQL 数据类型丰富,支持 int[]、string[] 数组类型,Dictionary<string, string> 字典类型。

class model
{
    public int id { get; set; }
    public int[] tagIds { get; set; } //映射数组
    public string[] tagNames { get; set; }

    public Dictionary<string, string> dict { get; set; } //映射 hstore
}

// 支持索引查询
fsql.Select<model>().Where(a => a.tagIds.Contains(100)).ToList();
fsql.Select<model>().Where(a => a.dict.ContainsKey("key1")).ToList();

参考资料:《PostgreSQL Array 数组类型与 FreeSql 打出一套【组合拳】》

JSONB

PostgreSQL JSON 类型的查询性能不输 mongodb,适合做动态结构的数据存储场景。

FreeSql.Provider.PostgreSQL 默认使用 Newtonsoft.Json.Linq 映射 jsonb 类型,如下三种均可:

class model
{
    public JToken jsonb1 { get; set; }
    public JObject jsonb2 { get; set; }
    public JArray jsonb3 { get; set; }
}

// 支持索引查询
fsql.Select<model>().Where(a => a.jsonb1.ContainsKey("key1")).ToList();
//SQL: WHERE coalesce(a.jsonb1, '{}') ? 'key1'

fsql.Select<model>().Where(a => a.jsonb2["key1"].ContainsKey("key2")).ToList();
//SQL: WHERE coalesce(a.jsonb2->key1, '{}') ? 'key2'

fsql.Select<model>().Where(a => a.jsonb2.Contains(JToken.Parse("{key1:'xxx'}")).ToList();
//SQL: WHERE coalesce(a.jsonb2, '{}') @> '{"key1":'xxx'}'::jsonb

fsql.Select<model>().Where(a => a.jsonb2["key1"]["key2"].ToString() == "xxx").ToList();
//SQL: WHERE (a.jsonb2->key1->key2)::text = 'xxx'

fsql.Select<model>().Where(a => int.Parse(a.jsonb2["key1"]["key2"].ToString()) > 100).ToList();
//SQL: WHERE (a.jsonb2->key1->key2)::text::int4 > 100

| lambda 表达式树函数 | PostgreSQL | 功能说明 | |

]]>
基于FreeSql扩展文档 https://freesql.net/extra/ https://freesql.net/extra/ 基于FreeSql扩展文档 基于FreeSql扩展文档 更多文档 CentOS8 ARM 下连接 SQL Server 2008 R2(Hypervisor) 一对多关系,分表只取关联的第一条记录,如何获取 Thu, 23 Jun 2022 16:16:24 GMT 更多文档 ]]> 多个 IFreeSql 注入使用 https://freesql.net/extra/idlebus-freesql.html https://freesql.net/extra/idlebus-freesql.html 多个 IFreeSql 注入使用 多个 IFreeSql 注入使用 多库切换,动态切库,动态注册数据库 一、定义多个 IFreeSql 该方法适用于固定数据库,固定配置项 1、定义两个标识类: 2、在 Startup.cs 中单例注入 3、在 Controller 中使用 二、使用 FreeSql.Cloud 为 FreeSql 提供跨数据库访问,分布式事务TCC、SAGA解决方案,支... Thu, 23 Jun 2022 16:16:24 GMT 多库切换,动态切库,动态注册数据库

一、定义多个 IFreeSql

该方法适用于固定数据库,固定配置项

1、定义两个标识类:

class MySqlFlag {}
class SqlServerFlag {}

2、在 Startup.cs 中单例注入

public void ConfigureServices(IServiceCollection services)
{
    Func<IServiceProvider, IFreeSql<MySqlFlag>> fsql1 = r =>
    {
        var fsql1 = new FreeSqlBuilder().UseConnectionString(DataType.MySql, "str1")
            .Build<MySqlFlag>();
        return fsql1;
    };
    Func<IServiceProvider, IFreeSql<SqlServerFlag>> fsql2 = r =>
    {
        var fsql2 = new FreeSqlBuilder().UseConnectionString(DataType.SqlServer, "str1")
            .Build<SqlServerFlag>();
        return fsql2;
    };

    services.AddSingleton<IFreeSql<MySqlFlag>>(fsql1);
    services.AddSingleton<IFreeSql<SqlServerFlag>>(fsql2);
}

3、在 Controller 中使用

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    public ValuesController(IFreeSql<MySqlFlag> mysql, IFreeSql<SqlServerFlag> sqlserver)
    {
    }
}

二、使用 FreeSql.Cloud

为 FreeSql 提供跨数据库访问,分布式事务TCC、SAGA解决方案,支持 .NET Core 2.1+, .NET Framework 4.0+.

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

dotnet add package FreeSql.Cloud

or

Install-Package FreeSql.Cloud

public enum DbEnum { db1, db2, db3 }

var fsql = new FreeSqlCloud<DbEnum>("myapp"); //提示:泛型可以传入 string
fsql.DistributeTrace = log => Console.WriteLine(log.Split('\n')[0].Trim());

fsql.Register(DbEnum.db1, () => new FreeSqlBuilder()
    .UseConnectionString(DataType.Sqlite, @"Data Source=db1.db")
    .Build());

fsql.Register(DbEnum.db2, () => new FreeSqlBuilder()
    .UseConnectionString(DataType.Sqlite, @"Data Source=db2.db")
    .Build());

fsql.Register(DbEnum.db3, () => new FreeSqlBuilder()
    .UseConnectionString(DataType.Sqlite, @"Data Source=db3.db")
    .Build());

FreeSqlCloud 必须定义成单例模式

new FreeSqlCloud<DbEnum>() 多连接管理

new FreeSqlCloud<DbEnum>("myapp") 开启 TCC/SAGA 事务生效

FreeSqlCloud 的访问方式和 IFreeSql 一样:

fsql.Select<T>();
fsql.Insert<T>();
fsql.Update<T>();
fsql.Delete<T>();

//...

切换数据库:

fsql.Change(DbEnum.db3).Select<T>();
//以后所有 fsql.Select/Insert/Update/Delete 操作是 db3

自动定向数据库配置:

//对 fsql.CRUD 方法名 + 实体类型 进行拦截,自动定向到对应的数据库,达到自动 Change 切换数据库目的
fsql.EntitySteering = (_, e) =>
{
    switch (e.MethodName)
    {
        case "Select":
            if (e.EntityType == typeof(T))
            {
                //查询 T 自动定向 db3
                e.DBKey = DbEnum.db3;
            }
            else if (e.DBKey == DbEnum.db1)
            {
                //此处像不像读写分离?
                var dbkeyIndex = new Random().Next(0, e.AvailableDBKeys.Length);
                e.DBKey = e.AvailableDBKeys[dbkeyIndex]; //重新定向到其他 db
            }
            break;
        case "Insert":
        case "Update":
        case "Delete":
        case "InsertOrUpdate":
            break;
    }
};

参考

多个 IFreeSql 实例,如何注入使用? · Issue #44 · dotnetcore/FreeSql (github.com)

]]>
支持我们 https://freesql.net/reference/service-support.html https://freesql.net/reference/service-support.html 支持我们 支持我们 FreeSql 采用 MIT 最宽松的开源协议,永久免费开源。任何公司或个人,都可以使用在任何商业项目中。 QQ 技术群:561616019(在线)、4336577(满员)、8578575(满员)、52508226(满员) 反馈问题请前往 https://github.com/dotnetcore/FreeSql/issues 💕 自愿捐赠... Wed, 25 May 2022 15:18:32 GMT FreeSql 采用 MIT 最宽松的开源协议,永久免费开源。任何公司或个人,都可以使用在任何商业项目中。

QQ 技术群:561616019(在线)、4336577(满员)、8578575(满员)、52508226(满员)

反馈问题请前往 https://github.com/dotnetcore/FreeSql/issues

💕 自愿捐赠

Donate the author to have a cup of coffee

L*y 58 元、花花 88 元、麦兜很乖 50 元、网络来者 2000 元、John 99.99 元、alex 666 元、bacongao 36 元、无名 100 元、Eternity 188 元、无名 10 元、⌒.Helper~..oO 66 元、习惯与被习惯 100 元、无名 100 元、蔡易喋 88.88 元、中讯科技 1000 元、Good Good Work 24 元、炽焰 6.6 元、Nothing 100 元、兰州天擎赵 500 元、哈利路亚 300 元、 无名 100 元、蛰伏 99.99 元、TCYM 66.66 元、MOTA 5 元、LDZXG 30 元、Near 30 元、建爽 66 元、无名 200 元、LambertWu 100 元、无名 18.88 元、乌龙 50 元、无名 100 元、陳怼怼 66.66 元、陳怼怼 66.66 元、丁淮 100 元、李伟坚-Excel 催化剂 100 元、白狐 6.66 元、她微笑的脸 y 30 元、Eternity²º²¹ 588 元、夜归柴门 88 元、蔡易喋 666.66 元、 礼 10 元、litrpa 88 元、Alax CHOW 200 元、Daily 66 元、kt 66 元、蓝 100 元、*菜 10 元、生命如歌 1000 元、山鸡 88元、平凡 100元、大树 1000元、软软的毛毛虫 66.66元、问卷星 2000元、与你无关 5000元、看门大爷 1000元,*来 88.88元,*博 500元,*南 88元,*园 500元,WPF机器视觉活动全体成员 2000元

🌳 有偿服务

| 服务项目 | 描述 | 价格(RMB) | 付款方式 | |

]]>
Sqlite https://freesql.net/guide/freesql-provider-sqlitecore.html https://freesql.net/guide/freesql-provider-sqlitecore.html Sqlite Sqlite FreeSql.Provider.SqliteCore是FreeSql基于微软提供的最新的Microsoft.Data.Sqlite.Core驱动的实现。 需要另外安装对应的bundle_xxx实现加密。 支持的版本 .NET Standard 2.0(支持.NET Framework4.6.1+/支持.NET Core 2.0+) ne... Fri, 25 Mar 2022 06:41:57 GMT FreeSql.Provider.SqliteCore是FreeSql基于微软提供的最新的Microsoft.Data.Sqlite.Core驱动的实现。 需要另外安装对应的bundle_xxx实现加密。

支持的版本

  • .NET Standard 2.0(支持.NET Framework4.6.1+/支持.NET Core 2.0+)
  • net6.0
  • SQLite(3.7 及以上版本)
  • Linux ARM/X86/MAUI Android/MAUI iOS等

安装FreeSql.Provider.SqliteCore包。

dotnet add package FreeSql.Provider.SqliteCore
dotnet add package SQLitePCLRaw.bundle_e_sqlite3

SQLitePCLRaw.bundle_e_sqlite3 不支持加密,但此方式是官方实现的SQlite版本,以下二选一:

| 捆绑 | 描述 | |

]]>
UowManager 事务 ✨ https://freesql.net/guide/unitofwork-manager.html https://freesql.net/guide/unitofwork-manager.html UowManager 事务 ✨ UowManager 事务 ✨ 本篇文章内容引导,如何在 asp.net core 项目中使用特性(注解) 的方式管理事务。 UnitOfWorkManager 只可以管理 Repository 仓储对象的事务 支持六种传播方式(propagation),跨方法的事务非常方便,支持同步异步: Required:如果当前没有事务,就新建一个事务,如果已存... Sat, 21 Aug 2021 05:59:59 GMT 本篇文章内容引导,如何在 asp.net core 项目中使用特性(注解) 的方式管理事务。

UnitOfWorkManager 只可以管理 Repository 仓储对象的事务

支持六种传播方式(propagation),跨方法的事务非常方便,支持同步异步:

  • Required:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,默认的选择。
  • Supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
  • Mandatory:使用当前事务,如果没有当前事务,就抛出异常。
  • NotSupported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • Never:以非事务方式执行操作,如果当前事务存在则抛出异常。
  • Nested:以嵌套事务方式执行。

最终呈现的事务代码如下:

public class SongService
{
    readonly IBaseRepository<Song> _songRepository;
    readonly IBaseRepository<Detail> _detailRepository;

    public SongService(IBaseRepository<Song> songRepository, IBaseRepository<Detail> detailRepository)
    {
        _songRepository = songRepository;
        _detailRepository = detailRepository;
    }

    [Transactional(Propagation.Required)]
    async public Task Test1()
    {
        //所有注入的仓储对象,都是一个事务
        await _songRepository.InsertAsync(xxx1);
        await _detailRepository.DeleteAsync(xxx2);
        this.Test2();
    }
}

第一步:依赖注入、中间件

//依赖注入
services.AddFreeRepository(typeof(Startup).Assembly);
services.AddScoped<IFreeSql>(r => r.GetService<UnitOfWorkManager>().Orm);
services.AddScoped<UnitOfWorkManager>(r => new UnitOfWorkManager(fsql));
//以这种方式注入的 IFreeSql 会跟随 UowManager 切换事务,原始 fsql 对象为 static 单例

//中间件
public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        TransactionalAttribute.SetServiceProvider(context.RequestServices);
        await next();
    });
}

第二步:引入动态代理库

肉夹馍:https://github.com/inversionhourglass/Rougamo

dotnet add package Rougamo.Fody

[AttributeUsage(AttributeTargets.Method)]
public class TransactionalAttribute : Rougamo.MoAttribute
{
    public Propagation Propagation { get; set; } = Propagation.Required;
    public IsolationLevel IsolationLevel { get => m_IsolationLevel.Value; set => m_IsolationLevel = value; }
    IsolationLevel? m_IsolationLevel;

    static AsyncLocal<IServiceProvider> m_ServiceProvider = new AsyncLocal<IServiceProvider>();
    public static void SetServiceProvider(IServiceProvider serviceProvider) => m_ServiceProvider.Value = serviceProvider;

    IUnitOfWork _uow;
    public override void OnEntry(MethodContext context)
    {
        var uowManager = m_ServiceProvider.Value.GetService<UnitOfWorkManager>();
        _uow = uowManager.Begin(this.Propagation, this.m_IsolationLevel);
    }
    public override void OnExit(MethodContext context)
    {
	if (typeof(Task).IsAssignableFrom(context.ReturnType) && context.ReturnValue != null && context.ReturnValue is Task)
            ((Task)context.ReturnValue).ContinueWith(t => _OnExit());
        else _OnExit();

        void _OnExit()
        {
            try
            {
                if (context.Exception == null) _uow.Commit();
                else _uow.Rollback();
            }
            finally
            {
                _uow.Dispose();
            }
        }
    }
}

| UnitOfWorkManager 成员 | 说明 | |

]]>
IFreeSql 和 DbContext https://freesql.net/guide/ifreesql-context.html https://freesql.net/guide/ifreesql-context.html IFreeSql 和 DbContext IFreeSql 和 DbContext 两者并存 FreeSql 支持 IFreeSql 和 DbContext 两种形式,使用方法差异也有些大,其中 DbContext 跟 EFCore 使用方式基本一致,使用简单、依赖注入方便。 而 IFreeSql 支持更多细粒度的操作。在 DbContext 中,要删除多个行记录,必须先查询实体后,再使用 R... Wed, 14 Jul 2021 05:16:31 GMT 两者并存

FreeSql 支持 IFreeSql 和 DbContext 两种形式,使用方法差异也有些大,其中 DbContext 跟 EFCore 使用方式基本一致,使用简单、依赖注入方便。

而 IFreeSql 支持更多细粒度的操作。在 DbContext 中,要删除多个行记录,必须先查询实体后,再使用 RemoveRange() 删除这些实体,当数据量非常大的时候,便会消耗大量时间和性能。而 IFreeSql 中,支持嵌入部分 SQL ,支持细粒度的行记录操作。在 IFreeSql 中批量删除、修改记录,可以使用类似 SQL 的模式:

await _freesql.Update<Roles>()
    .Set(x => x.IsDeleted, 1)
    .Where(x =>  x.TenantId == tenantId )
    .ExecuteAffrowsAsync();

update roles set is_deleted=1 where where tenant_id=1

所以往往将 IFreesql 和 DbContext 并存,在使用时,根据场景使用这两种方式操作数据库。

使用方法

IFreeSql 的创建需要定义为单例模式,示例如下:

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.MySql, connectionString)
    .UseAutoSyncStructure(true) //自动同步实体结构到数据库
    .Build(); //请务必定义成 Singleton 单例模式

而 Freesql 也支持了类似 EFCore 的 DbContext 模式。通过引入 FreeSql.DbContext 库,可以使用特性、Fluent api 等配置实体映射、导航属性等,生成类似于 EFCore 的 DbContext 类型,然后通过依赖注入实例化使用。

通过定义 DbContext ,我们可以根据 EFCore 的使用习惯,快速掌握 FreeSql 的使用,甚至可以直接将 EFCore DbContext 转为 Freesql DbContext,因为 Freesql DbContext 支持了 95% 的 EFCore Fluent api 。

下面说说如何利用 FreeSql 更加方便地迁移数据库以及定义 DbContext。

安装 FreeSql 工具:

dotnet tool install -g FreeSql.Generator

接着还原数据库表为实体:

FreeSql.Generator  -NameOptions 1,1,0,1 -NameSpace AuthCenter.Database.Entities -DB "MySql,data source=123.123.123.123;port=3306;user id=root;password=root;initial catalog=datab;charset=utf8;sslmode=none;max pool size=2" -FileName "{name}.cs"

还原的实体示例:

[JsonObject(MemberSerialization.OptIn), Table(Name = "roles", DisableSyncStructure = true)]
public partial class Roles
{

    [JsonProperty, Column(Name = "id", DbType = "bigint unsigned", IsPrimary = true)]
    public ulong Id { get; set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    [JsonProperty, Column(Name = "creation_time", DbType = "bigint unsigned")]
    public ulong CreationTime { get; set; }
}

在 FreeSql 中,实体特性优先于 Fluent api,同时 FreeSql 也兼容 EFCore 的实体特性。

由于 FreeSql.Generator 工具很方便,我们不需要另外使用 Fluent api 去定义复杂的逻辑结构。

接着定义 DbContext:

using FreeSql;

namespace My.Context
{
    public partial class AuthcenterContext : DbContext
    {
        private readonly IFreeSql iFreesql;
        public AuthcenterContext()
        {
        }

        public AuthcenterContext(IFreeSql freeSql)
        {
            iFreesql = freeSql;
        }

        public virtual DbSet<Roles> Roles { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder builder)
        {
            // iFreesql.xxxx
            // 使用 iFreesql 调用 Fluent api
            builder.UseFreeSql(iFreesql);
        }

        protected override void OnModelCreating(ICodeFirst codefirst)
        {

        }
    }
}

通过这种方法,IFreeSql 和 DbContext 中的实体及其实体特性、Fluent api 等都保持一致。

接着,在依赖注入中,将 IFreeSql 和 DbContext 都注入。

public void ConfigureServices(IServiceCollection services)
{
    IFreeSql fsql = new FreeSql.FreeSqlBuilder()
        .UseConnectionString(FreeSql.DataType.MySql, @"data source=123.123.123.123;port=3306;user id=root;password=root;initial catalog=authcenter;charset=utf8")
        .Build();
    services.AddSingleton<IFreeSql>(fsql);
    services.AddFreeDbContext<AuthcenterContext>(options => options.UseFreeSql(fsql));
}

我们在类型中通过依赖注入获取到这两种服务:

private readonly IFreeSql _freesql;
private readonly MyContext _context;

public Test(IFreeSql freeSql, MyContext context)
{
    _freesql = freeSql;
    _context = context;
}

然后就可以快乐地使用 Freesql 了。

]]>
WithSql https://freesql.net/guide/withsql.html https://freesql.net/guide/withsql.html WithSql WithSql WithSql 自定义 SQL 定义实体类 不同的查询方式。 返回DataTable 返回List&lt;Tuplue&gt; 即List&lt;(string,string)&gt;元组 返回List&lt;object&gt; 且能支持分页 返回List&lt;TestClassDto&gt;且能支持分页 1.动态表名 2.返回 DataTable 3.返回 DataTable ... Wed, 16 Jun 2021 16:56:09 GMT WithSql 自定义 SQL

定义实体类

public class TestClass
{
    [Column(Name = "ID", IsPrimary = true)]
    public string No { get; set; }
    public int? Age { get; set; }
    public string Name { get; set; }
    [Column(Name = "BIRTH_DAY")]
    public DateTime? Birthday { get; set; }
    public decimal Point { get; set; }
    public Sex? Sex { get; set; }
}
public enum Sex { Boy, Girl }
public class TestClssDto
{
    public string ID { get; set; }
    public int? Age { get; set; }
    public DateTime? Birthday { get; set; }
}

不同的查询方式。

  • 返回DataTable
  • 返回List<Tuplue>List<(string,string)>元组
  • 返回List<object> 且能支持分页
  • 返回List<TestClassDto>且能支持分页

1.动态表名

//对 TestClass_201903 表查询
fsql.Select<TestClass>().AsTable((t, old) => $"{old}_201903").ToList();

2.返回 DataTable

DataTable dt1 = _fsql.Select<object>()
    .WithSql("select * from TestClass")
    .Where(...)
    .ToDataTable("ID, Age");
SELECT ID, Age
FROM ( select * from TestClass ) a
WHERE ...

3.返回 DataTable

DataTable dt2 = _fsql.Select<object>()
    .WithSql("select * from TestClass")
    .Where(...)
    .ToDataTable("*");
SELECT *
FROM ( select * from TestClass ) a
WHERE ...

4.返回List<Tuple>List<(string,string)> 元组

List<(string,string)> list1 = _fsql
    .Select<object>()
    .WithSql("select * from TestClass")
    .Where(...)
    .ToList<(string, string)>("ID, Age");
SELECT ID, Age
FROM ( select * from TestClass ) a
WHERE ...

5.返回List<object>

var list2 = _fsql.Select<object>()
    .WithSql("select * from TestClass ")
    .Where(...)
    .ToList<object>("*");
SELECT *
FROM ( select * from TestClass ) a
WHERE ...

6.返回List<object> 且能支持分页

var list3 = _fsql.Select<object>()
    .WithSql("select * from TestClass ")
    .WhereIf(true, "1=1")
    .Page(1, 10).OrderBy("ID DESC")
    .ToList<object>("ID,Age");
SELECT ID, Age
FROM ( select * from TestClass ) a
WHERE (1 = 1)
ORDER BY ID DESC
limit 0,10

7.返回List<TestClassDto>且能支持分页

var list4 = _fsql.Select<object>()
    .WithSql("select * from TestClass ")
    .WhereIf(true, "1=1")
    .Page(1, 10)
    .OrderBy("ID DESC")
    .ToList<TestClssDto>("ID,Age,BIRTH_DAY as Birthday");
SELECT ID,Age,BIRTH_DAY as Birthday
FROM ( select * from TestClass ) a
WHERE (1 = 1)
ORDER BY ID DESC
limit 0,10

通过 WithSql + ToSQL 实现 Union ALL 查询方法

1、二次 ISelect 查询:WithSql 使用多次,等于 UNION ALL 查询

WithSql 使用多次为 UNION ALL 查询,所以我们可以利用 ISelect.ToSql(FieldAliasOptions.AsProperty) 得到生成的 SQL,如下:

var sql1 = fsql.Select<Topic>()
    .Where(a => a.Title.Contains("xxx"))
    .ToSql();
var sql2 = fsql.Select<Topic>()
    .Where(a => a.Title.Contains("yyy"))
    .ToSql();

fsql.Select<Topic>()
    .WithSql(sql1)
    .WithSql(sql2)
    .ToList();
SELECT  * from (SELECT a.`Id`, a.`Clicks`, a.`TypeGuid`, a.`Title`, a.`CreateTime`
FROM ( SELECT a.`Id`, a.`Clicks`, a.`TypeGuid`, a.`Title`, a.`CreateTime`
    FROM `tb_topic` a
    WHERE ((a.`Title`) LIKE '%xxx%') ) a) ftb

UNION ALL

SELECT  * from (SELECT a.`Id`, a.`Clicks`, a.`TypeGuid`, a.`Title`, a.`CreateTime`
FROM ( SELECT a.`Id`, a.`Clicks`, a.`TypeGuid`, a.`Title`, a.`CreateTime`
    FROM `tb_topic` a
    WHERE ((a.`Title`) LIKE '%yyy%') ) a) ftb

2、跨分表查询:AsTable 相同实体多次操作,等于 Union ALL 查询

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

3、利用 ToSql 拼接新的 SQL,使用 IAdo 执行

var sql1 = fsql.Select<Topic>()
    .Where(a => a.Id > 100 && a.Id < 200)
    .ToSql(a => new { a.Id, a.Title }, FieldAliasOptions.AsProperty);
var sql2 = fsql.Select<Topic>()
    .Where(a => a.Id > 1001 && a.Id < 1200)
    .ToSql(a => new { a.Id, a.Title }, FieldAliasOptions.AsProperty);

fsql.Ado.CommandFluent($"{sql1} UNION ALL {sql2}")
    .ExecuteDataTable();

分页问题

Union All 之后 如果直接 分页会有一个问题。请看具体示例

多次 WithSql + Page 存在问题:每个 WithSql 内都有一个 Page 分页

var sql1 = fsql.Select<Topic>()
    .Where(a => a.Title.Contains("xxx"))
    .ToSql();
var sql2 = fsql.Select<Topic>()
    .Where(a => a.Title.Contains("yyy"))
    .ToSql();

fsql.Select<Topic>().WithSql(sql1).WithSql(sql2).Page(1, 20).ToList();
 SELECT  * from (SELECT a.`Id`, a.`Clicks`, a.`TypeGuid`, a.`Title`, a.`CreateTime`
    FROM ( SELECT a.`Id`, a.`Clicks`, a.`TypeGuid`, a.`Title`, a.`CreateTime`
        FROM `tb_topic` a
        WHERE ((a.`Title`) LIKE '%xxx%') ) a
    limit 0,20) ftb

    UNION ALL

    SELECT  * from (SELECT a.`Id`, a.`Clicks`, a.`TypeGuid`, a.`Title`, a.`CreateTime`
    FROM ( SELECT a.`Id`, a.`Clicks`, a.`TypeGuid`, a.`Title`, a.`CreateTime`
        FROM `tb_topic` a
        WHERE ((a.`Title`) LIKE '%yyy%') ) a
    limit 0,20) ftb

多个 sql union all 使用 withsql,直接 Page 分页,会导致每个子表都生效,子表都生成分页。

WithSql 可以和 AsTable 实现分表的功能。

分表跨表查询的时候,分页是要向每个子表(即每个 WithSql 中的 SQL 分页)都生效。

解决方案

多次 withsql ,如需分页,需要按下面的二步操作

  • 第一步:通过 witsql,将二个 sql 组成一个 sql。
 var sql = fsql.Select<Topic>()
    .WithSql("SELECT * FROM tb_topic where id > 11")
    .WithSql("SELECT * FROM tb_topic where id < 10")
    .ToSql("*")

如上生成的 UOION ALL 的 sql

SELECT  * from (SELECT *
    FROM ( SELECT * FROM tb_topic where id > 11 ) a) ftb

    UNION ALL

    SELECT  * from (SELECT *
    FROM ( SELECT * FROM tb_topic where id < 10 ) a) ftb
  • 第二步:之后 调用 Page 则是通过 Union ALL 后的结果上分页
 var sql2 = g.mysql.Select<Topic>()
     .WithSql(sql)
     .Page(2, 10)
     .ToSql();
SELECT a.`Id`, a.`Clicks`, a.`TypeGuid`, a.`Title`, a.`CreateTime`
FROM ( SELECT  * from (SELECT *
    FROM ( SELECT * FROM tb_topic where id > 11 ) a) ftb

    UNION ALL

    SELECT  * from (SELECT *
    FROM ( SELECT * FROM tb_topic where id < 10 ) a) ftb ) a
limit 10,10
]]>
Awesome FreeSql https://freesql.net/reference/awesome-freesql.html https://freesql.net/reference/awesome-freesql.html Awesome FreeSql Awesome FreeSql 基于 FreeSql 的仓库项目 FreeSql 官方博客 https://www.cnblogs.com/kellynic/ https://www.cnblogs.com/freesql/ 官方文档 https://freesql.net https://github.com/dotnetcore/FreeSql/w... Tue, 09 Feb 2021 12:13:50 GMT 基于 FreeSql 的仓库项目

FreeSql 官方博客

官方文档

源码

]]>
特别鸣谢 https://freesql.net/reference/donation.html https://freesql.net/reference/donation.html 特别鸣谢 特别鸣谢 🌟 我们由衷感谢 OhOhMan 为本项目提供的服务器支持! OhOhMan 开源社区的慷慨助力为项目的稳定运行注入强大动力,特此深表感谢! Tue, 09 Feb 2021 12:13:50 GMT | 捐赠者 | 金额 | 时间 | |

]]>
ADO https://freesql.net/guide/ado.html https://freesql.net/guide/ado.html ADO ADO Ado 是 IFreeSql 下重要的对象之一,它包括所有对 SQL 操作的封装,提供 ExecuteReader、ExecuteDataSet、ExecuteDataTable、ExecuteNonQuery、ExecuteScalar 等方法,使用起来和传统 SqlHelper 一样。 查询 SQL 返回实体 参数化 Ado 下面所有参数 ... Sat, 06 Feb 2021 16:18:46 GMT Ado 是 IFreeSql 下重要的对象之一,它包括所有对 SQL 操作的封装,提供 ExecuteReader、ExecuteDataSet、ExecuteDataTable、ExecuteNonQuery、ExecuteScalar 等方法,使用起来和传统 SqlHelper 一样。

查询 SQL 返回实体

//返回多条记录
List<T> list = fsql.Ado.Query<T>("select * from t1");

//返回单条记录
T item = fsql.Ado.QuerySingle<T>("select * from t1 where id = @id", new { id = 1 });

//返回多个结果集
var result = fsql.Ado.Query<T1, T2>("select * from t1; select * from t2");
List<T1> list1 = result.Item1;
List<T2> list2 = result.Item2;

//like 查询
string searchText = "abc";
List<T> users = _fsql.Ado.Query<T>("select * from t1 where name like @name", new { name = "%" + searchText + "%" });

//获取数据库时间
//SELECT now(), utc_timestamp()
var result = fsql.Ado.QuerySingle(() => new
{
    DateTime.Now,
    DateTime.UtcNow
});

参数化

Ado 下面所有参数 object parms 都可以接受匿名对象,或者字典:

 new { id = 1, name = "xx" }
 new Dictionary<string, object> { ["id"] = 1, ["name"] = "xx" }

参数前缀

  • odbc 是 ? 并且没有标识,所以 freesql 禁用了 odbc 参数化

| 类型 | 前缀符号 | |

]]>
表达式函数 https://freesql.net/guide/expression-function.html https://freesql.net/guide/expression-function.html 表达式函数 表达式函数 这是 FreeSql 非常特色的功能之一,请别错过文档的细节,可映射的类型基本都可以使用对应的表达式函数,例如 日期、字符串、IN 查询、数组(PostgreSQL 数组)、字典(PostgreSQL HStore)等等。 SqlExt.xxx 是 FreeSql 默认提供常见的自定义函数,详细见开窗函数。 Lambda接拼 单表 多表 I... Thu, 04 Feb 2021 16:03:18 GMT 这是 FreeSql 非常特色的功能之一,请别错过文档的细节,可映射的类型基本都可以使用对应的表达式函数,例如 日期、字符串、IN 查询、数组(PostgreSQL 数组)、字典(PostgreSQL HStore)等等。

SqlExt.xxx 是 FreeSql 默认提供常见的自定义函数,详细见开窗函数

Lambda接拼

  • 单表
Expression<Func<T, bool>> where = null;
where = where.And(b => b.num > 0);
where = where.Or(b => b.num > 0);
fsql.Select<T>().Where(where)
  • 多表
Expression<Func<HzyTuple<T1, T2, T3, T4, T5, T6>, bool>> where = null;
where = where.Or(a => a.t6.Id > 0);
fsql.Select<T1, T2, T3, T4, T5, T6>().Where(where)

In查询

var t1 = fsql.Select<T>()
  .Where(a => new[] { 1, 2, 3 }.Contains(a.Id))
  .ToList();
//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)

In多列查询

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

var t2 = fsql.Select<T>()
  .Where(a => lst.Contains(a.Id, a.ct1))
  .ToList();
//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')

v3.2.650 使用 .Where(a => list.Any(b => b.Item1 == a. Id && b.Item2 == a. ct1))

WHERE (id, code) in ((1,'code1'), (2,'code2')) 实现代码:In多列查询,表达式自定义实现

In子表

fsql.Select<Topic>()
  .Where(a => fsql.Select<Topic>().As("b").ToList(b => b.Id).Contains(a.Id))
  .ToList();
//SELECT a.`Id`, a.`Title`, a.`Clicks`, a.`CreateTime`, a.`CategoryId`
//FROM `Topic` a
//WHERE (((a.`Id`) in (SELECT b.`Id`
//    FROM `Topic` b)))

Exists子表

fsql.Select<Topic>()
  .Where(a => fsql.Select<Topic>().As("b").Where(b => b.Id == a.Id).Any())
  .ToList();
//SELECT a.`Id`, a.`Title`, a.`Clicks`, a.`CreateTime`, a.`CategoryId`
//FROM `Topic` a
//WHERE (exists(SELECT 1
//    FROM `Topic` b
//    WHERE (b.`Id` = a.`Id`)
//    limit 0,1))

提示:由于子查询的实体类与上层相同,使用 As("b") 指明别名,以便区分

查找今天创建的数据

fsql.Select<T>()
  .Where(a => a.CreateTime.Date == DateTime.Today)
  .ToList();
//这行代码说明 FreeSql 表达式解析强大,不是所有 ORM 都支持

fsql.Select<T>()
  .Where(a => a.CreateTime.Between(DateTime.Today, DateTime.Today.AddDays(1)))
  .ToList();
//正常用法应该是这样

fsql.Select<T>()
  .Where(a => a.CreateTime.Subtract(DateTime.Today).TotalDays == 0)
  .ToList();
//WHERE datediff(day, date1, date2) = 0

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

日期格式化

fsql.Select<T>()
  .First(a => a.CreateTime.ToString("HH:mm:ss"));
// SELECT date_format(a.`CreateTime`, '%H:%i:%s') as1
// FROM `xxx` a
// limit 0,1

v1.5.0 支持了常用 c# 日期格式化,yyyy MM dd HH mm ss yy M d H hh h m s tt t

tt t 为 AM PM

开窗函数

fsql.Select<T1, T2>()
  .InnerJoin((a, b) => b.Id == a.Id)
  .ToList((a, b) => new
  {
    Id = a.Id,
    EdiId = b.Id,
    over1 = SqlExt.Rank().Over().OrderBy(a.Id).OrderByDescending(b.EdiId).ToValue()
  });

v1.6.0 利用自定义解析功能,增加 SqlExt.Rank().Over().PartitionBy(...)、MySql group_concat 常用函数,欢迎 PR 补充

FreeSql 默认集成了 SqlExt.cs 扩展解析方法:

| lambda | sql | 说明 | |

]]>
分页查询 https://freesql.net/guide/paging.html https://freesql.net/guide/paging.html 分页查询 分页查询 每页 20 条数据,查询第 1 页 Count(out var total) 是同步方法,原因是 out 不支持异步,如果介意可以单独执行如下: 提示:数据量大一般不建议查 Count/CountAsync,而应该采用流式分页(上一页、下一页、不返回总数量) BasePagingInfo 拥有 PageNumber,PageSize,Coun... Thu, 04 Feb 2021 16:03:18 GMT
IFreeSql fsql; //如何创建请移步入门文档

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

    public int CategoryId { get; set; }
}

每页 20 条数据,查询第 1 页

var list = fsql.Select<Topic>()
    .Where(a => a.Id > 10)
    .Count(out var total) //总记录数量
    .Page(1, 20)
    .ToList();

Count(out var total) 是同步方法,原因是 out 不支持异步,如果介意可以单独执行如下:

提示:数据量大一般不建议查 Count/CountAsync,而应该采用流式分页(上一页、下一页、不返回总数量)

var select = fsql.Select<Topic>().Where(a => a.Id > 10);
var total = await select.CountAsync();
var list = await select.Page(1, 20).ToListAsync();

BasePagingInfo 拥有 PageNumber,PageSize,Count,如下内容 .Page(page)page.Count 就有统计值了

public class TopicGetListInput: BasePagingInfo
{
    public string Name { get; set; }
}
var list = await fsql.Select<Topic>()
    .WhereIf(!string.IsNullOrEmpty(page.Name),r => r.Name.Contains(page.Name))
    .OrderByDescending(c => c.CreateTime)
    .Page(page)
    .ToListAsync();

优化

SqlServer 2012 以前的版本,使用 row_number 分页;

SqlServer 2012+ 版本,使用最新的 fetch next rows 分页;

API

| 方法 | 返回值 | 参数 | 描述 | |

]]>
分组聚合 https://freesql.net/guide/select-group-by.html https://freesql.net/guide/select-group-by.html 分组聚合 分组聚合 1、单表分组 注意:v3.5.108 GroupBySelf 返回 ISelect 而非 ISelectGrouping 不分组求聚合值,请使用 ToAggregate 替代 ToList 2、多表分组 g.Value.Item1 对应 Topic g.Value.Item2 对应 Category g.Value.Item3 对应 Area... Thu, 04 Feb 2021 16:03:18 GMT
IFreeSql fsql; //如何创建请移步入门文档

class Topic
{
    [Column(IsIdentity = true, IsPrimary = true)]
    public int Id { get; set; }
    public int Clicks { get; set; }
    public string Title { get; set; }
    public DateTime CreateTime { get; set; }
}

1、单表分组

注意:v3.5.108 GroupBySelf 返回 ISelect 而非 ISelectGrouping

var list = fsql.Select<Topic>()
    .GroupBy(a => new { tt2 = a.Title.Substring(0, 2), mod4 = a.Id % 4 })
    .Having(g => g.Count() > 0 && g.Avg(g.Key.mod4) > 0 && g.Max(g.Key.mod4) > 0)
    .Having(g => g.Count() < 300 || g.Avg(g.Key.mod4) < 100)
    .OrderBy(g => g.Key.tt2)
    .OrderByDescending(g => g.Count())
    .ToList(g => new
    {
        g.Key,
        cou1 = g.Count(),
        arg1 = g.Avg(g.Value.Clicks),
        arg2 = g.Sum(g.Value.Clicks > 100 ? 1 : 0)
    });
//SELECT
//substr(a.`Title`, 1, 2) as1,
//(a.`Id` % 4) as2,
//count(1) as3,
//avg(a.`Clicks`) as4,
//sum(case when a.`Clicks` > 100 then 1 else 0 end) as5
//FROM `Topic` a
//GROUP BY substr(a.`Title`, 1, 2), (a.`Id` % 4)
//HAVING (count(1) > 0 AND avg((a.`Id` % 4)) > 0 AND max((a.`Id` % 4)) > 0) AND (count(1) < 300 OR avg((a.`Id` % 4)) < 100)
//ORDER BY substr(a.`Title`, 1, 2), count(1) DESC

不分组求聚合值,请使用 ToAggregate 替代 ToList

var list = fsql.Select<Topic>()
    .ToAggregate(g => new
    {
        cou1 = g.Count(),
        arg1 = g.Avg(g.Key.Clicks),
        arg2 = g.Sum(g.Key.Clicks > 100 ? 1 : 0)
    });

2、多表分组

var list = fsql.Select<Topic, Category, Area>()
    .InnerJoin((a, b, c) => b.Id == a.CategoryId)
    .InnerJoin((a, b, c) => c.Id == b.AreaId)
    .GroupBy((a, b, c) => new { a.Title, c.Name })
    .Having(g => g.Count() < 300 || g.Avg(g.Value.Item1.Clicks) < 100)
    .ToList(g => new { count = g.Count(), Name = g.Key.Name });
//SELECT count(1), c.name
//FROM topic a
//LEFT JOIN cagetory b ON b.id = a.category_id
//LEFT JOIN area c ON c.id = b.area_id
//GROUP BY a.title, c.name
//HAVING count(1) < 300 AND avg(a.clicks) < 100
  • g.Value.Item1 对应 Topic
  • g.Value.Item2 对应 Category
  • g.Value.Item3 对应 Area

| 说明 | 方法 | SQL | |

]]>
查询 https://freesql.net/guide/select.html https://freesql.net/guide/select.html 查询 查询 FreeSql 在查询数据下足了功夫,链式风格、多表查询、表达式函数、导航属性支持得非常到位。 表达式函数 更详细请前往 SqlServer WithLock/WithIndex 多表: 全局设置 NoLock: 动态过滤 WhereDynamicFilter 《高效理解 FreeSql WhereDynamicFilter,深入了解设计初衷》 ... Thu, 04 Feb 2021 16:03:18 GMT FreeSql 在查询数据下足了功夫,链式风格、多表查询、表达式函数、导航属性支持得非常到位。

| | | | |

]]>
类型映射 https://freesql.net/guide/type-mapping.html https://freesql.net/guide/type-mapping.html 类型映射 类型映射 类型映射(默认) 提示:因排版问题,不显示所有支持的数据库 string 指定长度 [Column(DbType = &quot;varchar(max)&quot;)] 或者 [MaxLength(-1)] 或者 [Column(StringLength = -1)],当长度 -1 时产生的映射如下: 注意:MySql [MaxLength(-2)] 或者 [... Thu, 04 Feb 2021 16:03:18 GMT 类型映射(默认)

提示:因排版问题,不显示所有支持的数据库

| csharp | MySql | SqlServer | PostgreSQL | Oracle | Sqlite | 达梦 | |

]]>
API 文档 https://freesql.net/reference/api.html https://freesql.net/reference/api.html API 文档 API 文档 FreeSqlBuilder IFreeSql BaseRepository&lt;TEntity, TKey&gt; 状态管理,可实现 Update 只更新变化的字段(不更新所有字段),灵活使用 Attach 和 Update 用起来非常舒服。 DbContext 与 BaseRepository 功能大致类似。 DbContext 自身 = 完整... Thu, 04 Feb 2021 16:03:18 GMT FreeSqlBuilder

| 方法 | 返回值 | 说明 | |

]]>
与 Dapper 比较 https://freesql.net/reference/vs-dapper.html https://freesql.net/reference/vs-dapper.html 与 Dapper 比较 与 Dapper 比较 众所周知 Dapper 是 .NET 下最轻最快的 ORM,它是喜欢写 SQL 码农的福音,相对于 SqlHelper 它更加方便,据统计 10 个 .NETer 有 9 个 用过 Dapper。 这篇文章为准备使用 FreeSql 的朋友解惑,对比 Dapper 有何优势,为什么要使用 FreeSql?希望本文内容对大家有所帮... Thu, 04 Feb 2021 16:03:18 GMT 众所周知 Dapper 是 .NET 下最轻最快的 ORM,它是喜欢写 SQL 码农的福音,相对于 SqlHelper 它更加方便,据统计 10 个 .NETer 有 9 个 用过 Dapper。

这篇文章为准备使用 FreeSql 的朋友解惑,对比 Dapper 有何优势,为什么要使用 FreeSql?希望本文内容对大家有所帮助。

关于性能(输了)

Dapper + SQL 是大家一般所用的方式,性能对比主要体现在两个阶段:

1、执行前,表达式树解析,拼接 SQL 的消耗:

  • Dapper 几乎没有消耗;
  • FreeSql 会存在递归解析、对象拆箱等操作;

从项目工程可维护性角度看,这一点性能损失是能被容忍的,请看下面的测试结果。

2、执行后,返回数据转换为 List:

  • Dapper 采用 Emit 构造委托并缓存,性能接近原生代码;
  • FreeSql 采用 ExpressionTree 构造委托并缓存,为了映射类型更加易用使用了一点装箱操作,性能比 Dapper 略低;
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19044
Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=6.0.100
  [Host]     : .NET Core 5.0.11 (CoreCLR 5.0.1121.47308, CoreFX 5.0.1121.47308), X64 RyuJIT DEBUG
  Job-LEQVAV : .NET Core 5.0.11 (CoreCLR 5.0.1121.47308, CoreFX 5.0.1121.47308), X64 RyuJIT DEBUG

BuildConfiguration=Debug  InvocationCount=1  UnrollFactor=1

| Method | size | Mean | Error | StdDev | Median | Rank | |

]]>
前言 https://freesql.net/guide/BaseEntity.html https://freesql.net/guide/BaseEntity.html 前言 前言 尝试过 ado.net、dapper、ef,以及 Repository 仓储,甚至自己还写过生成器工具,以便做常规 CRUD 操作。 它们日常操作不方便之处: 每次使用前需要声明,再操作; 很多人一个实体类,对应一个操作类(或 DAL、DbContext、Repository); 本文介绍 BaseEntity 一种极简约的 CRUD 操作方法。... Fri, 16 Oct 2020 05:57:03 GMT 尝试过 ado.net、dapper、ef,以及 Repository 仓储,甚至自己还写过生成器工具,以便做常规 CRUD 操作。

它们日常操作不方便之处:

  • 每次使用前需要声明,再操作;

  • 很多人一个实体类,对应一个操作类(或 DAL、DbContext、Repository);

本文介绍 BaseEntity 一种极简约的 CRUD 操作方法。

功能特点

  • 自动迁移实体结构(CodeFirst),到数据库;

  • 直接操作实体的方法,进行 CRUD 操作;

  • 简化用户定义实体类型,省去主键、常用字段的配置(如 CreateTime、UpdateTime);

  • 实现单表、多表查询的软删除逻辑;

声明

参考 BaseEntity.cs 源码(约 100 行),copy 到项目中使用,然后添加 nuget 引用包:

dotnet add package FreeSql.DbContext

dotnet add package FreeSql.Provider.Sqlite

1、定义一个主键 int 并且自增的实体类型,BaseEntity TKey 指定为 int/long 时,会认为主键是自增;

public class UserGroup : BaseEntity<UserGroup, int> {
    public string GroupName { get; set; }
}

如果不想主键是自增键,可以重写属性:

public class UserGroup : BaseEntity<UserGroup, int> {
    [Column(IsIdentity = false)]
    public override int Id { get; set; }
    public string GroupName { get; set; }
}

有关更多实体的特性配置,可参阅 实体属性

2、定义一个主键 Guid 的实体类型,保存数据时会自动产生有序不重复的 Guid 值(不用自己指定 Guid.NewGuid());

public class User : BaseEntity<UserGroup, Guid> {
    public string UserName { get; set; }
}

3、定义多主键的实体类型,可以在 static 构造函数中重写字段名;

public class User2 : BaseEntity<User2, Guid, int> {
    static User2()
    {
        User2.Orm.CodeFirst.ConfigEntity<User2>(t =>
        {
            t.Property(a => a.PkId1).Name("UserId");
            t.Property(a => a.PkId2).Name("Index");
        });
    }

    public string Username { get; set; }
}

CRUD 使用

//添加
var item = new UserGroup { GroupName = "组一" };
item.Insert();

//更新
item.GroupName = "组二";
item.Update();

//添加或更新
item.Save();

//软删除
item.Delete();

//恢复软删除
item.Restore();

//根据主键获取对象
var item = UserGroup.Find(1);

//查询数据
var items = UserGroup.Where(a => a.Id > 10).ToList();

实体类型.Select 是一个查询对象,使用方法和 FreeSql.ISelect 一样;

支持多表查询时,软删除条件会附加在每个表中;

有关更多查询方法,可参阅 查询

示范项目:https://github.com/dotnetcore/FreeSql/tree/master/Examples/base_entity

]]>
入门 https://freesql.net/guide/ https://freesql.net/guide/ 入门 入门 FreeSql 是一款功能强大的对象关系映射(O/RM)组件,支持 .NET Core 2.1+、.NET Framework 4.0+ QQ 群:561616019(在线)、4336577(已满)、8578575(已满)、52508226(已满) 反馈问题请前往 https://github.com/dotnetcore/FreeSql/iss... Fri, 16 Oct 2020 05:57:03 GMT FreeSql 是一款功能强大的对象关系映射(O/RM)组件,支持 .NET Core 2.1+、.NET Framework 4.0+

QQ 群:561616019(在线)、4336577(已满)、8578575(已满)、52508226(已满)

反馈问题请前往 https://github.com/dotnetcore/FreeSql/issues

安装包

需要访问什么数据库,就安装对应的 FreeSql.Provider.XX,也可直接安装 FreeSql.All

| Package Name | 说明 | |

]]>
AOP✨ https://freesql.net/guide/aop.html https://freesql.net/guide/aop.html AOP✨ AOP✨ FreeSql AOP 已有的功能介绍,未来为会根据用户需求不断增强。 审计命令(如何监视 SQL?) 如果因为某个 sql 骚操作耗时很高,没有一个相关的审计功能,排查起来可以说无从下手 fsql.Aop.CommandBefore、fsql.Aop.CommandAfter 这两个事件触发所有 SQL 命令的执行前、和执行后。 执行后的事... Fri, 16 Oct 2020 05:57:03 GMT FreeSql AOP 已有的功能介绍,未来为会根据用户需求不断增强。

审计命令(如何监视 SQL?)

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

fsql.Aop.CommandBefore、fsql.Aop.CommandAfter 这两个事件触发所有 SQL 命令的执行前、和执行后。

执行后的事件会附带异常信息、耗时信息等。

建议在开发模式下开启无参数化模式,new FreeSqlBuilder().UseNoneCommandParameter(true)。

提示:new FreeSqlBuilder().UseMonitorCommand 也可以审计命令执行前后。

fsql.Aop.CommandBefore += (s, e) =>
{
    //e.Command.CommandText = null; 可拦截命令
};

fsql.Aop.CommandAfter += (s, e) =>
{
    if (e.Exception != null)
    {
        //做一些日志记录的操作。以下为示例。
        Trace.WriteLine($"Message:{e.Exception.Message }\r\nStackTrace:{e.Exception.StackTrace}\r\nCommandText:{e.Command.CommandText}");
    }
};

审计属性值

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

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;

v3.2.666 可设置 e.ObjectAuditBreak = true 中断对象审计,变相实现每个对象只触发一次 AuditValue 事件

审计迁移脚本

FreeSql 自带迁移功能,那么迁移的 SQL 语句长啥样,你可能会好奇。

  • 比如创建表时;

  • 比如添加字段时;

  • 比如修改表名、修改字段名时;

  • 又比如字段类型更改之后时;

这些操作在 FreeSql.CodeFirst 实现下基本不需要理会,而且我们只推荐在开发环境使用自动迁移的功能,正式环境可使用其他工具替代此操作。

但我们仍然可能需要对项目做完整的日志记录。

fsql.Aop.SyncStructureBefore、fsql.Aop.SyncStructureAfter 这两个事件将排上用场。

ConfigEntity

统一设置架构名

提前设置 FreeSqlBuilder AOP 优先级:

UseMappingPriority(MappingPriorityType.Attribute, MappingPriorityType.FluentApi, MappingPriorityType.Aop)


fsql.Aop.ConfigEntity += (s, e) =>
{
    e.ModifyResult.Name = "public." + e.ModifyResult.Name;
    //提示:可以利用 AsyncLocal 动态设置表名 v3.2.833
};

MySql Enum 映射

默认情况 c# 枚举会映射为 MySql Enum 类型,如果想映射为 int 在 FreeSqlBuilder Build 之后执行以下 Aop 统一处理:

fsql.Aop.ConfigEntityProperty += (s, e) =>
{
    if (e.Property.PropertyType.IsEnum)
        e.ModifyResult.MapType = typeof(int);
};

修改 decimal 默认特性

因为默认 decimal 只支持 decimal(10,2),范围太小,我们可以全局修改 decimal 类型的支持范围,比如支持 decimal(18,6)

fsql1.Aop.ConfigEntityProperty += (s, e) =>
{
    if (e.Property.PropertyType == typeof(decimal)|| e.Property.PropertyType == typeof(decimal?))
    {
       e.ModifyResult.Precision = 18;
       e.ModifyResult.Scale = 6;
    }
};

字典表应用

利用 全局过滤器 + Aop 实现如下效果:

[MapTable(typeof(SysParam), nameof(SysParam.Value), "001")]
class Param001
{
    [Column(Name = "Value2")]
    public string Name { get; set; }
    [Column(Name = "Value3")]
    public int Age { get; set; }
}
fsql.Select<Param001>().ToList();
//SELECT Value2, Value3 FROM SysParam WHERE Value = '001'

public class SysParam
{
    public int Id { get; set; }
    public string Value { get; set; }
    public string Value2 { get; set; }
    public string Value3 { get; set; }
    //...
}

实现参考:https://github.com/dotnetcore/FreeSql/issues/1997

自定义实体特性

比如项目内已经使用了其它 orm,如 efcore,这样意味着实体中可能存在 [Key],但它与 FreeSql [Column(IsPrimary = true] 不同。

Q: FreeSql 实体特性为啥这么别扭?

A: 为了考虑一致性用法,全部封装在 ColumnAttribute 下,这样用户使用起来,不用到处 using 或者 回忆特性应该用哪个名字,如自增 [Column(IsIdentity = true)] 即可。

FreeSql 提供 AOP 自定义特性功能,实现与多个 orm 共同拥有一套实体特性,可避免重复定义特性。

v1.4.0+ 已自动识别 EFCore 实体特性 Key/Required/NotMapped/MaxLength/StringLength/DatabaseGenerated/Table/Column

fsql.Aop.ConfigEntity += (s, e) =>
{
    var attr = e.EntityType.GetCustomAttributes(typeof(MyTableAttribute), false).FirstOrDefault() as MyTableAttribute;
    if (attr != null)
        e.ModifyResult.Name = attr.Name; //表名
};
fsql.Aop.ConfigEntityProperty += (s, e) =>
{
    var attr = e.Property.GetCustomAttributes(typeof(MyColumnAttribute), false).FirstOrDefault() as MyColumnAttribute;
    if (attr != null)
        e.ModifyResult.Name = attr.Name; //字段名
};

[MyTable("xxx")]
class YourEntity
{
    [MyColumn("id")]
    public int pkid { get; set; }
}

class MyTableAttribute : Attribute
{
    public string Name { get; }
    public MyTableAttribute(string name)
    {
      this.Name = name;
    }
}
class MyColumnAttribute : Attribute
{
    public string Name { get; }
    public MyColumnAttribute(string name)
    {
      this.Name = name;
    }
}

Ado .net 读取拦截

fsql.Aop.AuditDataReader += (_, e) =>
{
    if (e.DataReader.GetFieldType(e.Index) == typeof(string) &&
        e.Value == DBNull.Value)
            e.Value = "";
};

表达式拦截

FreeSql 内部表达式支持非常丰富,对各大数据库的兼容度也做得很好。

有关表达式支持的程度,可参阅:表达式函数

即便如此丰富,也仍然无法满足用户需求,FreeSql 对外开放了自定义表达式解析接口:

fsql.Aop.ParseExpression += (s, e) =>
{
    if (e.Expression.NodeType == Call && e.Expression.Name == "get_Item")
        e.Result = "1111";
};

这个解析有点复杂,当 e.Expression 很复杂的时候,我们还提供了 e.FreeParse 方法,使用它相当于调用 FreeSql 内置表达式解析引擎,辅助您进行解析。

自定义全局类型转换

框架中,除基础类型以外可以使用 TypeHandlers 添加转换器,一个具体的类对应一个转换器。

现在假定你有个BT需求:把数据库中的 'A10' 转换成 枚举的 TestType.A(int值10)

  • 在 EF 中,框架遍历所有实体,在ctx创建时根据具体的 Enum 类型添加转换器。
  • 在 FreeSql 中,思路类似。ConfigEntityProperty 委托中可以获取到属性的类型,然后创建一个具体的转换器即可。
//配置代码
freeSql.Aop.ConfigEntityProperty += (s, e) =>
{
    if(e.Property.PropertyType.IsEnum)
    {
        EnumToValueStringHandler hander = new EnumToValueStringHandler(e.Property.PropertyType);
        FreeSql.Internal.Utils.TypeHandlers.TryAdd(hander.ModelType, hander);
    }
};

//转换器代码
public class EnumToValueStringHandler : ITypeHandler
{
    //ModelType这里使用 ModelType 来表达最后Handler针对那个Type进行处理
    private readonly Type enumType;
    Type ITypeHandler.Type { get => this.enumType; }
    public Type ModelType { get => this.enumType; }

    //构造函数上传递具体的 type 信息,就能针对具体的枚举执行转换了
    //你也可以根据你的需要替换为另一个Type类
    public EnumToValueStringHandler(Type enumType)
    {
        this.enumType = enumType;
    }

    // xxEnum -> string 附加A
    object ITypeHandler.Serialize(object value)
    {
        return "A" + ((TestType)value).ToString("D");
    }

    // string -> xxEnum 去掉A
    object ITypeHandler.Deserialize(object value)
    {
        return Enum.Parse<TestType>(((string)value).Replace("A", ""));
    }
}

核心思路是 ITypeHandler.Type 变成变量,可以从外部传递。额外有些问题要注意:

  1. 所有的 "xxxEnum" 都会执行这个转换,如果有多个数据库多种格式,需要在 Handler 中处理
  2. 实体类超级多,枚举属性超级多时,可能影响性能 。确实很多时建议不在实体类上修改,可以通过部分类(partial class),新加一个属性去处理。(属性设置为Ignore,在getter、setter中执行转换)
]]>
级联保存 https://freesql.net/guide/cascade-saving.html https://freesql.net/guide/cascade-saving.html 级联保存 级联保存 接下来的内容,严重依赖的正确配置,请先学会再继续向下! Topic:文章表 Category:分类表 Comment:评论表 Tag:标签表 ManyToOne(多对一):Topic(多个)关联 Category(一个); OneToOne(一对一):Topic(一个)关联 Content(一个); OneToMany(一对多:Topic(一... Fri, 16 Oct 2020 05:57:03 GMT 接下来的内容,严重依赖【导航属性】的正确配置,请先学会再继续向下!

  • Topic:文章表
  • Category:分类表
  • Comment:评论表
  • Tag:标签表
  • ManyToOne(多对一):Topic(多个)关联 Category(一个);
  • OneToOne(一对一):Topic(一个)关联 Content(一个);
  • OneToMany(一对多:Topic(一个)关联 Comment(多个);
  • ManyToMany(多对多):Topic(多个)关联 Tag(多个);

ManyToOne(多对一)不适合做级联保存,保存 Topic 的时候把 Category 也保存,显然不合理(思考原因),自下向上保存的功能太不可控了。因此下面只讲 OneToOne/OneToMany/ManyToMany 级联保存。

若以上内容不能理解,请多看几遍!

开启功能

本功能 2019 年实现的(稳定),可移步了解 2022 年新发布的《聚合根仓储》(级联更自动)

仓储默认关闭了级联功能,需要手工开启:

repo.DbContextOptions.EnableCascadeSave = true;

机制规则

1、OneToOne 级联保存

v3.2.606+ 支持,并且支持级联删除功能

2、OneToMany 追加或更新子表,不删除子表已存在的数据

repo.Insert(topic);
  • 不删除 Comment 子表已存在的数据
  • 当 topic.Comments 属性为 Empty 时,不做任何操作
  • 保存 topic.Comments 的时候,还会保存 topic.Comments[0-..] 的下级集合属性,向下 18 层

向下 18 层的意思,比如【文章】表,下面有集合属性【评论】,【评论】下面有集合属性【子评论】。

保存【文章】表对象的时候,他会向下检索出集合属性【评论】,然后如果【评论】被保存的时候,再继续向下检索出集合属性【子评论】。一起做 InsertOrUpdate 操作。

3、ManyToMany 完整对比保存中间表,外部表只追加不更新

完整对比保存中间表,对比【多对多】中间表已存在的数据,计算出添加、修改、删除执行。

示例

测试 1:追加保存 OneToMany


class Cagetory
{
    public Guid Id { get; set; }
    public string Name { get; set; }

    public Guid ParentId { get; set; }
    [Navigate(nameof(ParentId))]
    public List<Cagetory> Childs { get; set; }
}
public void TestOneToManyParent()
{
    var repo = fsql.GetRepository<Cagetory>();
    repo.DbContextOptions.EnableCascadeSave = true;
    var cts = new[]
    {
        new Cagetory
        {
            Name = "分类1",
            Childs = new List<Cagetory>(new[]
            {
                new Cagetory { Name = "分类1_1" },
                new Cagetory { Name = "分类1_2" },
                new Cagetory { Name = "分类1_3" }
            })
        },
        new Cagetory
        {
            Name = "分类2",
            Childs = new List<Cagetory>(new[]
            {
                new Cagetory { Name = "分类2_1" },
                new Cagetory { Name = "分类2_2" }
            })
        }
    };
    repo.Insert(cts);
    //执行创建表,和插入数据:
    //INSERT INTO "Cagetory"("Id", "Name", "ParentId") VALUES('5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f', '分类1', '00000000-0000-0000-0000-000000000000'), ('5d90afcb-ed57-f6f4-0082-cb6c5b531b3e', '分类2', '00000000-0000-0000-0000-000000000000')
    //INSERT INTO "Cagetory"("Id", "Name", "ParentId") VALUES('5d90afcb-ed57-f6f4-0082-cb6d0c1c5f1a', '分类1_1', '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f'), ('5d90afcb-ed57-f6f4-0082-cb6e74bd8eef', '分类1_2', '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f'), ('5d90afcb-ed57-f6f4-0082-cb6f6267cc5f', '分类1_3', '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f'), ('5d90afcb-ed57-f6f4-0082-cb7057c41d46', '分类2_1', '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e'), ('5d90afcb-ed57-f6f4-0082-cb7156e0375e', '分类2_2', '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e')
    cts[0].Name = "分类11";
    cts[0].Childs.Clear();
    cts[1].Name = "分类22";
    cts[1].Childs.Clear();
    repo.Update(cts);
    //UPDATE "Cagetory" SET "Name" = CASE "Id"
    //WHEN '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f' THEN '分类11'
    //WHEN '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e' THEN '分类22' END
    //WHERE ("Id" IN ('5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f','5d90afcb-ed57-f6f4-0082-cb6c5b531b3e'))
    //Childs.Clear 后没有执行删除子集合操作,说明没有做完整的对比
    cts[0].Name = "分类111";
    cts[0].Childs.Clear();
    cts[0].Childs.Add(new Cagetory { Name = "分类1_33" });
    cts[1].Name = "分类222";
    cts[1].Childs.Clear();
    cts[1].Childs.Add(new Cagetory { Name = "分类2_22" });
    repo.Update(cts);
    //UPDATE "Cagetory" SET "Name" = CASE "Id"
    //WHEN '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f' THEN '分类111'
    //WHEN '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e' THEN '分类222' END
    //WHERE ("Id" IN ('5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f','5d90afcb-ed57-f6f4-0082-cb6c5b531b3e'))
    //INSERT INTO "Cagetory"("Id", "Name", "ParentId") VALUES('5d90afe8-ed57-f6f4-0082-cb725df546ea', '分类1_33', '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f'), ('5d90afe8-ed57-f6f4-0082-cb7338a6214c', '分类2_22', '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e')
}
]]>
CodeFirst https://freesql.net/guide/code-first.html https://freesql.net/guide/code-first.html CodeFirst FreeSql 支持 CodeFirst 迁移结构至数据库,这应该是(O/RM)必须标配的一个功能。 与其他(O/RM)不同的是:FreeSql支持更多的数据库特性,而不只是支持基础的数据类型,这既是优点也是缺点,优点是充分利用数据库特性辅助开发,缺点是切换数据库变得困难。不同程序员的理念可能不太一致,FreeSql尽量把功能支持到极致,至于是否使用是... Fri, 16 Oct 2020 05:57:03 GMT FreeSql 支持 CodeFirst 迁移结构至数据库,这应该是(O/RM)必须标配的一个功能。

与其他(O/RM)不同的是:FreeSql支持更多的数据库特性,而不只是支持基础的数据类型,这既是优点也是缺点,优点是充分利用数据库特性辅助开发,缺点是切换数据库变得困难。不同程序员的理念可能不太一致,FreeSql尽量把功能支持到极致,至于是否使用是项目组技术衡量的另一个问题。

尽管多种数据库适配逻辑非常复杂,FreeSql始终秉承优化程序开发习惯的原则尽量去实现,中间碰到了一些非技术无法攻克的难题,比如数据库的自定义类型,和实体类本身就是一种冲突,为了减少使用成本,诸如此类的数据库功能没有得到支持。

IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.MySql, connectionString)
    .UseAutoSyncStructure(true) //自动同步实体结构【开发环境必备】,FreeSql不会扫描程序集,只有CRUD时才会生成表。
    .UseMonitorCommand(cmd => Console.Write(cmd.CommandText))
    .Build(); //请务必定义成 Singleton 单例模式

迁移结构

| 创建数据库 | Sqlite | Sql Server | MySql | PostgreSQL | Oracle | |

]]>
https://freesql.net/guide/config-entity-from-db-first.html https://freesql.net/guide/config-entity-from-db-first.html 可以解决,数据库有主键 + 自增,实体层没有配置对应的特性; 从数据库导入主键、自增信息,适用 DbFirst 模式,无须在实体类型上设置 [Column(IsPrimary)] 或者 ConfigEntity; 此功能目前可用于 mysql/sqlserver/postgresql/oracle。 开启该功能会增加首次执行时间(耗时情况和表数量有关)... Fri, 16 Oct 2020 05:57:03 GMT 可以解决,数据库有主键 + 自增,实体层没有配置对应的特性;

从数据库导入主键、自增信息,适用 DbFirst 模式,无须在实体类型上设置 [Column(IsPrimary)] 或者 ConfigEntity;

fsql.CodeFirst.IsConfigEntityFromDbFirst = true;

此功能目前可用于 mysql/sqlserver/postgresql/oracle。

开启该功能会增加首次执行时间(耗时情况和表数量有关)

优先级

数据库特性 > 实体特性 > FluentApi(配置特性) > Aop(配置特性)

]]>
自定义特性 https://freesql.net/guide/custom-attribute.html https://freesql.net/guide/custom-attribute.html 自定义特性 自定义特性 本功能可实现与其他 ORM 使用一套 Attribute,避免维护两份实体特性的烦恼: v1.4.0+ 已自动识别 EFCore 实体特性 Key/Required/NotMapped/MaxLength/StringLength/DatabaseGenerated/Table/Column 优先级 数据库特性 &gt; 实体特性 &gt; Fluen... Fri, 16 Oct 2020 05:57:03 GMT 本功能可实现与其他 ORM 使用一套 Attribute,避免维护两份实体特性的烦恼:

v1.4.0+ 已自动识别 EFCore 实体特性 Key/Required/NotMapped/MaxLength/StringLength/DatabaseGenerated/Table/Column

fsql.Aop.ConfigEntity += (s, e) => {
  var attr = e.EntityType.GetCustomAttributes(typeof(MyTableAttribute), false).FirstOrDefault() as MyTableAttribute;
  if (attr != null)
    e.ModifyResult.Name = attr.Name; //表名
};
fsql.Aop.ConfigEntityProperty += (s, e) => {
  var attr = e.Property.GetCustomAttributes(typeof(MyColumnAttribute), false).FirstOrDefault() as MyColumnAttribute;
  if (attr != null)
    e.ModifyResult.Name = attr.Name; //字段名
};

[MyTable("xxx")]
class YourEntity {
  [MyColumn("id")]
  public int pkid { get; set; }
}

class MyTableAttribute : Attribute {
  public string Name { get; }
  public MyTableAttribute(string name)
  {
    this.Name = name;
  }
}
class MyColumnAttribute : Attribute {
  public string Name { get; }
  public MyColumnAttribute(string name)
  {
    this.Name = name;
  }
}

优先级

数据库特性 > 实体特性 > FluentApi(配置特性) > AOP(配置特性)

]]>
DbContext https://freesql.net/guide/db-context.html https://freesql.net/guide/db-context.html DbContext DbContext FreeSql.DbContext 实现类似 EFCore 使用习惯,跟踪对象状态,最终通过 SaveChanges 方法提交事务。 特性 Select/Attach 快照对象,Update 只更新变化的字段; Add/AddRange 插入数据,适配各数据库优化执行 ExecuteAffrows/ExecuteIdentity/E... Fri, 16 Oct 2020 05:57:03 GMT FreeSql.DbContext 实现类似 EFCore 使用习惯,跟踪对象状态,最终通过 SaveChanges 方法提交事务。

特性

  • Select/Attach 快照对象,Update 只更新变化的字段;
  • Add/AddRange 插入数据,适配各数据库优化执行 ExecuteAffrows/ExecuteIdentity/ExecuteInserted;
  • AddOrUpdate 插入或更新;
  • SaveMany 方法快速保存导航对象(一对多、多对多);

安装

dotnet add package FreeSql.DbContext

如何使用

0、通用方法

using (var ctx = fsql.CreateDbContext()) {
  //var db1 = ctx.Set<Song>();
  //var db2 = ctx.Set<Tag>();

  var item = new Song { };
  ctx.Add(item);
  ctx.SaveChanges();
}

注意:DbContext 对象多线程不安全

1、在 OnConfiguring 方法上配置与 IFreeSql 关联

public class SongContext : DbContext {

  public DbSet<Song> Songs { get; set; }
  public DbSet<Tag> Tags { get; set; }

  protected override void OnConfiguring(DbContextOptionsBuilder builder) {
    builder.UseFreeSql(GlobalVar.fsql);
    //这里直接指定一个静态的 IFreeSql 对象即可,切勿重新 Build()
  }

  //每个 DbContext 只触发一次
  protected override void OnModelCreating(ICodeFirst codefirst)
  {
    codefirst.Entity<Song>(eb =>
    {
      eb.ToTable("tb_song");
      eb.Ignore(a => a.Field1);
      eb.Property(a => a.Title).HasColumnType("varchar(50)").IsRequired();
      eb.Property(a => a.Url).HasMaxLength(100);

      eb.Property(a => a.RowVersion).IsRowVersion();
      eb.Property(a => a.CreateTime).HasDefaultValueSql("current_timestamp");

      eb.HasKey(a => a.Id);
      eb.HasIndex(a => new { a.Id, a.Title }).IsUnique().HasName("idx_xxx11");

      //一对多、多对一
      eb.HasOne(a => a.Type).HasForeignKey(a => a.TypeId).WithMany(a => a.Songs);

      //多对多
      eb.HasMany(a => a.Tags).WithMany(a => a.Songs, typeof(Song_tag));
    });

    codefirst.Entity<SongType>(eb =>
    {
      eb.HasMany(a => a.Songs).WithOne(a => a.Type).HasForeignKey(a => a.TypeId);

      eb.HasData(new[]
      {
        new SongType
        {
          Id = 1,
          Name = "流行",
          Songs = new List<Song>(new[]
          {
            new Song{ Title = "真的爱你" },
            new Song{ Title = "爱你一万年" },
          })
        },
        new SongType
        {
          Id = 2,
          Name = "乡村",
          Songs = new List<Song>(new[]
          {
            new Song{ Title = "乡里乡亲" },
          })
        },
      });
    });

    codefirst.SyncStructure<SongType>();
    codefirst.SyncStructure<Song>();
  }
}

public class SongType {
  public int Id { get; set; }
  public string Name { get; set; }

  public List<Song> Songs { get; set; }
}
public 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; }

  public int TypeId { get; set; }
  public SongType Type { get; set; }
  public List<Tag> Tags { get; set; }

  public int Field1 { get; set; }
  public long RowVersion { get; set; }
}
public class Song_tag {
  public int Song_id { get; set; }
  public Song Song { get; set; }

  public int Tag_id { get; set; }
  public Tag Tag { get; set; }
}
public class Tag {
  [Column(IsIdentity = true)]
  public int Id { get; set; }

  public string Name { get; set; }

  public List<Song> Songs { get; set; }
}

使用的时候与 EFCore 类似:

long id = 0;

using (var ctx = new SongContext()) {

  var song = new Song { };
  await ctx.Songs.AddAsync(song);
  id = song.Id;

  var adds = Enumerable.Range(0, 100)
    .Select(a => new Song { Create_time = DateTime.Now, Is_deleted = false, Title = "xxxx" + a, Url = "url222" })
    .ToList();
  await ctx.Songs.AddRangeAsync(adds);

  for (var a = 0; a < adds.Count; a++)
    adds[a].Title = "dkdkdkdk" + a;

  ctx.Songs.UpdateRange(adds);

  ctx.Songs.RemoveRange(adds.Skip(10).Take(20).ToList());

  //ctx.Songs.Update(adds.First());

  adds.Last().Url = "skldfjlksdjglkjjcccc";
  ctx.Songs.Update(adds.Last());

  //throw new Exception("回滚");

  await ctx.SaveChangesAsync();
}

2、注入方式使用

public void ConfigureServices(IServiceCollection services) {
  services.AddSingleton<IFreeSql>(Fsql);
  services.AddFreeDbContext<SongContext>(options => options.UseFreeSql(Fsql));
}

在 mvc 中获取:

IFreeSql _orm;
public ValuesController(SongContext songContext) {
}

优先级

OnConfiguring > AddFreeDbContext

说明

  • DbContext 操作的数据在最后 SaveChanges 时才批量保存;
  • DbContext 内所有操作,使用同一个事务;
  • 当实体存在自增时,或者 Add/AddRange 的时候主键值为空,会提前开启事务;
  • 支持同步/异步方法;

合并机制

db.Add(new Xxx()); db.Add(new Xxx()); db.Add(new Xxx());

这三步,会合并成一个批量插入的语句执行,前提是它们没有自增属性。

适用 Guid 主键,Guid 主键的值不用设置,交给 FreeSql 处理即可,空着的 Guid 主键会在插入时获取有序不重值的 Guid 值。

又比如:

db.Add(new Xxx()); db.Add(new Xxx()); db.Update(xxx); db.Add(new Xxx());

Guid Id 的情况下,执行三次命令:前两次插入合并执行,update 为一次,后面的 add 为一次。

联级保存

请移步文档 【联级保存】

实体变化事件

全局设置:

fsql.SetDbContextOptions(opt => {
  opt.OnEntityChange = report => {
    Console.WriteLine(report);
  };
});

单独设置 DbContext 或者 UnitOfWork:

var ctx = fsql.CreateDbContext();
ctx.Options.OnEntityChange = report => {
  Console.WriteLine(report);
};

var uow = fsql.CreateUnitOfWork();
uow.OnEntityChange = report => {
  Console.WriteLine(report);
};

参数 report 是一个 List 集合,集合元素的类型定义如下:

public class ChangeInfo {
  public object Object { get; set; }
  public EntityChangeType Type { get; set; }
  /// <summary>
  /// Type = Update 的时候,获取更新之前的对象
  /// </summary>
  public object BeforeObject { get; set; }
}
public enum EntityChangeType { Insert, Update, Delete, SqlRaw }

| 变化类型 | 说明 | |

]]>
DbFirst https://freesql.net/guide/db-first.html https://freesql.net/guide/db-first.html DbFirst DbFirst 获取所有数据库 获取指定数据库的表信息 .NET Core CLI(推荐使用) 代码生成器FreeSql.Generator,是 FreeSql 的代码生成器,可生成实体类,支持将数据库实体动态生成实体,默认有二个模板,基于 Razor,可指定自定义模板 dotnet-tool安装 FreeSql.Generator 更新FreeSql... Fri, 16 Oct 2020 05:57:03 GMT
static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.MySql, connectionString)
    .Build(); //请务必定义成 Singleton 单例模式

获取所有数据库

var t1 = fsql.DbFirst.GetDatabases();
//返回字符串数组, ["cccddd", "test"]

获取指定数据库的表信息

var t2 = fsql.DbFirst.GetTablesByDatabase(fsql.DbFirst.GetDatabases()[0]);
//返回包括表、列详情、主键、唯一键、索引、外键、备注等等

var t3 = fsql.DbFirst.GetTableByName("table1");
//返回表的列详情、主键、唯一键、索引、备注等等

.NET Core CLI(推荐使用)

代码生成器FreeSql.Generator,是 FreeSql 的代码生成器,可生成实体类,支持将数据库实体动态生成实体,默认有二个模板,基于 Razor,可指定自定义模板

  • dotnet-tool安装 FreeSql.Generator
dotnet tool install -g FreeSql.Generator
  • 更新FreeSql.Generator
dotnet tool update -g FreeSql.Generator

新建目录,在地址栏输入 cmd 快速打开命令窗口,输入命令:

FreeSql.Generator --help

命令行工具生成实体类极大好处,后续再次生成覆盖操作等于一键完成,并且支持 Mac/Linux 平台。

详细解读:生成器是如何实现的?

C:\WINDOWS\system32>FreeSql.Generator --help
        ____                   ____         __
       / __/  ____ ___  ___   / __/ ___ _  / /
      / _/   / __// -_)/ -_) _\ \  / _ `/ / /
     /_/    /_/   \__/ \__/ /___/  \_, / /_/
                                    /_/


  # Github # https://github.com/dotnetcore/FreeSql v2.0.105

    FreeSql 快速生成数据库的实体类

    更新工具:dotnet tool update -g FreeSql.Generator


  # 快速开始 #

  > FreeSql.Generator -Razor 1 -NameOptions 0,0,0,0 -NameSpace MyProject -DB "MySql,Data Source=127.0.0.1;..."

     -Razor 1                  * 选择模板:实体类+特性
     -Razor 2                  * 选择模板:实体类+特性+导航属性
     -Razor "d:\diy.cshtml"    * 自定义模板文件

     -NameOptions              * 4个布尔值对应:
                                 首字母大写
                                 首字母大写,其他小写
                                 全部小写
                                 下划线转驼峰

     -NameSpace                * 命名空间

     -DB "MySql,data source=127.0.0.1;port=3306;user id=root;password=root;initial catalog=数据库;charset=utf8;sslmode=none;max pool size=2"
     -DB "SqlServer,data source=.;integrated security=True;initial catalog=数据库;pooling=true;max pool size=2"
     -DB "PostgreSQL,host=192.168.164.10;port=5432;username=postgres;password=123456;database=数据库;pooling=true;maximum pool size=2"
     -DB "Oracle,user id=user1;password=123456;data source=//127.0.0.1:1521/XE;pooling=true;max pool size=2"
     -DB "Sqlite,data source=document.db"
     -DB "Firebird,database=localhost:D:\fbdata\EXAMPLES.fdb;user=sysdba;password=123456;max pool size=2"
     -DB "Dameng,server=127.0.0.1;port=5236;user id=2user;password=123456789;database=2user;poolsize=2"
     -DB "KingbaseES,server=127.0.0.1;port=54321;uid=USER2;pwd=123456789;database=数据库"
     -DB "ShenTong,host=192.168.164.10;port=2003;database=数据库;username=SYSDBA;password=szoscar55;maxpoolsize=2"
                               * Dameng(达梦数据库)、KingbaseES(人大金仓数据库)、ShenTong(神舟通用数据库)

     -Filter                   Table+View+StoreProcedure
                               默认生成:表+视图+存储过程
                               如果不想生成视图和存储过程 -Filter View+StoreProcedure

     -Match                    表名或正则表达式,只生成匹配的表,如:dbo\.TB_.+

     -FileName                 文件名,默认:{name}.cs
     -Output                   保存路径,默认为当前 shell 所在目录
                               推荐在实体类目录创建 gen.bat,双击它重新所有实体类

常用选项

| 选项 | 说明 | | :

]]>
删除 https://freesql.net/guide/delete.html https://freesql.net/guide/delete.html 删除 删除 删除是一个非常危险的操作,FreeSql 默认仅支持单表、且有条件的删除方法。 若 Where 条件为空,将不执行真正的 SQL 删除操作。 1、动态条件 dywhere 可以是: 主键值 new[] { 主键值1, 主键值2 } Topic 对象 new[] { Topic对象1, Topic对象2 } new { id = 1 } 2、动态表... Fri, 16 Oct 2020 05:57:03 GMT 删除是一个非常危险的操作,FreeSql 默认仅支持单表、且有条件的删除方法。

Where 条件为空,将不执行真正的 SQL 删除操作。

IFreeSql fsql; //如何创建请移步入门文档

class Topic
{
    [Column(IsIdentity = true, IsPrimary = true)]
    public int Id { get; set; }
    public int Clicks { get; set; }
    public string Title { get; set; }
    public DateTime CreateTime { get; set; }
}

1、动态条件

fsql.Delete<Topic>(object dywhere).ExecuteAffrows()

dywhere 可以是:

  • 主键值
  • new[] { 主键值1, 主键值2 }
  • Topic 对象
  • new[] { Topic对象1, Topic对象2 }
  • new { id = 1 }
var t1 = fsql.Delete<Topic>(new[] { 1, 2 }).ExecuteAffrows();
//DELETE FROM `Topic` WHERE (`Id` = 1 OR `Id` = 2)

var t2 = fsql.Delete<Topic>(new Topic { Id = 1, Title = "test" }).ExecuteAffrows();
//DELETE FROM `Topic` WHERE (`Id` = 1)

var t3 = fsql.Delete<Topic>(new[] { new Topic { Id = 1, Title = "test" }, new Topic { Id = 2, Title = "test" } }).ExecuteAffrows();
//DELETE FROM `Topic` WHERE (`Id` in (1, 2))

var t4 = fsql.Delete<Topic>(new { id = 1 }).ExecuteAffrows();
//DELETE FROM `Topic` WHERE (`Id` = 1)

2、动态表名

fsql.Delete<Topic>(1).AsTable("Topic_201903").ExecuteAffrows(); //对 Topic_201903 表删除

3、删除条件

出于安全考虑,没有条件不执行删除动作,避免误删除全表数据。删除全表数据:fsql.Delete<T>().Where(a => true).ExecuteAffrows()

var t5 = fsql.Delete<Topic>().Where(a => a.Id == 1).ExecuteAffrows();
//DELETE FROM `Topic` WHERE (`Id` = 1)

var t6 = fsql.Delete<Topic>().Where("id = @id", new { id = 1 }).ExecuteAffrows();
//DELETE FROM `Topic` WHERE (id = @id)

var item = new Topic { Id = 1, Title = "newtitle" };
var t7 = fsql.Delete<Topic>().Where(item).ExecuteAffrows();
//DELETE FROM `Topic` WHERE (`Id` = 1)

var items = new List<Topic>();
for (var a = 0; a < 10; a++) items.Add(new Topic { Id = a + 1, Title = $"newtitle{a}", Clicks = a * 100 });
var t8 = fsql.Delete<Topic>().Where(items).ExecuteAffrows();
//DELETE FROM `Topic` WHERE (`Id` IN (1,2,3,4,5,6,7,8,9,10))

4、字典删除

var dic = new Dictionary<string, object>();
dic.Add("id", 1);
dic.Add("name", "xxxx");

fsql.DeleteDict(dic).AsTable("table1").ExecuteAffrows();
//提示:List<Dictionary<string, object>> 为批量删除

5、ISelect.ToDelete 高级删除

IDelete 默认不支持导航对象,多表关联等。ISelect.ToDelete 可将查询转为 IDelete,以便使用导航对象删除数据,如下:

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)

复杂删除使用此方法的好处:

  • 删除前可预览测试数据,防止错误删除操作;
  • 支持复杂的删除操作,例如:ISelect 上使用 Limit(10) 删除附合条件的前 10 条记录;

6、IBaseRepository 级联删除

1、第一种:基于【对象】级联删除

使用 Include/IncludeMany 贪婪加载 OneToOne/OneToMany/ManyToMany 导航属性,可以使用此方法级联删除它们。

var repo = fsql.GetRepository<UserGroup>();
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)

2、第二种:基于【数据库】级联删除

根据设置的导航属性,递归删除 OneToOne/OneToMany/ManyToMany 对应数据,并返回已删除的数据。此功能不依赖数据库外键

var repo = fsql.GetRepository<UserGroup>();
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 UserGroup
{
    [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; }
}

API

| 方法 | 返回值 | 参数 | 描述 | |

]]>
实体特性✨ https://freesql.net/guide/entity-attribute.html https://freesql.net/guide/entity-attribute.html 实体特性✨ 实体特性✨ v1.4.0+ 已自动识别 EF 特性 Key/Required/NotMapped/MaxLength/StringLength/DatabaseGenerated/Table/Column 表名 架构:[Table(Name = &quot;dbo.tb_topic&quot;)] 注意:带点的表名,使用 [Table(Name = &quot;`sys.confi... Fri, 16 Oct 2020 05:57:03 GMT v1.4.0+ 已自动识别 EF 特性 Key/Required/NotMapped/MaxLength/StringLength/DatabaseGenerated/Table/Column

表名

[Table(Name = "tb_topic")]
class Topic { }

架构:[Table(Name = "dbo.tb_topic")]

注意:带点的表名,使用 [Table(Name = "`sys.config`")] 解决

表名映射的几种方法,优先级从小到大:

  • 1、实体类名
  • 2、Aop fsql.Aop.ConfigEntity += (_, e) => e.ModifyResult.Name = "public.tabname"
  • 3、FluentApi fsql.CodeFirst.ConfigEntity(a => a.Name("public.tabname"))
  • 4、[Table(Name = "public.tabname")]
  • 5、AsTable fsql.Select<T>().AsTable((_, old) => "public.tabname").ToList()

v3.2.833 可通过 UseMappingPriority 调整优先级,使用 Aop 实现动态表名

属性:[Column(Name = "xxx")]

主键(Primary Key)

class Topic
{
    [Column(IsPrimary = true)]
    public int Id { get; set; }
}
  • 当没有指明主键时,命名为 id 的字段将成为主键;(不区分大小写)
  • 当主键是 Guid 类型时,插入时会自动创建(有序、不重复)的值,所以不需要自己赋值;(支持分布式)

联合主键,在多个属性标记特性

Oracle 主键名长度大于30 [OraclePrimaryKeyName(name)] class table {...}

自增(Identity)

class Topic
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
}
  • 当没有指明主键时,标记自增的成员将成为主键;
  • DbFirst 模式序列:[Column(IsIdentity = true, InsertValueSql = "seqname.nextval")]

唯一键(Unique Key)、索引(Index)

[Index("uk_phone", "Phone", true)]
[Index("uk_group_index", "Group,Index11", true)]
[Index("uk_group_index22", "Group,Index22 desc", true)]
class Topic
{
    public int Id { get; set; }
    public string Phone { get; set; }

    public string Group { get; set; }
    public int Index11 { get; set; }
    public string Index22 { get; set; }
}
  • 第三个参数 true 的时候是唯一键,false 的时候是普通索引。
  • 分表场景的索引:[Index("{tablename}_idx_01", "phone")]

数据库类型(DbType)

提示:一般只需使用 .NET 常用类型映射即可(如 int/string/DateTime 等),不需要单独设置 DbType 内容。

class Topic
{
    [Column(DbType = "varchar(128) NOT NULL default....")]
    public string Title { get; set; }
}

可以在类型上指定 NOT NULL,也可以通过 [Column(IsNullable = false)] 设置。

decimal 精度

class Topic
{
    [Column(Precision = 10, Scale = 2)]
    public decimal Amount { get; set; }
}

string 长度

class Topic
{
    [Column(StringLength = 128)]
    //或者 [MaxLength(128)]
    public string Title { get; set; }
}

当长度 -1 时产生的映射如下:

| MySql | PostgreSQL | SqlServer | Oracle | Sqlite | Firebird | DuckDB | MsAccess | 达梦 | 金仓 | 神通 | 南通 | |

]]>
过滤器 https://freesql.net/guide/filters.html https://freesql.net/guide/filters.html 过滤器 过滤器 IFreeSql 基础层实现了 Select/Update/Delete 可设置的全局过滤器功能,这些设置将追加到执行的 SQL WHERE 语句中。 Apply 泛型参数可以设置为任何类型,当使用 Select/Update/Delete 方法时会进行过滤器匹配尝试(try catch): 匹配成功的,将附加 where 条件; 匹配失败的,... Fri, 16 Oct 2020 05:57:03 GMT IFreeSql 基础层实现了 Select/Update/Delete 可设置的全局过滤器功能,这些设置将追加到执行的 SQL WHERE 语句中。

public static AsyncLocal<int> TenantId { get; set; } = new AsyncLocal<int>();

fsql.GlobalFilter
    .Apply<ITenant>("test1", a => a.TenantId == TenantId.Value)
    .Apply<AuthorTest>("test2", a => a.Name == "11")

    .ApplyOnly<AuthorTest>("test3", a => a.Name == "11")
    //指定类型精准设置

    .ApplyIf<TestAddEnum>("test4", () => TenantId.Value != 0, a => a.Id == TenantId.Value);
    //1.9.0 ApplyIf 委托的返回值(第二个参数) true 才生效

    .Apply<ITenant>("test5", a => a.TenantId == TenantId.Value, before: true)
    //v3.2.700 增加 before 将条件放在 where 最前面

Apply 泛型参数可以设置为任何类型,当使用 Select/Update/Delete 方法时会进行过滤器匹配尝试(try catch):

  • 匹配成功的,将附加 where 条件;
  • 匹配失败的,标记下次不再匹配,避免性能损耗;

ApplyOnly 泛型参数指定一个类型生效。

如何禁用?

fsql.Select<TestAddEnum>().ToList(); //所有生效
fsql.Select<TestAddEnum>().DisableGlobalFilter("test1").ToList(); //禁用 test1
fsql.Select<TestAddEnum>().DisableGlobalFilter().ToList(); //禁用所有

fsql.Update/Delete 方法效果同上。

租户字段(动态值)

请移步文档:【多租户 - 按租户字段区分】

]]>
Fluent API https://freesql.net/guide/fluent-api.html https://freesql.net/guide/fluent-api.html Fluent API 支持 Fluent API FreeSql 提供了 Fluent Api 的方式,使用链式调用,可在外部配置实体的数据库特性。Fluent Api 的方法命名与特性名保持一致,共三种使用方法,选择一种即可: fsql 是一个 IFreeSql 对象、配置尽量只执行一次,避免性能损耗 参考: ConfigEntity FreeSql.Extensions... Fri, 16 Oct 2020 05:57:03 GMT 支持 Fluent API

FreeSql 提供了 Fluent Api 的方式,使用链式调用,可在外部配置实体的数据库特性。Fluent Api 的方法命名与特性名保持一致,共三种使用方法,选择一种即可

fsql 是一个 IFreeSql 对象、配置尽量只执行一次,避免性能损耗 参考:《实体特性说明》

ConfigEntity

fsql.CodeFirst
    .ConfigEntity<Table1>(a =>
    {
        a.Name("dbo.table1");
        a.Property(b => b.Id).Name("table1_id").IsIdentity(true);
        a.Property(b => b.Name).DbType("varchar(100)").IsNullable(true);
    })
    .ConfigEntity<Table2>(a =>
    {
        a.Name("dbo.table2");
        a.Property(b => b.Id).Name("table2_id").IsIdentity(true);
    });

FreeSql.Extensions.EFModel(实验室) 实现了 EFCore FluentApi 代码让 FreeSql 生效

fsql.CodeFirst.ApplyConfigurationFromEFCore(typeof(BloggingContext), typeof(OrderingContext));

Entity

fsql.CodeFirst.Entity<Song>(eb => {
    eb.ToTable("tb_song");
    eb.Ignore(a => a.Field1);
    eb.Property(a => a.Title).HasColumnType("varchar(50)").IsRequired();
    eb.Property(a => a.Url).HasMaxLength(100);

    eb.Property(a => a.RowVersion).IsRowVersion();
    eb.Property(a => a.CreateTime).HasDefaultValueSql("current_timestamp");

    eb.HasKey(a => a.Id);
    eb.HasIndex(a => new { a.Id, a.Title }).IsUnique().HasName("idx_xxx11");

    //一对多、多对一
    eb.HasOne(a => a.Type).HasForeignKey(a => a.TypeId).WithMany(a => a.Songs);

    //多对多
    eb.HasMany(a => a.Tags).WithMany(a => a.Songs, typeof(Song_tag));
});

fsql.CodeFirst.Entity<SongType>(eb => {
    eb.HasMany(a => a.Songs).WithOne(a => a.Type).HasForeignKey(a => a.TypeId);
    eb.HasData(new[]
    {
        new SongType
        {
            Id = 1,
            Name = "流行",
            Songs = new List<Song>(new[]
            {
                new Song{ Title = "真的爱你" },
                new Song{ Title = "爱你一万年" },
            })
        },
        new SongType
        {
            Id = 2,
            Name = "乡村",
            Songs = new List<Song>(new[]
            {
                new Song{ Title = "乡里乡亲" },
            })
        },
    });
});

public class SongType {
    public int Id { get; set; }
    public string Name { get; set; }

    public List<Song> Songs { get; set; }
}
public 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; }

    public int TypeId { get; set; }
    public SongType Type { get; set; }

    public int Field1 { get; set; }
    public long RowVersion { get; set; }
}

IEntityTypeConfiguration

以继承接口IEntityTypeConfiguration形式配置实体的。

  • .NET Framework4.0 不支持

实体配置类

public class SongConfiguration : IEntityTypeConfiguration<Song>
{
    public void Configure(EfCoreTableFluent<Song> eb)
    {
        eb.ToTable("tb_song1");
        eb.Ignore(a => a.Field1);
        eb.Property(a => a.Title).HasColumnType("varchar(50)").IsRequired();
        eb.Property(a => a.Url).HasMaxLength(100);

        eb.Property(a => a.RowVersion).IsRowVersion();
        eb.Property(a => a.CreateTime).HasDefaultValueSql("current_timestamp");

        eb.HasKey(a => a.Id);
        eb.HasIndex(a => a.Title).IsUnique().HasName("idx_tb_song1111");

        //一对多、多对一
        eb.HasOne(a => a.Type).HasForeignKey(a => a.TypeId).WithMany(a => a.Songs);

        //多对多
        eb.HasMany(a => a.Tags).WithMany(a => a.Songs, typeof(Song_tag));
    }
}

二种使用方式

1.单个配置

fsql.CodeFirst.ApplyConfiguration(new SongConfiguration());

2.批量配置

fsql.CodeFirst.ApplyConfigurationsFromAssembly(typeof(SongConfiguration).Assembly);

优先级

数据库特性 > 实体特性 > FluentApi(配置特性) > Aop(配置特性)

]]>
插入或更新 https://freesql.net/guide/insert-or-update.html https://freesql.net/guide/insert-or-update.html 插入或更新 插入或更新 1、IFreeSql.InsertOrUpdate IFreeSql 定义了 InsertOrUpdate 方法实现添加或修改的功能,利用数据库特性: 当实体类有自增属性时,批量 InsertOrUpdate 最多可被拆成两次执行,内部计算出未设置自增值、和有设置自增值的数据,分别执行 insert into 和 上面讲到的 merge i... Fri, 16 Oct 2020 05:57:03 GMT 1、IFreeSql.InsertOrUpdate

IFreeSql 定义了 InsertOrUpdate 方法实现添加或修改的功能,利用数据库特性:

| Database | Features | | Database | Features | |

]]>
插入 https://freesql.net/guide/insert.html https://freesql.net/guide/insert.html 插入 插入 1、单条插入 2、返回自增 如果表有自增列,插入数据后应该要返回 id。 方法 1:(原始) 方法 2:(依赖 FreeSql.Repository) 仓储内部会将插入后的自增值填充给 items[0].Id (支持批量插入回填) DbFirst 模式序列:[Column(IsIdentity = true, InsertValueSql = &quot;... Fri, 16 Oct 2020 05:57:03 GMT
IFreeSql fsql; //如何创建请移步入门文档

class Topic
{
    [Column(IsIdentity = true, IsPrimary = true)]
    public int Id { get; set; }
    public int Clicks { get; set; }
    public string Title { get; set; }
    public DateTime CreateTime { get; set; }
}

var items = new List<Topic>();
for (var a = 0; a < 10; a++) items.Add(new Topic { Title = $"newtitle{a}", Clicks = a * 100 });

1、单条插入

var t1 = fsql.Insert(items[0]).ExecuteAffrows();
//INSERT INTO `Topic`(`Clicks`, `Title`, `CreateTime`)
//VALUES(?Clicks0, ?Title0, ?CreateTime0)

2、返回自增

如果表有自增列,插入数据后应该要返回 id。

方法 1:(原始)

long id = fsql.Insert(items[0]).ExecuteIdentity();
items[0].Id = id;

方法 2:(依赖 FreeSql.Repository)

IBaseRepository<Topic> repo = fsql.GetRepository<Topic>();  //可以从 IOC 容器中获取
repo.Insert(items[0]);

仓储内部会将插入后的自增值填充给 items[0].Id (支持批量插入回填)

DbFirst 模式序列:[Column(IsIdentity = true, InsertValueSql = "seqname.nextval")]

3、批量插入

var t2 = fsql.Insert(items).ExecuteAffrows();
//INSERT INTO `Topic`(`Clicks`, `Title`, `CreateTime`)
//VALUES(?Clicks0, ?Title0, ?CreateTime0), (?Clicks1, ?Title1, ?CreateTime1),
//(?Clicks2, ?Title2, ?CreateTime2), (?Clicks3, ?Title3, ?CreateTime3),
//(?Clicks4, ?Title4, ?CreateTime4), (?Clicks5, ?Title5, ?CreateTime5),
//(?Clicks6, ?Title6, ?CreateTime6), (?Clicks7, ?Title7, ?CreateTime7),
//(?Clicks8, ?Title8, ?CreateTime8), (?Clicks9, ?Title9, ?CreateTime9)

批量插入建议关闭参数化功能,使用 .NoneParameter() 提升执行效率。

当插入大批量数据时,内部分批执行,规则如下:

| | 数量 | 参数量 | |

]]>
LinqToSql https://freesql.net/guide/linq-to-sql.html https://freesql.net/guide/linq-to-sql.html LinqToSql LinqToSql linq to sql 写法过于生硬不灵活,left join 写法非常不好,建议慢慢放充转向 Labmda 链式风格,早日走向康庄大道。 IQueryable 方法入侵问题,如下图很多不能实现的,以及第三方包入侵的扩展方法,严重影响编程体验。 imageimage dotnet add package FreeSql.Extens... Fri, 16 Oct 2020 05:57:03 GMT linq to sql 写法过于生硬不灵活,left join 写法非常不好,建议慢慢放充转向 Labmda 链式风格,早日走向康庄大道。

IQueryable 方法入侵问题,如下图很多不能实现的,以及第三方包入侵的扩展方法,严重影响编程体验。

image
image

dotnet add package FreeSql.Extensions.Linq

特别说明

  • 请尽量不要在 ISelect 模式下的使用 Linq 方法:GroupJoin、Select、SelectMany、Join、DefaultIfEmpty;

  • 如果一定要在 ISelect 中使用 .Select() 方法,请务必在 .ToList() 之前调用它;

IQueryable

FreeSql 提供强大的数据查询对象 ISelect。

FreeSql.Extensions.Linq v1.4.0+ 实现了 IQueryable 查询对象常用功能,以便在各框架中交互使用。

//将 ISelect 转为 IQueryable
IQueryable<Student> queryable = fsql.Select<Student>().AsQueryable();

//Linq 方法查询
var t1 = queryable.Where(a => a.id == 1).FirstOrDefault();

//将 IQueryable 还原为 ISelect
ISelect<Studeng> select = queryable.RestoreToSelect();

注意:IQueryable 的实现目前不支持 GroupBy,可以考虑使用 RestoreSelect 方法转回 ISelect 进行查询

Where

var t1 = (
    from a in fsql.Select<Student>()
    where a.id == item.id
    select a
).ToList();

Select(指定字段)

var t1 = (
    from a in fsql.Select<Student>()
    where a.id == item.id
    select new { a.id }
).ToList();

CaseWhen

var t1 = (
    from a in fsql.Select<Student>()
    where a.id == item.id
    select new {
        a.id,
        a.name,
        testsub = new {
            time = a.age > 10 ? "大于" : "小于或等于"
        }
    }
).ToList();

Join

var t1 = (
    from a in fsql.Select<Student>()
    join b in fsql.Select<School>() on a.id equals b.StudentId
    select a
).ToList();

var t2 = (
    from a in fsql.Select<Student>()
    join b in fsql.Select<School>() on a.id equals b.StudentId
    select new { a.id, bid = b.id }
).ToList();

var t3 = (
    from a in fsql.Select<Student>()
    join b in fsql.Select<School>() on a.id equals b.StudentId
    where a.id == item.id
    select new { a.id, bid = b.id }
).ToList();

LeftJoin

var t1 = (
    from a in fsql.Select<Student>()
    join b in fsql.Select<School>() on a.id equals b.StudentId into temp
    from tc in temp.DefaultIfEmpty()
    select a
).ToList();

var t2 = (
    from a in fsql.Select<Student>()
    join b in fsql.Select<School>() on a.id equals b.StudentId into temp
    from tc in temp.DefaultIfEmpty()
    select new { a.id, bid = tc.id }
).ToList();

var t3 = (
    from a in fsql.Select<Student>()
    join b in fsql.Select<School>() on a.id equals b.StudentId into temp
    from tc in temp.DefaultIfEmpty()
    where a.id == item.id
    select new { a.id, bid = tc.id }
).ToList();

From(多表查询)

var t1 = (
    from a in fsql.Select<Student>()
    from b in fsql.Select<School>()
    where a.id == b.StudentId
    select a
).ToList();

var t2 = (
    from a in fsql.Select<Student>()
    from b in fsql.Select<School>()
    where a.id == b.StudentId
    select new { a.id, bid = b.id }
).ToList();

var t3 = (
    from a in fsql.Select<Student>()
    from b in fsql.Select<School>()
    where a.id == b.StudentId
    where a.id == item.id
    select new { a.id, bid = b.id }
).ToList();

GroupBy(分组)

var t1 = (
    from a in fsql.Select<Student>()
    where a.id == item.id
    group a by new {a.id, a.name } into g
    select new {
        g.Key.id, g.Key.name,
        cou = g.Count(),
        avg = g.Avg(g.Value.age),
        sum = g.Sum(g.Value.age),
        max = g.Max(g.Value.age),
        min = g.Min(g.Value.age)
    }
).ToList();
]]>
你不知道的功能 ✨ https://freesql.net/guide/more.html https://freesql.net/guide/more.html 你不知道的功能 ✨ 你不知道的功能 ✨ 1、备注 -&gt; 迁移到数据库 FreeSql CodeFirst 支持将 c# 代码内的注释,迁移至数据库的备注。先决条件: 1、实体类所在程序集,需要开启 xml 文档功能; 2、xml 文件必须与程序集同目录,且文件名:xxx.dll -&gt; xxx.xml; v1.5.0+ 版本增加了对 Description 特性的解析,优先... Fri, 16 Oct 2020 05:57:03 GMT 1、备注 -> 迁移到数据库

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

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

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

v1.5.0+ 版本增加了对 Description 特性的解析,优先级低于 c# 代码注释;

]]>
多租户 https://freesql.net/guide/multi-tenancy.html https://freesql.net/guide/multi-tenancy.html 多租户 多租户 什么是多租户 维基百科:“软件多租户是指一种软件架构,在这种软件架构中,软件的一个实例运行在服务器上并且为多个租户服务”。一个租户是一组共享该软件实例特定权限的用户。有了多租户架构,软件应用被设计成为每个租户提供一个 专用的实例包括该实例的数据的共享,还可以共享配置,用户管理,租户自己的功能和非功能属性。多租户和多实例架构相比,多租户分离了代表... Fri, 16 Oct 2020 05:57:03 GMT 什么是多租户

维基百科:“软件多租户是指一种软件架构,在这种软件架构中,软件的一个实例运行在服务器上并且为多个租户服务”。一个租户是一组共享该软件实例特定权限的用户。有了多租户架构,软件应用被设计成为每个租户提供一个 专用的实例包括该实例的数据的共享,还可以共享配置,用户管理,租户自己的功能和非功能属性。多租户和多实例架构相比,多租户分离了代表不同的租户操作的多个实例。

多租户用于创建 Saas(Software as-a service)应用(云处理)。

方案一:按租户字段区分

第1步:了解 AsyncLocal<int>

ThreadLocal 可以理解为字典 Dictionary<int, string> Key=线程ID Value=值,跨方法时只需要知道线程ID,就能取得对应的 Value。

我们知道跨异步方法可能造成线程ID变化,ThreadLocal 将不能满足我们使用。

AsyncLocal 是 ThreadLocal 的升级版,解决跨异步方法也能获取到对应的 Value。

public class TenantManager
{
    // 注意一定是 static 静态化
    static AsyncLocal<int> _asyncLocal = new AsyncLocal<int>();

    public static int Current
    {
        get => _asyncLocal.Value;
        set => _asyncLocal.Value = value;
    }
}

第2步:FreeSql 全局过滤器,让任何查询/更新/删除,都附带租户条件;

以下代码若当前没有设置租户值,则过滤器不生效,什么意思?

// 全局过滤器只需要在 IFreeSql 初始化处执行一次
// ITenant 可以是自定义接口,也可以是任何一个包含 TenantId 属性的实体类型,FreeSql 不需要为每个实体类型都设置过滤器(一次即可)
fsql.GlobalFilter.ApplyIf<ITenant>(
    "TenantFilter", // 过滤器名称
    () => TenantManager.Current > 0, // 过滤器生效判断
    a => a.TenantId == TenantManager.Current // 过滤器条件
);

TenantManager.Current = 0;
fsql.Select<T>().ToList(); // SELECT .. FROM T

TenantManager.Current = 1;
fsql.Select<T>().ToList(); // SELECT .. FROM T WHERE TenantId = 1

第3步:FreeSql Aop.AuditValue 对象审计事件,实现统一拦截插入、更新实体对象;

fsql.Aop.AuditValue += (_, e) =>
{
    if (TenantManager.Current > 0 && e.Property.PropertyType == typeof(int) && e.Property.Name == "TenantId")
    {
        e.Value = TenantManager.Current
    }
};

第4步:AspnetCore Startup.cs Configure 中间件处理租户逻辑;

public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        try
        {
            // 使用者通过 aspnetcore 中间件,解析 token 获得 租户ID
            TenantManager.Current = YourGetTenantIdFunction();
            await next();
        }
        finally
        {
            // 清除租户状态
            TenantManager.Current = 0;
        }
    });
    app.UseRouting();
    app.UseEndpoints(a => a.MapControllers());
}

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

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

可应用范围:

  • 子查询,一对多、多对多、自定义的子查询;
  • Join 查询,导航属性、自定义的 Join 查询;
  • Include/IncludeMany 的子集合查询;

暂时不支持【延时属性】的广播;

此功能和【过滤器】不同,用于单次多表查询条件的传播;

方案二:按租户分表

此方案要求每个租户对应不同的数据表,如 Goods_1、Goods_2、Goods_3 分别对应 租户1、租户2、租户3 的商品表。

这其实就是一般的分表方案,FreeSql 提供了分表场景的几个 API:

  • 创建表 fsql.CodeFirst.SyncStructure(typeof(Goods), "Goods_1")
  • 操作表 CURD
var goodsRepository = fsql.GetRepository<Goods>(null, old => $"{Goods}_{TenantManager.Current}");

上面我们得到一个仓储按租户分表,使用它 CURD 最终会操作 Goods_1 表。

更多说明参考:《FreeSql.Repository 仓储》《分表分库》

v3.2.833 动态设置表名

var fsql = new FreeSql.FreeSqlBuilder()
    .UseMappingPriority(MappingPriorityType.Attribute, MappingPriorityType.FluentApi, MappingPriorityType.Aop)
    ....;
fsql.Aop.ConfigEntity += (s, e) => { e.ModifyResult.Name = $"{TenantAccessor.Current}.{e.ModifyResult.Name}"; //表名 };

app.Use(async (context, next) =>
{
    // 使用者通过 aspnetcore 中间件,解析 token 得到租户信息
    string tenant = YourGetTenantFunction();
    using (new TenantAccessor(tenant))
    {
        await next();
    }
});

public class TenantAccessor : IDisposable
{
    static AsyncLocal<string> current = new AsyncLocal<string>();
    public static string Current => current.Value ?? "public";

    public TenantAccessor(string tenant)
    {
        current.Value = tenant;
    }

    public void Dispose()
    {
        current.Value = null;
    }
}

方案三:按租户分库

  • 场景1:同数据库实例(未跨服务器),租户间使用不同的数据库名或Schema区分,使用方法与方案二相同;
  • 场景2:跨服务器分库,本段讲解该场景;

第1步:FreeSql.Cloud 为 FreeSql 提供跨数据库访问,分布式事务TCC、SAGA解决方案,支持 .NET Core 2.1+, .NET Framework 4.0+.

原本使用 FreeSqlBuilder 创建 IFreeSql,需要使用 FreeSqlCloud 代替,因为 FreeSqlCloud 也实现了 IFreeSql 接口。

dotnet add package FreeSql.Cloud

or

Install-Package FreeSql.Cloud

FreeSqlCloud<string> fsql = new FreeSqlCloud<string>();

public void ConfigureServices(IServiceCollection services)
{
    fsql.DistributeTrace = log => Console.WriteLine(log.Split('\n')[0].Trim());
    fsql.Register("main", () =>
    {
        var db = new FreeSqlBuilder().UseConnectionString(DataType.SqlServer, "data source=main.db").Build();
        //db.Aop.CommandAfter += ...
        return db;
    });

    services.AddSingleton<IFreeSql>(fsql);
    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.Use(async (context, next) =>
    {
        try
        {
            // 使用者通过 aspnetcore 中间件,解析 token,查询  main 库得到租户信息。
            (string tenant, string connectionString) = YourGetTenantFunction();

            // 只会首次注册,如果已经注册过则不生效
            fsql.Register(tenant, () =>
            {
                var db = new FreeSqlBuilder().UseConnectionString(DataType.SqlServer, connectionString).Build();
                //db.Aop.CommandAfter += ...
                return db;
            });

            // 切换租户
            fsql.Change(tenant);
            await next();
        }
        finally
        {
            // 切换回 main 库
            fsql.Change("main");
        }
    });
    app.UseRouting();
    app.UseEndpoints(a => a.MapControllers());
}

第2步:直接使用 IFreeSql 访问租户数据库

public class HomeController : ControllerBase
{

    [HttpGet]
    public object Get([FromServices] IFreeSql fsql)
    {
        // 使用 fsql 操作当前租户对应的数据库
        return "";
    }
}
  • 临时访问其他数据库表,使用 FreeSqlCloud 对象 Use("db3").Select<T>().ToList()
  • 主库基础表,应该使用 FreeSqlCloud 对象 EntitySteering 设置固定永久定向到 main,而不需要使用 .Use 手工切换
fsql.EntitySteering = (_, e) =>
{
    if (e.EntityType == typeof(T))
    {
        //查询 T 自动定向 db3
        e.DBKey = "db3";
    }
};
]]>
导航属性 https://freesql.net/guide/navigate-attribute.html https://freesql.net/guide/navigate-attribute.html 导航属性 导航属性 FreeSql 提供 OneToMany, ManyToOne, ManyToMany, OneToOne, Parent, PgArrayToMany 六种导航属性关系。 导航属性能干什么? Where(a =&gt; a.Parent.Parent.Name == &quot;xx&quot;) Where(a =&gt; a.Childs.Any(b =&gt; b.tit... Fri, 16 Oct 2020 05:57:03 GMT FreeSql 提供 OneToMany, ManyToOne, ManyToMany, OneToOne, Parent, PgArrayToMany 六种导航属性关系。

导航属性能干什么?

导航属性进行多表查询非常方便,lambda 表达式中直接使用导航对象点点点,舒服!!

自定义配置

OneToMany/ManyToMany 支持的类型:ICollection<T>、List<T>、ObservableCollection<T>


//ManyToOne
public class User
{
    [Column(IsPrimary = true, IsIdentity = true)]
    public int Id { get; set; }
    public int GroupId { get; set; }

    //在 本实体 查找 GroupId 属性,与 Group.主键 关联
    [Navigate(nameof(GroupId))]
    public Group Group { get; set; }
}

//OneToMany
public class Group
{
    [Column(IsPrimary = true, IsIdentity = true)]
    public int Id { get; set; }
    public string GroupName { get; set; }

    //在 User 查找 GroupId 属性,与 本实体.主键 关联
    [Navigate(nameof(User.GroupId))]
    public List<User> Users { get; set; }
}

//ManyToMany
[Navigate(ManyToMany = typeof(TagSong))]
public List<Tag> Items { get; set; }
]]>
性能 https://freesql.net/guide/performance.html https://freesql.net/guide/performance.html 性能 性能 FreeSql 实现了强大功能的同时,性能没有受到影响,项目中使用反射或耗时的操作都经过了缓存处理。读取数据部分采用了 ExpressionTree,使得 FreeSql 解析实体数据的速度与 Dapper 非常接近。 插入测试 测试结果(52 个字段) 18W 解释:插入 18 万行记录,表格中的数字是执行时间(单位 ms) Oracle 插入... Fri, 16 Oct 2020 05:57:03 GMT FreeSql 实现了强大功能的同时,性能没有受到影响,项目中使用反射或耗时的操作都经过了缓存处理。读取数据部分采用了 ExpressionTree,使得 FreeSql 解析实体数据的速度与 Dapper 非常接近。

插入测试

测试结果(52 个字段)

| | 18W | 1W | 5K | 2K | 1K | 500 | 100 | 50 | |

]]>
读写分离 https://freesql.net/guide/read-write-splitting.html https://freesql.net/guide/read-write-splitting.html 读写分离 读写分离 FreeSql 支持数据库读写分离,本功能是客户端的读写分离行为,数据库服务器该怎么配置仍然那样配置,不受本功能影响,为了方便描述后面讲到的【读写分离】都是指客户端的功能支持。 各种数据库的读写方案不一,数据库端开启读写分离功能后,读写分离的实现大致分为以下几种: 1、nginx 代理,配置繁琐且容易出错; 2、中间件,如 MyCat; 3、... Fri, 16 Oct 2020 05:57:03 GMT FreeSql 支持数据库读写分离,本功能是客户端的读写分离行为,数据库服务器该怎么配置仍然那样配置,不受本功能影响,为了方便描述后面讲到的【读写分离】都是指客户端的功能支持。

各种数据库的读写方案不一,数据库端开启读写分离功能后,读写分离的实现大致分为以下几种:

1、nginx 代理,配置繁琐且容易出错;

2、中间件,如 MyCat;

3、在 client 端支持;

FreeSql 实现了第 3 种方案,支持一个【主库】多个【从库】,【从库】的查询策略为随机方式。

若某【从库】发生故障,将切换到其他可用【从库】,若已全部不可用则使用【主库】查询。

出现故障【从库】被隔离起来间隔性的检查可用状态,以待恢复。

var connstr = "Data Source=127.0.0.1;Port=3306;User ID=root;Password=root;" +
    "Initial Catalog=cccddd;Charset=utf8;SslMode=none;Max pool size=10";

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.MySql, connstr)
    .UseSlave("connectionString1", "connectionString2") //使用从数据库,支持多个
    .Build(); //请务必定义成 Singleton 单例模式

fsql.Select<T>().Where(a => a.Id == 1).ToOne(); //读【从库】(默认)

fsql.Select<T>().Master().WhereId(a => a.Id == 1).ToOne(); //强制读【主库】

fsql.Ado.Query<T>("/*master*/ select * from t where ..."); //强制读【主库】

使用 FreeSqlCloud 另一种读写分离

public enum DbEnum { db1, db2, db3 }

var fsql = new FreeSqlCloud();

fsql.Register(DbEnum.db3, () => new FreeSqlBuilder()
    .UseConnectionString(DataType.Sqlite, @"Data Source=:memory:;max pool size=1")
    .UseAutoSyncStructure(true).Build());
fsql.Register(DbEnum.db2, () => new FreeSqlBuilder()
    .UseConnectionString(DataType.Sqlite, @"Data Source=:memory:;max pool size=2")
    .UseAutoSyncStructure(true).Build());
fsql.Register(DbEnum.db1, () => new FreeSqlBuilder()
    .UseConnectionString(DataType.Sqlite, @"Data Source=:memory:;max pool size=3")
    .UseAutoSyncStructure(true).Build());

fsql.EntitySteering = (_, e) =>
{
    switch (e.MethodName)
    {
        case "Select":
            if (e.DBKey == DbEnum.db1) //判断主库时
            {
                var dbkeyIndex = new Random().Next(0, e.AvailableDBKeys.Length);
                e.DBKey = e.AvailableDBKeys[dbkeyIndex]; //重新定向到其他 db
            }
            break;
        case "Insert":
        case "Update":
        case "Delete":
        case "InsertOrUpdate":
            break;
    }
};
]]>
仓储 https://freesql.net/guide/repository.html https://freesql.net/guide/repository.html 仓储 FreeSql.DbContext 参考 abp vnext 接口规范,实现了通用的仓储层功能(CURD),理解成传统增强版(DAL)。 Select/Attach 快照对象,Update 只更新变化的字段; Insert 插入数据,适配各数据库优化执行 ExecuteAffrows/ExecuteIdentity/ExecuteInserted; 级... Fri, 16 Oct 2020 05:57:03 GMT FreeSql.DbContext 参考 abp vnext 接口规范,实现了通用的仓储层功能(CURD),理解成传统增强版(DAL)。

  • Select/Attach 快照对象,Update 只更新变化的字段;
  • Insert 插入数据,适配各数据库优化执行 ExecuteAffrows/ExecuteIdentity/ExecuteInserted;
  • 级联保存、级联删除(一对一、一对多、多对多);
  • 仓储 + 工作单元设计模式,风格简洁、统一;
public class Song
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public string Title { get; set; }
}

注意:Repository 对象多线程不安全,因此不应在多个线程上同时对其执行工作。

临时用法

var curd = fsql.GetRepository<Song>();

适合在局部代码中,临时的创建仓储,用完就扔掉。

泛型仓储(依赖注入)

方法 2、泛型仓储+依赖注入(.NET Core);

//先看入门文档注入 IFreeSql
services.AddFreeRepository();

//在控制器使用泛型仓储
public SongsController(IBaseRepository<Song> songRepository)
{
}

继承仓储(依赖注入)

//先看入门文档注入 IFreeSql
services.AddFreeRepository(typeof(SongRepository).Assembly); //如果没有继承的仓储,第二个参数不用传

//使用继承的仓储
public SongsController(SongRepository repo1, TopicRepository repo2)
{
}

public class SongRepository : BaseRepository<Song>
{
    public SongRepository(IFreeSql fsql) : base(fsql) {}

    //在这里增加 CURD 以外的方法
}

对比更新

只更新变化的属性:

var repo = fsql.GetRepository<Topic>();
var item = repo.Where(a => a.Id == 1).First();  //此时快照 item
item.Title = "newtitle";
repo.Update(item); //对比快照时的变化
//UPDATE `tb_topic` SET `Title` = ?p_0
//WHERE (`Id` = 1)

是不是觉得先查询再更新,啰嗦?

var repo = fsql.GetRepository<Topic>();
var item = new Topic { Id = 1 };
repo.Attach(item); //此时快照 item
item.Title = "newtitle";
repo.Update(item); //对比快照时的变化
//UPDATE `tb_topic` SET `Title` = ?p_0
//WHERE (`Id` = 1)

repo.CompareState(item) 可获取 item 的状态变化信息

/// <summary>
/// 比较实体,计算出值发生变化的属性,以及属性变化的前后值
/// </summary>
/// <param name="newdata">最新的实体对象,它将与附加实体的状态对比</param>
/// <returns>key: 属性名, value: [旧值, 新值]</returns>
Dictionary<string, object[]> CompareState(TEntity newdata);

需要注意在使用Repository更新时,不应在ColumnAttribute中指定ServerTime

var repo = fsql.GetRepository<Dictionaries>();
var item = await repo.Where(a => a.DictId == "1").FirstAsync();

//如果Column特性中存在ServerTime属性可能导致无法修改的情况
item.UpdateTime =  DateTime.Now;
await repo.UpdateAsync(item);

public class Dictionaries
{
    [Column(Name = "id", IsPrimary = true)]
    public string Id { get; set; }

    [Column(Name = "name")]
    public string Name { get; set; }

    [Column(Name = "update_time", ServerTime = DateTimeKind.Local)]
    public DateTime? UpdateTime { get; set; }
}

登陆信息(依赖注入)

repo.DbContextOptions.AuditValue 适合与 AddScoped(依赖注入) 信息结合,统一设置登陆信息。

如下示例:使用仓储插入/更新时自动使用登陆信息

services.AddSingleton(fsql);
services.AddScoped(typeof(IBaseRepository<>), typeof(MyRepository<>));
services.AddScoped(typeof(IBaseRepository<,>), typeof(MyRepository<,>));
services.AddScoped(r => new MyRepositoryOptions
{
    AuditValue = e => {
        var user = r.GetService<User>();
        if (user == null) return;
        if (e.AuditValueType == AuditValueType.Insert &&
            e.Object is IEntityCreated obj1 && obj1 != null) {
            obj1.CreatedUserId = user.Id;
            obj1.CreatedUserName = user.Username;
        }
        if (e.AuditValueType == AuditValueType.Update &&
            e.Object is IEntityModified obj2 && obj2 != null) {
            obj2.ModifiedUserId = user.Id;
            obj2.ModifiedUserName = user.Username;
        }
    }
});

class MyRepository<TEntity, TKey> : BaseRepository<TEntity, TKey> where TEntity : class
{
    public MyRepository(IFreeSql fsql, MyRepositoryOptions options) : base(fsql)
    {
        if (options?.AuditValue != null) DbContextOptions.AuditValue += (_, e) => options.AuditValue(e);
    }
}
class MyRepository<TEntity> : MyRepository<TEntity, long> where TEntity : class
{
    public MyRepository(IFreeSql fsql, MyRepositoryOptions options) : base(fsql, options) { }
}
class MyRepositoryOptions
{
    public Action<DbContextAuditValueEventArgs> AuditValue { get; set; }
}

兼容问题

SqlServer 提供的 output inserted 特性,在表使用了自增或数据库定义了默认值的时候,使用它可以快速将 insert 的数据返回。PostgreSQL 也有相应的功能,如此方便但不是每个数据库都支持。

当采用了不支持该特性的数据库(Sqlite/MySql/Oracle/达梦/南大通用/MsAccess),并且实体使用了自增属性,仓储批量插入将变为逐条执行,可以考虑以下改进:

  • 使用 uuid 作为主键(即 Guid);
  • 避免使用数据库的默认值功能;

联级保存

请移步文档 《联级保存》

API

| 属性 | 返回值 | 说明 | |

]]>
树型查询 ✨ https://freesql.net/guide/select-as-tree.html https://freesql.net/guide/select-as-tree.html 树型查询 ✨ 树型查询 ✨ 无限级分类(父子)是一种比较常用的表设计,每种设计方式突出优势的同时也带来缺陷,如: 方法 1:表设计中只有 parent_id 字段,困扰:查询麻烦(本文可解决); 方法 2:表设计中冗余子级 id 便于查询,困扰:添加/更新/删除的时候需要重新计算; 方法 3:表设计中存储左右值编码,困扰:同上; 方法 1 设计最简单,本文解决它的递... Fri, 16 Oct 2020 05:57:03 GMT 无限级分类(父子)是一种比较常用的表设计,每种设计方式突出优势的同时也带来缺陷,如:

  • 方法 1:表设计中只有 parent_id 字段,困扰:查询麻烦(本文可解决);
  • 方法 2:表设计中冗余子级 id 便于查询,困扰:添加/更新/删除的时候需要重新计算;
  • 方法 3:表设计中存储左右值编码,困扰:同上;

方法 1 设计最简单,本文解决它的递归查询问题,让使用透明化。

父子导航属性

FreeSql 导航属性之中,有针对父子关系的设置方式,如下:

public class Area
{
    [Column(IsPrimary = true)]
    public string Code { get; set; }

    public string Name { get; set; }
    public string ParentCode { get; set; }

    [Navigate(nameof(ParentCode))]
    public Area Parent { get; set; }
    [Navigate(nameof(ParentCode))]
    public List<Area> Childs { get; set; }
}

定义 Parent 属性,在表达式中可以这样:

fsql.Select<Area>().Where(a => a.Parent.Parent.Parent.Name == "中国").First();

定义 Childs 属性,在表达式中可以这样(子查询):

fsql.Select<Area>().Where(a => a.Childs.Any(c => c.Name == "北京")).First();

定义 Childs 属性,还可以使用【级联保存】、【贪婪加载】等等操作。

var repo = fsql.GetRepository<Area>();
repo.DbContextOptions.EnableCascadeSave = true;
repo.Insert(new Area
{
    Code = "100000",
    Name = "中国",
    Childs = new List<Area>(new[]
    {
        new Area
        {
            Code = "110000",
            Name = "北京",
            Childs = new List<Area>(new[]
            {
                new Area{ Code="110100", Name = "北京市" },
                new Area{ Code="110101", Name = "东城区" },
            })
        }
    })
});

1、ToTreeList

配置好父子属性之后,就可以这样用了:

var t1 = fsql.Select<Area>().ToTreeList();
Assert.Single(t1);
Assert.Equal("100000", t1[0].Code);
Assert.Equal("110000", t1[0].Childs[0].Code);
Assert.Equal("110100", t1[0].Childs[0].Childs[0].Code);
Assert.Equal("110101", t1[0].Childs[0].Childs[1].Code);

ToList 查询数据是平面的,ToTreeList 将返回的平面数据在内存加工为树型 List 返回。

2、AsTreeCte 递归删除

MySql 连接字符串需要增加 Allow User Variables=True,否则会有 MySqlException Parameter '@cte ids' must be defined

很常见的无限级分类表功能,删除树节点时,把子节点也处理一下。

fsql.Select<Area>()
    .Where(a => a.Name == "中国")
    .AsTreeCte()
    .ToDelete()
    .ExecuteAffrows(); //删除 中国 下的所有记录

如果软删除:

fsql.Select<Area>()
    .Where(a => a.Name == "中国")
    .AsTreeCte()
    .ToUpdate()
    .Set(a => a.IsDeleted, true)
    .ExecuteAffrows(); //软删除 中国 下的所有记录

3、AsTreeCte 递归查询

若不做数据冗余的无限级分类表设计,递归查询少不了,AsTreeCte 正是解决递归查询的封装,方法参数说明:

| 参数 | 描述 | |

]]>
贪婪加载 ✨ https://freesql.net/guide/select-include.html https://freesql.net/guide/select-include.html 贪婪加载 ✨ 贪婪加载 ✨ 1、子表ToList 接下来的内容,严重依赖的正确配置,请先学会再继续向下! 2、导航属性 ManyToOne/OneToOne Include 最终使用 Left Join 的方式(查询一次)返回多表记录。 3、集合属性 OneToMany/ManyToMany/PgArrayToMany IncludeMany 最终在 ToList ... Fri, 16 Oct 2020 05:57:03 GMT 1、子表ToList
//最多执行3次 SQL
fsql.Select<Song>().ToList(a => new
{
    all = a,
    list1 = fsql.Select<T2>().ToList(),
    list2 = fsql.Select<T2>().Where(b => b.SongId == a.Id).ToList()
});

//分组之后,最多执行3次 SQL
fsql.Select<Song>()
    .GroupBy(a => new { a.Author })
    .WithTempQuery(a => new { Author = a.Key.Author, Count = a.Count() })
    .ToList(a => new
    {
        a.Author, a.Count,
        list1 = fsql.Select<T2>().ToList(),
        list2 = fsql.Select<T2>().Where(b => b.Author == a.Author).ToList()
    });
]]>
延时加载 https://freesql.net/guide/select-lazy-loading.html https://freesql.net/guide/select-lazy-loading.html 延时加载 延时加载 FreeSql 支持延时加载,当需要用到的时候才加载(读数据库),支持 1对1、多对 1、1对多、多对多导航属性。 陷阱:延时加载功能的滥用,会造成多次与 DB 交互,性能降低,请谨慎。 例如:浏览某条订单信息的时候,才显示其对应的订单详细数据,只需要在 Model 导航属性前面添加 virtual 关键字。 延时加载功能默认被关闭的,使用此... Fri, 16 Oct 2020 05:57:03 GMT FreeSql 支持延时加载,当需要用到的时候才加载(读数据库),支持 1对1、多对 1、1对多、多对多导航属性。

陷阱:延时加载功能的滥用,会造成多次与 DB 交互,性能降低,请谨慎。

例如:浏览某条订单信息的时候,才显示其对应的订单详细数据,只需要在 Model 导航属性前面添加 virtual 关键字。

public class Order
{
    [Column(IsPrimary = true)]
    public int OrderID { get; set; }
    public string OrderTitle { get; set; }
    public string CustomerName { get; set; }
    public DateTime TransactionDate { get; set; }
    public virtual List<OrderDetail> OrderDetails { get; set; }
}
public class OrderDetail
{
    [Column(IsPrimary = true)]
    public int DetailId { get; set; }

    public int OrderId { get; set; }
    public virtual Order Order { get; set; }
}

延时加载功能默认被关闭的,使用此功能之前,请在声明处开启;

延时加载功能,依赖 FreeSql.Extensions.LazyLoading 包,请前往 nuget 下载;

new FreeSql.FreeSqlBuilder()
    ...
    .UseLazyLoading(true) //开启延时加载功能
    ...

var order1 = fsql.Select<Order>().Where(a => a.OrderID == 1).ToOne(); //查询订单表
var details1 = order.OrderDetails; //第一次访问,查询数据库
var details2 = order.OrderDetails; //第二次访问,不查
var order2 = details1[0].Order; //此时不查数据库 Order,因为 OrderDetails 查询出来的时候已填充了该属性

控制台输出内容:

SELECT a.`OrderID`, a.`OrderTitle`, a.`CustomerName`, a.`TransactionDate`
FROM `Order` a
WHERE (a.`OrderID` = 1)
limit 0,1

SELECT a.`DetailId`, a.`OrderId`
FROM `OrderDetail` a
WHERE (a.`OrderId` = 1)

FreeSql 延时加载支持 1对1、多对 1、1对多、多对多关系的导航属性,前三者大小同异,以下我们单独介绍多对多关系。

多对多延时加载

public partial class Song
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public DateTime? Create_time { get; set; }
    public bool? Is_deleted { get; set; }
    public string Title { get; set; }
    public string Url { get; set; }

    public virtual ICollection<Tag> Tags { get; set; }
}
public partial class Song_tag
{
    public int Song_id { get; set; }
    public virtual Song Song { get; set; }

    public int Tag_id { get; set; }
    public virtual Tag Tag { get; set; }
}
public partial class Tag
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public int? Parent_id { get; set; }
    public virtual Tag Parent { get; set; }

    public decimal? Ddd { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Song> Songs { get; set; }
}

如上有三个表,音乐、标签,以及他们的关系表。

var songs = fsql.Select<Song>().Limit(10).ToList(); //取10条音乐
var tags1 = songs[0].Tags; //第一次访问,查询数据库
var tags2 = Songs[0].Tags; //第二次访问,不查

控制台输出内容:

SELECT a.`Id`, a.`Create_time`, a.`Is_deleted`, a.`Title`, a.`Url`
FROM `Song` a
limit 0,10

SELECT a.`Id`, a.`Parent_id`, a.`Ddd`, a.`Name`
FROM `Tag` a
WHERE (exists(SELECT 1
FROM `Song_tag` b
WHERE (b.`Song_id` = 2 AND b.`Tag_id` = a.`Id`)
limit 0,1))

总结

优点:只在需要的时候加载数据,不需要预先计划,避免了各种复杂的外连接、索引、视图操作带来的低效率问题。

陷阱:滥用会造成多次与 DB 交互,性能降低,谨慎使用,谨慎使用,谨慎使用。

如果要在循环中使用数据,请使用贪婪加载,否则使用懒加载。

]]>
多表查询 ✨ https://freesql.net/guide/select-multi-table.html https://freesql.net/guide/select-multi-table.html 多表查询 ✨ 多表查询 ✨ 1、多表 Join 经验:一对多,分表只取最后一条记录 2、导航属性 Join 提示:正确配置 【导航关系】后,不需要手工调用 LeftJoin 3、WithoutJoin 4、子表Exists 提示:由于子查询的实体类与上层相同,使用 As(&quot;b&quot;) 指明别名,以便区分 5、子表In 6、子表List导航属性 效果等同于: 将集合属性快... Fri, 16 Oct 2020 05:57:03 GMT
IFreeSql fsql; //如何创建请移步入门文档

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

    public int CategoryId { get; set; }
    public Category Category { get; set; }
}
class Category
{
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public string Name { get; set; }

    public int ParentId { get; set; }
    public CategoryType Parent { get; set; }
    public List<Topic> Topics { get; set; }
}
class CategoryType
{
    public int Id { get; set; }
    public string Name { get; set; }
}

1、多表 Join

fsql.Select<Topic, Category, CategoryType>()
    .LeftJoin((a,b,c) => a.CategoryId == b.Id)
    .LeftJoin((a,b,c) => b.ParentId == c.Id)
    .Where((a,b,c) => c.Id > 0)
    .ToList((a,b,c) => new { a,b,c });

//或者
fsql.Select<Topic>().From<Category, CategoryType>((s, b, c) => s
    .LeftJoin(a => a.CategoryId == b.Id)
    .LeftJoin(a => b.ParentId == c.Id))
    .Where((a,b,c) => c.Id > 0)
    .ToList((a,b,c) => new { a,b,c });

//减少定义 a,b,c 写法
fsql.Select<Topic, Category, CategoryType>()
    .LeftJoin(w => w.t1.CategoryId == w.t2.Id)
    .LeftJoin(w => w.t2.ParentId == w.t3.Id)
    .Where(w => w.t3.Id > 0)
    .ToList(w => new { w.t1,w.t2,w.t3 });

//SELECT ...
//FROM `Topic` a
//LEFT JOIN `Category` b ON a.`CategoryId` = b.`Id`
//LEFT JOIN `CategoryType` c ON b.`ParentId` = c.`Id`
//WHERE c. `Id` > 0

经验:一对多,分表只取最后一条记录

2、导航属性 Join

fsql.Select<Topic>()
    .LeftJoin(a => a.Category.Id == a.CategoryId)
    .LeftJoin(a => a.Category.Parent.Id == a.Category.ParentId)
    .Where(a => a.Category.Parent.Id > 0)
    .ToList();
//SELECT a.`Id`, a.`Title`, a.`Clicks`, a.`CreateTime`, a.`CategoryId`, a__Category.`Id` as6, a__Category.`Name`, a__Category.`ParentId`
//FROM `Topic` a
//LEFT JOIN `Category` a__Category ON a__Category.`Id` = a.`CategoryId`
//LEFT JOIN `CategoryType` a__Category__Parent ON a__Category__Parent.`Id` = a__Category.`ParentId`

提示:正确配置 【导航关系】后,不需要手工调用 LeftJoin

3、WithoutJoin

fsql.Select<Order, Product, User>()
    .InnerJoin((o, p, u) => o.UserId == u.Id)
    .LeftJoin((o, p, u) => o.ProductId == p.Id)
    .WithoutJoin(t2: !includeProductInfo)
    .ToList((o, p, u) => new OrderDetailDto
    {
        OrderId = o.Id,
        OrderNo = o.OrderNo,
        Username = u.Username, // User (T3) 始终 JOIN,可直接引用
        ProductName = includeProductInfo ? p.Name : "N/A"
    });

// includeProductInfo = true: SQL 将 JOIN Product
// includeProductInfo = false: SQL 将不 JOIN Product,ProductName 会是 "N/A"

4、子表Exists

fsql.Select<Topic>()
    .Where(a => fsql.Select<Topic>().As("b").Where(b => b.Id == a.Id).Any())
    .ToList();
//SELECT a.`Id`, a.`Title`, a.`Clicks`, a.`CreateTime`, a.`CategoryId`
//FROM `Topic` a
//WHERE (exists(SELECT 1
//    FROM `Topic` b
//    WHERE (b.`Id` = a.`Id`)
//    limit 0,1))

提示:由于子查询的实体类与上层相同,使用 As("b") 指明别名,以便区分

5、子表In

fsql.Select<Topic>()
    .Where(a => fsql.Select<Topic>().As("b").ToList(b => b.Id).Contains(a.Id))
    .ToList();
//SELECT a.`Id`, a.`Title`, a.`Clicks`, a.`CreateTime`, a.`CategoryId`
//FROM `Topic` a
//WHERE (((a.`Id`) in (SELECT b.`Id`
//    FROM `Topic` b)))

6、子表List导航属性

fsql.Select<Category>()
    .Where(a => a.Topics.Any(b => b.Title.Contains("xx"))) //v3.2.600 以下使用 a.Topics.AsSelect()
    .ToList();

效果等同于:

fsql.Select<Category>()
    .Where(a => fsql.Select<Topic>().Any(b => b.CategoryId == a.Id && b.Title.Contains("xx")))
    .ToList();

将集合属性快速转换为 ISelect 进行子查询操作。

7、子表string.Join

v1.8.0+ string.Join + ToList 实现将子查询的多行结果,拼接为一个字符串,如:"1,2,3,4"

fsql.Select<Topic>().ToList(a => new
{
    id = a.Id,
    concat = string.Join(",", fsql.Select<StringJoin01>().ToList(b => b.Id))
});
//SELECT a.`Id`, (SELECT group_concat(b.`Id` separator ',')
//    FROM `StringJoin01` b)
//FROM `Topic` a

提示:子查询 string.Join + ToList 适配了 sqlserver/pgsql/oracle/mysql/sqlite/firebird/duckdb/达梦/金仓/南大/翰高 #405

8、子表First/Count/Sum/Max/Min/Avg

fsql.Select<Category>().ToList(a => new
{
    all = a,
    first = fsql.Select<Topic>().Where(b => b.CategoryId == a.Id).First(b => b.Id),
    count = fsql.Select<Topic>().Where(b => b.CategoryId == a.Id).Count(),
    sum = fsql.Select<Topic>().Where(b => b.CategoryId == a.Id).Sum(b => b.Clicks),
    max = fsql.Select<Topic>().Where(b => b.CategoryId == a.Id).Max(b => b.Clicks),
    min = fsql.Select<Topic>().Where(b => b.CategoryId == a.Id).Min(b => b.Clicks),
    avg = fsql.Select<Topic>().Where(b => b.CategoryId == a.Id).Avg(b => b.Clicks)
});

9、子表ToList

v3.2.650+ 以下最多执行3次 SQL

fsql.Select<Topic>().ToList(a => new
{
    all = a,
    list1 = fsql.Select<T2>().ToList(),
    list2 = fsql.Select<T2>().Where(b => b.TopicId == a.Id).ToList()
});

fsql.Select<Topic>()
    .GroupBy(a => new { a.Author })
    .WithTempQuery(a => new { Author = a.Key.Author, Count = a.Count() })
    .ToList(a => new
    {
        a.Author, a.Count,
        list1 = fsql.Select<T2>().ToList(),
        list2 = fsql.Select<T2>().Where(b => b.Author == a.Author).ToList()
    });

10、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

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

可应用范围:

  • 子查询,一对多、多对多、自定义的子查询;
  • Join 查询,导航属性、自定义的Join查询;
  • Include/IncludeMany 的子集合查询;

暂时不支持【延时属性】的广播;

此功能和【过滤器】不同,用于单次多表查询条件的传播;

]]>
返回数据 ✨ https://freesql.net/guide/select-return-data.html https://freesql.net/guide/select-return-data.html 返回数据 ✨ 返回数据 ✨ FreeSql 使用 ExpressionTree 读取数据记录,.NET 技术下除了原生代码,最快的方案是 Emit 和 ExpressionTree。 ExpressionTree 天然支持 .NET AOT 编译,FreeSql 在 2018 年做出的技术决策将在未来一直受益。 1、返回单条记录 FreeSql 约定,ToOne/F... Fri, 16 Oct 2020 05:57:03 GMT FreeSql 使用 ExpressionTree 读取数据记录,.NET 技术下除了原生代码,最快的方案是 Emit 和 ExpressionTree。

ExpressionTree 天然支持 .NET AOT 编译,FreeSql 在 2018 年做出的技术决策将在未来一直受益。

1、返回单条记录

Topic t1 = fsql.Select<Topic>().ToOne();

FreeSql 约定,ToOne/First 永远返回 null 或 有数据的实体对象,ToList 永远返回非 null 的 List<实体类型>

2、返回 List

List<Topic> t1 = fsql.Select<Topic>().ToList();

3、返回 TreeList

List<Area> t2 = fsql.Select<Area>.ToTreeList();
List<Area> t3 = fsql.Select<Area>.Where(a => a.Name = "湖北").AsTreeCte().ToTreeList();
//v1.6.0 AsTreeCte() 递归CTE查询 湖北 下的所有子分类

查询数据加工为树型,注意:实体需要配置父子导航属性

4、返回 List + 导航属性的数据

List<Topic> t4 = fsql.Select<Topic>().LeftJoin(a => a.Category.Id == a.CategoryId).ToList();
//此时会查询 Topic普通字段 + 导航对象Category 字段

更多导航属性的数据返回:贪婪加载

5、指定返回

//返回一个字段
List<int> t5 = fsql.Select<Topic>().ToList(a => a.Id);

//返回匿名类
List<匿名类> t6 = fsql.Select<Topic>().ToList(a => new { a.Id, a.Title });

//返回元组
List<(int, string)> t7 = fsql.Select<Topic>().ToList<(int, string)>("id, title");

//返回导航属性
List<匿名类> t8 = fsql.Select<Topic>().ToList(a => new
{
    a.Id, a.Title,
    a.Category //导航属性
});

//返回SQL字段
List<匿名类> t9 = fsql.Select<Topic>().ToList(a => new
{
    cstitle = "substr(a.title, 0, 2)", //将 substr(a.title, 0, 2) 作为查询字段
    csnow = Convert.ToDateTime("now()"), //将 now() 作为查询字段
});

//返回子查询的字段
List<匿名类> t10 = fsql.Select<Topic>().ToList(a => new
{
    a.Id,
    count = fsql.Select<T2>().Count(),
    max = fsql.Select<T2>().Max(b => b.Id),
    min = fsql.Select<T2>().Min(b => b.Id),
    name = fsql.Select<2>().First(b => b.name)
});

//返回子查询集合 v3.2.650+ 以下最多执行3次 SQL
List<匿名类> t11 = fsql.Select<Topic>().ToList(a => new
{
    a.Id,
    list1 = fsql.Select<T2>().ToList(),
    list2 = fsql.Select<T2>().Where(b => b.TopicId == a.Id).ToList()
});
List<匿名类> t12 = fsql.Select<Topic>()
    .GroupBy(a => new { a.Author })
    .WithTempQuery(a => new { Author = a.Key.Author, Count = a.Count() })
    .ToList(a => new
    {
        a.Author, a.Count,
        list1 = fsql.Select<T2>().ToList(),
        list2 = fsql.Select<T2>().Where(b => b.Author == a.Author).ToList()
    });

常量机制早期留给了原生 SQL,如果真的需要返回该字符串:"'xxx'"

6、忽略字段返回

参考实现:https://github.com/dotnetcore/FreeSql/issues/528

7、Dto 映射返回

fsql.Select<Song>().ToList<Dto>();
//情况1:Dto 与 Song 属性名相同的字段被查询,返回 List<Dto>

fsql.Select<Song>().ToList(a => new Dto { xxx = a.ext })
//情况2:Dto 与 Song 属性名相同的字段被查询,纠正映射 ext,返回 List<Dto>

fsql.Select<Song>().ToList(a => new Song { id = a.id })
//情况3:Lambda 与 Song 类型一样,只查询指定字段 id,返回 List<Song>

fsql.Select<Song>().ToList(a => new { id = a.id })
//情况4:Lambda 匿名类型,只查询指定字段 id,返回 List<匿名对象>

请仔细处理区别,请仔细处理区别,请仔细处理区别

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

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

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

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

GroupBy 所有方法不使用 DTO 映射规则

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

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

如:

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

DTO 查询只映射默认字段(普通属性),映射对象请使用:

导航对象:ToList(a => new Dto { Catalog = a.Catalog })

多表对象:ToList((a, b) => new Dto { Catalog = b })

8、ToChunk 分段返回

执行查询,分块返回数据,可减少内存开销。比如读取 10 万条数据,每次返回 100 条处理。

fsql.Select<Song>().OrderBy(a => a.Id).ToChunk(100, done =>
{
    List<Song> list = done.Object;
    //done.IsBreak = true; v1.7.0 停止读取
});

//新版本
var asyncEnum = fsql.Select<Song>().OrderBy(a => a.Id).ToChunkAsyncEnumerable(100);
await foreach (var items in asyncEnum)
{
    foreach (var item in items)
        Console.WriteLine(item.Nickname);
}

9、ToSql

每个 ToList 都可以使用 ToSql 返回 SQL String,有两个选项:

  • FieldAliasOptions.AsIndex(默认) 自动产生 as1, as2, as3 .... 字段别名,可以最大程度防止多表,存在相同字段的问题;
  • FieldAliasOptions.AsProperty 使用属性名作为字段别名,合适使用二次构造 SQL 再次执行;

10、执行 SQL

class xxx
{
    public int Id { get; set; }
    public string Path { get; set; }
    public string Title2 { get; set; }
}

List<xxx> t11 = fsql.Ado.Query<xxx>("select * from song");
List<(int, string ,string)> t12 = fsql.Ado.Query<(int, string, string)>("select * from song");
List<dynamic> t13 = fsql.Ado.Query<dynamic>("select * from song");

注意:Ado.Query 的实体特性是无效的,比如 [Column(Name = "xxx")] 无效

11、API

| 方法 | 返回值 | 参数 | 描述 | |

]]>
单表查询 https://freesql.net/guide/select-single-table.html https://freesql.net/guide/select-single-table.html 单表查询 单表查询 单表 WithSql 使用多次 WithSql 等于 UNION ALL 查询 v3.2.666 、 v3.2.666 WithMemory 使用内存数据进行查询 假设跨数据库服务器,或者数据表被缓存过,WithMemory 便可以实现数据表与内存关联查询。 Fri, 16 Oct 2020 05:57:03 GMT
IFreeSql fsql; //如何创建请移步入门文档

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

    public int CategoryId { get; set; }
}

单表

fsql.Select<Topic>()
    .Where(a => a.Id == 10)
    .ToList();
///SELECT a.`Id`, a.`Clicks`, a.`CategoryId`, a.`Title`, a.`CreateTime`
//FROM `Topic` a
//WHERE (a.`Id` = 10)

fsql.Select<Topic>()
    .Where(a => a.Id == 10 && a.Id > 10 || a.Clicks > 100)
    .ToList();
///SELECT a.`Id`, a.`Clicks`, a.`CategoryId`, a.`Title`, a.`CreateTime`
//FROM `Topic` a
//WHERE (a.`Id` = 10 AND a.`Id` > 10 OR a.`Clicks` > 100)

fsql.Select<Topic>()
    .Where(a => new []{1,2,3}.Contains(a.Id))
    .ToList();
//SELECT a.`Id`, a.`Clicks`, a.`CategoryId`, a.`Title`, a.`CreateTime`
//FROM `Topic` a
//WHERE (a.`Id` in (1,2,3))

WithSql

fsql.Select<Topic>()
    .WithSql("select * from Topic where clicks > @val", new { val = 10 })
    .Page(1, 10)
    .ToList()
//SELECT a.`Id`, a.`Clicks`, a.`CategoryId`, a.`Title`, a.`CreateTime`
//FROM (select * from Topic where clicks > @val) a

使用多次 WithSql 等于 UNION ALL 查询 请注意参数前缀

v3.2.666 UnionAll 联合查询WithTempQuery + FromQuery 嵌套查询

v3.2.666 WithMemory 使用内存数据进行查询

假设跨数据库服务器,或者数据表被缓存过,WithMemory 便可以实现数据表与内存关联查询。

var list = new List<Topic>();
list.Add(new Topic { ... });
list.Add(new Topic { ... });

fsql.Select<Topic>()
    .WithMemory(list)
    .ToList()
//SELECT a.`Id`, a.`Clicks`, a.`CategoryId`, a.`Title`, a.`CreateTime`
//FROM (
//  SELECT ...
//  UNION ALL
//  SELECT ...
//) a
]]>
分表分库 https://freesql.net/guide/sharding.html https://freesql.net/guide/sharding.html 分表分库 分表分库 理论知识 分表 - 从表面意思上看呢,就是把一张表分成 N 多个小表,每一个小表都是完整的一张表。分表后数据都是存放在分表里,总表只是一个外壳,存取数据发生在一个一个的分表里面。分表后单表的并发能力提高了,磁盘 I/O 性能也提高了。并发能力为什么提高了呢,因为查询一次所花的时间变短了,如果出现高并发的话,总表可以根据不同 的查询,将并发压力... Fri, 16 Oct 2020 05:57:03 GMT 理论知识

分表 - 从表面意思上看呢,就是把一张表分成 N 多个小表,每一个小表都是完整的一张表。分表后数据都是存放在分表里,总表只是一个外壳,存取数据发生在一个一个的分表里面。分表后单表的并发能力提高了,磁盘 I/O 性能也提高了。并发能力为什么提高了呢,因为查询一次所花的时间变短了,如果出现高并发的话,总表可以根据不同 的查询,将并发压力分到不同的小表里面。

分库 - 把原本存储于一个库的数据分块存储到多个库上,把原本存储于一个表的数据分块存储到多个表上。数据库中的数据量不一定是可控的,在未进行分表分库的情况下,随着时间和业务的发展,库中的表会越来越多,表中的数据量也会越来越大,相应地,数据操作,增删改查的开销也会越来越大;另外,一台服务器的资源(CPU、磁盘、内存、IO 等)是有限的,最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。

手工分表 AsTable

FreeSql 原生用法、FreeSql.Repository 仓储用法 都提供了 AsTable 方法对分表进行 CRUD 操作,例如:

var repo = fsql.GetRepository<Log>();
repo.AsTable(oldname => $"{oldname}_201903"); //对 Log_201903 表 CRUD
//repo.AsTable((type, 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 会自动创建不存在的分表;
  • 不可在分表分库的实体类型中使用《延时加载》;

SqlServer 跨库事务 可以使用 TransactionScope,如下:

var repoLog = fsql.GetRepository<Log>();
var repoComment = fsql.GetRepository<Comment>();
repoLog.AsTable(oldname => $"{201903}.dbo.{oldname}");
repoComment.AsTable(oldname => $"{201903}.dbo.{oldname}");

using (TransactionScope ts = new TransactionScope())
{
    repoComment.Insert(new Comment { ... });
    repoLog.Insert(new Log { ... });
    ts.Complete();
}

分布式数据库 TCC/SAGA 方案请移步:https://github.com/2881099/FreeSql.Cloud

自动分表 AsTable (beta)

【自动分表】不同于 CURD.AsTable 方法,目前第一期完成按【时间】自动分表(不支持分库)。

欢迎积极参与测试、反馈,请优先使用源代码进行测试,方便反馈定位问题,谢谢。

[Table(Name = "as_table_log_{yyyyMM}", AsTable = "createtime=2022-1-1(1 month)")]
class AsTableLog
{
    public Guid id { get; set; }
    public string msg { get; set; }
    public DateTime createtime { get; set; }
}

从 2022-1-1 开始至当前时间,每月创建一个分表,按 createtime 字段分表

若最大日期大于当前时间,可手工扩容分表:

var tableName = fsql.CodeFirst.GetTableByEntity(typeof(AsTableLog))
    .AsTableImpl
    .GetTableNameByColumnValue(DateTime.Parse("2023-7-1"), autoExpand: true);

//创建数据库表
if (fsql.DbFirst.ExistsTable(tableName) == false)
    fsql.CodeFirst.SyncStructure(typeof(AsTableLog), tableName);

| 示范 | 说明 | |

]]>
事务Transaction https://freesql.net/guide/transaction.html https://freesql.net/guide/transaction.html 事务Transaction 事务Transaction 1、常规事务 UnitOfWork 是对 DbTransaction 事务对象的封装,方便夹带私有数据。 提示:uow 范围内,尽量别使用 fsql 对象,以免不处在一个事务 使用 UnitOfWorkManager 管理 UnitOfWork,如下: 2、仓储事务(依赖注入) TransactionalAttribute ... Fri, 16 Oct 2020 05:57:03 GMT 1、常规事务

UnitOfWork 是对 DbTransaction 事务对象的封装,方便夹带私有数据。

using (var uow = fsql.CreateUnitOfWork())
{
    await uow.Orm.Insert(item).ExecuteAffrowsAsync(); //uow.Orm API 和 IFreeSql 一样
    await uow.Orm.Ado.ExecuteNoneQueryAsync(sql);

    await fsql.Insert(item)... //错误,不在一个事务

    var repo = uow.GetRepository<Song>(); //仓储 CRUD
    await repo.InsertAsync(item);

    uow.Commit();
}

提示:uow 范围内,尽量别使用 fsql 对象,以免不处在一个事务

使用 UnitOfWorkManager 管理 UnitOfWork,如下:

using (var uowManager = new UnitOfWorkManager(fsql))
{
    using (var uow = uowManager.Begin())
    {
        using (var uow2 = uowManager.Begin()) //与 uow 同一个事务
        {
            uow2.Commit(); //事务还未提交
        }
        uow.Commit(); //事务提交
    }
}

2、仓储事务(依赖注入)

var builder = WebApplication.CreateBuilder(args);
Func<IServiceProvider, IFreeSql> fsqlFactory = r =>
{
    IFreeSql fsql = new FreeSql.FreeSqlBuilder()
        .UseConnectionString(FreeSql.DataType.Sqlite, @"Data Source=freedb.db")
        .UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}"))
        .Build();
    return fsql;
};
builder.Services.AddSingleton<IFreeSql>(fsqlFactory);

builder.Services.AddFreeRepository();
builder.Services.AddScoped<UnitOfWorkManager>();
builder.Services.AddScoped<SongService>();
WebApplication app = builder.Build();


public class SongService
{
    readonly IBaseRepository<Song> _songRepository;
    readonly IBaseRepository<Detail> _detailRepository;
    readonly UnitOfWorkManager _unitOfWorkManager;

    public SongService(
      IBaseRepository<Song> songRepository,
      IBaseRepository<Detail> detailRepository,
      UnitOfWorkManager unitOfWorkManager
    )
    {
        _songRepository = songRepository;
        _detailRepository = detailRepository;
        _unitOfWorkManager = unitOfWorkManager;
    }

    [Transactional]
    public async Task Test1()
    {
        //所有注入的仓储对象,都是一个事务
        await _songRepository.InsertAsync(xxx1);
        await _detailRepository.DeleteAsync(xxx2);
        this.Test2();
    }

    [Transactional(Propagation = Propagation.Nested)]
    public void Test2() //嵌套事务
    {
    }

    public async Task Test3()
    {
      using (var uow = _unitOfWorkManager.Begin())
      {
          await _songRepository.InsertAsync(xxx1);
          await _detailRepository.DeleteAsync(xxx2);
          uow.Commit();
      }
    }
}

具体请移步文档:- AOP 特性标签实现跨方法事务

3、同线程事务

同线程事务内置在 FreeSql.dll,由 fsql.Transaction 管理事务提交回滚(缺点:不支持异步)。

用户购买了价值 100 元的商品:扣余额、扣库存。

fsql.Transaction(() =>
{
    //fsql.Ado.TransactionCurrentThread 获得当前事务对象

    var affrows = fsql.Update<User>()
        .Set(a => a.Wealth - 100)
        .Where(a => a.Wealth >= 100).ExecuteAffrows();
        //判断别让用户余额扣成负数

    //抛出异常,回滚事务,事务退出
    if (affrows < 1) throw new Exception("用户余额不足");

    affrows = fsql.Update<Goods>()
        .Set(a => a.Stock - 1)
        .Where(a => a.Stock >= 1).ExecuteAffrows();

    if (affrows < 1) throw new Exception("商品库存不足");
});

同线程事务使用简单,需要注意的限制:

  • 事务对象在线程挂载,每个线程只可开启一个事务连接,嵌套使用的是同一个事务;

  • 事务体内代码不可以切换线程,因此不可使用任何异步方法,包括 FreeSql 提供的数据库异步方法(可以使用任何 Curd 同步方法);

4、悲观锁

var user = fsql.Select<User>().ForUpdate(true).Where(a => a.Id == 1).ToOne();
//SELECT ... FROM User a for update nowait

for update 在 Oracle/PostgreSQL/MySql 是通用的写法,我们对 SqlServer 做了特别适配,执行的 SQL 语句大致如下:

SELECT ... FROM [User] a With(UpdLock, RowLock, NoWait)

5. 外部事务

// 场景:已经有一个开启的 Connection 和 Transaction
using (var conn = new SqlConnection("..."))
{
    conn.Open();
    using (var tran = conn.BeginTransaction())
    {
        // 1. 原生/Dapper 操作
        // command.Transaction = tran;
        // command.ExecuteNonQuery();

        // 2. 桥接给 FreeSql
        // 使用扩展方法创建适配器,传入现有的 tran
        using (var uow = fsql.CreateUnitOfWork(tran)) 
        {
            // 在此 uow 下获取的仓储或 Orm,都会使用传入的 tran
            var repo = uow.GetRepository<MyEntity>();
            repo.Insert(new MyEntity { Name = "FreeSql Insert" });
            
            // 或者直接使用 Orm
            uow.Orm.Insert(new MyEntity { Name = "Direct Orm Insert" }).ExecuteAffrows();

            // 这里的 Commit 只会触发 FreeSql 的事件,不会提交物理事务
            uow.Commit(); 
        }

        // 3. 真正的提交由最外层控制
        tran.Commit();
    }
}
]]>
UnitOfWork https://freesql.net/guide/unit-of-work.html https://freesql.net/guide/unit-of-work.html UnitOfWork UnitOfWork UnitOfWork 是对 DbTransaction 事务对象的封装,方便夹带私有数据。 如何使用 提示:uow 范围内,尽量别使用 fsql 对象,以免不处在一个事务 依赖注入(参考): 外部事务 接口定义 uow.GetOrBeginTransaction() 方法可获取事务对象。 实体变化事件 全局设置: 单独设置: 参数... Fri, 16 Oct 2020 05:57:03 GMT UnitOfWork 是对 DbTransaction 事务对象的封装,方便夹带私有数据。

如何使用

using (var uow = fsql.CreateUnitOfWork())
{
    await uow.Orm.Insert(item).ExecuteAffrowsAsync(); //uow.Orm API 和 IFreeSql 一样
    await uow.Orm.Ado.ExecuteNoneQueryAsync(sql);

    await fsql.Insert(item)... //错误,不在一个事务

    var repo1 = uow.GetRepository<Song>();
    await repo1.InsertAsync(item);

    uow.Commit();
}

提示:uow 范围内,尽量别使用 fsql 对象,以免不处在一个事务

依赖注入(参考):在 asp.net core 中使用 TransactionalAttribute + UnitOfWorkManager 实现多种事务传播

外部事务

// 场景:已经有一个开启的 Connection 和 Transaction
using (var conn = new SqlConnection("..."))
{
    conn.Open();
    using (var tran = conn.BeginTransaction())
    {
        // 1. 原生/Dapper 操作
        // command.Transaction = tran;
        // command.ExecuteNonQuery();

        // 2. 桥接给 FreeSql
        // 使用扩展方法创建适配器,传入现有的 tran
        using (var uow = fsql.CreateUnitOfWork(tran)) 
        {
            // 在此 uow 下获取的仓储或 Orm,都会使用传入的 tran
            var repo = uow.GetRepository<MyEntity>();
            repo.Insert(new MyEntity { Name = "FreeSql Insert" });
            
            // 或者直接使用 Orm
            uow.Orm.Insert(new MyEntity { Name = "Direct Orm Insert" }).ExecuteAffrows();

            // 这里的 Commit 只会触发 FreeSql 的事件,不会提交物理事务
            uow.Commit(); 
        }

        // 3. 真正的提交由最外层控制
        tran.Commit();
    }
}

接口定义

uow.GetOrBeginTransaction() 方法可获取事务对象。

public interface IUnitOfWork : IDisposable
{
    /// <summary>
    /// 该对象 Select/Delete/Insert/Update/InsertOrUpdate 与工作单元事务保持一致,可省略传递 WithTransaction
    /// </summary>
    IFreeSql Orm { get; }

    DbTransaction GetOrBeginTransaction(bool isCreate = true);

    IsolationLevel? IsolationLevel { get; set; }

    void Commit();

    void Rollback();

    /// <summary>
    /// 工作单元内的实体变化跟踪
    /// </summary>
    DbContext.EntityChangeReport EntityChangeReport { get; }

    /// <summary>
    /// 用户自定义的状态数据,便于扩展
    /// </summary>
    Dictionary<string, object> States { get; }
}

实体变化事件

全局设置:

fsql.SetDbContextOptions(opt =>
{
    opt.OnEntityChange = report =>
    {
        Console.WriteLine(report);
    };
});

单独设置:

using (var uow = fsql.CreateUnitOfWork())
{
    uow.OnEntityChange = report =>
    {
        Console.WriteLine(report);
    };
}

参数 report 是一个 List 集合,集合元素的类型定义如下:

public class ChangeInfo
{
    public object Object { get; set; }
    public EntityChangeType Type { get; set; }
    /// <summary>
    /// Type = Update 的时候,获取更新之前的对象
    /// </summary>
    public object BeforeObject { get; set; }
}
public enum EntityChangeType { Insert, Update, Delete, SqlRaw }

| 变化类型 | 说明 | |

]]>
修改 https://freesql.net/guide/update.html https://freesql.net/guide/update.html 修改 修改 FreeSql 提供丰富的数据库更新功能,支持单条或批量更新,在特定的数据库执行还可以返回更新后的记录。 1、动态条件 dywhere 可以是: 主键值 new[] { 主键值1, 主键值2 } Topic 对象 new[] { Topic对象1, Topic对象2 } new { id = 1 } 2、动态表名 3、更新条件 除了上面介绍的 d... Fri, 16 Oct 2020 05:57:03 GMT FreeSql 提供丰富的数据库更新功能,支持单条或批量更新,在特定的数据库执行还可以返回更新后的记录。

IFreeSql fsql; //如何创建请移步入门文档

class Topic
{
    [Column(IsIdentity = true, IsPrimary = true)]
    public int Id { get; set; }
    public int Clicks { get; set; }
    public string Title { get; set; }
    public DateTime CreateTime { get; set; }
}

1、动态条件

fsql.Update<Topic>(object dywhere)

dywhere 可以是:

  • 主键值
  • new[] { 主键值1, 主键值2 }
  • Topic 对象
  • new[] { Topic对象1, Topic对象2 }
  • new { id = 1 }

2、动态表名

fsql.Update<Topic>(1).AsTable("Topic_201903").ExecuteAffrows(); //对 Topic_201903 表更新

3、更新条件

除了上面介绍的 dywhere 构造参数外,还支持 Where lambda/sql 方法

出于安全考虑,没有条件不执行更新动作,避免误更新全表数据。更新全表数据:fsql.Update<T>().Where(a => true).Set(a => a.Xxx == xxx).ExecuteAffrows()

fsql.Update<Topic>()
    .Set(a => a.Title, "新标题")
    .Set(a => a.Time, DateTime.Now)
    .Where(a => a.Id == 1)
    .ExecuteAffrows();
//UPDATE `Topic` SET `Title` = @p_0, `Time` = @p_1
//WHERE (Id = 1)

4、更新指定列 Set

fsql.Update<Topic>(1)
    .Set(a => a.CreateTime, DateTime.Now)
    .ExecuteAffrows();
//UPDATE `Topic` SET `CreateTime` = '2018-12-08 00:04:59'
//WHERE (`Id` = 1)

支持 Set() 多次,相当于拼接

fsql.Update<Topic>(1)
    .Set(a => a.Clicks + 1)
    .Set(a => a.Time == DateTime.Now)
    .ExecuteAffrows();
//UPDATE `Topic` SET `Clicks` = ifnull(`Clicks`,0) + 1, `Time` = now()
//WHERE (`Id` = 1)

fsql.Update<Topic>(1)
    .Set(a => new Topic
    {
        Clicks = a.Clicks + 1,
        Time = DateTime.Now
    })
    .ExecuteAffrows();
//UPDATE `Topic` SET `Clicks` = ifnull(`Clicks`,0) + 1, `Time` = now()
//WHERE (`Id` = 1)

5、更新实体 SetSource

方法 1:(推荐)

只更新变化的属性,依赖 FreeSql.Repository

var repo = fsql.GetRepository<Topic>(); //可以从 IOC 容器中获取
var item = repo.Where(a => a.Id == 1).First();  //此时快照 item
item.Title = "newtitle";
repo.Update(item); //对比快照时的变化
//UPDATE `Topic` SET `Title` = @p_0
//WHERE (`Id` = 1)

方法 2:(原始)

//v1.5.0 忽略更新 null 值的属性
fsql.Update<Topic>()
    .SetSourceIgnore(item, col => col == null)
    .ExecuteAffrows();
var item = new Topic { Id = 1, Title = "newtitle" };
fsql.Update<Topic>()
    .SetSource(item)
    .ExecuteAffrows();
//UPDATE `Topic` SET `Clicks` = @p_0, `Title` = @p_1, `CreateTime` = @p_2
//WHERE (`Id` = 1)

fsql.Update<Topic>()
    .SetSource(item)
    .UpdateColumns(a => new { a.Title, a.CreateTime })
    .ExecuteAffrows();
//UPDATE `Topic` SET `Title` = @p_0, `CreateTime` = @p_1
//WHERE (`Id` = 1)

fsql.Update<Topic>()
    .SetSource(item)
    .IgnoreColumns(a => new { a.Clicks, a.CreateTime })
    .ExecuteAffrows();
//UPDATE `Topic` SET `Title` = @p_0
//WHERE (`Id` = 1)

var items = new List<Topic>();
for (var a = 0; a < 10; a++) items.Add(new Topic { Id = a + 1, Title = $"newtitle{a}", Clicks = a * 100 });

fsql.Update<Topic>()
    .SetSource(items)
    .ExecuteAffrows();
//UPDATE `Topic` SET `Clicks` = CASE `Id` WHEN 1 THEN @p_0 WHEN 2 THEN @p_1 WHEN 3 THEN @p_2 WHEN 4 THEN @p_3 WHEN 5 THEN @p_4 WHEN 6 THEN @p_5 WHEN 7 THEN @p_6 WHEN 8 THEN @p_7 WHEN 9 THEN @p_8 WHEN 10 THEN @p_9 END,
//`Title` = CASE `Id` WHEN 1 THEN @p_10 WHEN 2 THEN @p_11 WHEN 3 THEN @p_12 WHEN 4 THEN @p_13 WHEN 5 THEN @p_14 WHEN 6 THEN @p_15 WHEN 7 THEN @p_16 WHEN 8 THEN @p_17 WHEN 9 THEN @p_18 WHEN 10 THEN @p_19 END,
//`CreateTime` = CASE `Id` WHEN 1 THEN @p_20 WHEN 2 THEN @p_21 WHEN 3 THEN @p_22 WHEN 4 THEN @p_23 WHEN 5 THEN @p_24 WHEN 6 THEN @p_25 WHEN 7 THEN @p_26 WHEN 8 THEN @p_27 WHEN 9 THEN @p_28 WHEN 10 THEN @p_29 END
//WHERE (`Id` IN (1,2,3,4,5,6,7,8,9,10))

fsql.Update<Topic>()
    .SetSource(items)
    .IgnoreColumns(a => new { a.Clicks, a.CreateTime })
    .ExecuteAffrows();
//UPDATE `Topic` SET `Title` = CASE `Id` WHEN 1 THEN @p_0 WHEN 2 THEN @p_1 WHEN 3 THEN @p_2 WHEN 4 THEN @p_3 WHEN 5 THEN @p_4 WHEN 6 THEN @p_5 WHEN 7 THEN @p_6 WHEN 8 THEN @p_7 WHEN 9 THEN @p_8 WHEN 10 THEN @p_9 END
//WHERE (`Id` IN (1,2,3,4,5,6,7,8,9,10))

fsql.Update<Topic>()
    .SetSource(items)
    .Set(a => a.CreateTime, DateTime.Now)
    .ExecuteAffrows();
//UPDATE `Topic` SET `CreateTime` = @p_0
//WHERE (`Id` IN (1,2,3,4,5,6,7,8,9,10))

指定 Set 列更新后,SetSource 将失效

SetSource 默认依赖实体 IsPrimary 特性,临时主键可使用 SetSource(items, a => a.Code)

Oracle CASE when N'' 字符集不匹配,

  • 原因:实体主键 Column DbType 与表类型不一致造成,
  • 解决:[Column(DbType = "varchar2", StingLength = 255)]

6、更新 SetDto

fsql.Update<T>()
    .SetDto(new { title = "xxx", clicks = 2 })
    .Where(a => a.Id == 1)
    .ExecuteAffrows();
//UPDATE `Topic` SET `Title` = @p_0, `Clicks` = @p_1 WHERE (Id = 1)

fsql.Update<T>()
    .SetDto(new Dictionary<string, object> { ["title"] = "xxx", ["clicks"] = 2 })
    .Where(a => a.Id == 1)
    .ExecuteAffrows();

7、Set/SetSource/SetDto 区别

他们三个是平级功能,分别对应:

  • Set/SetRaw 在知道实体的时候使用,对应 update t set x = x

  • SetSource 更新整个实体,可以配合 UpdateColumnsIgnoreColumns 指定或忽略字段

  • SetDtoSet 的批量操作

8、字典更新

var dic = new Dictionary<string, object>();
dic.Add("id", 1);
dic.Add("name", "xxxx");

fsql.UpdateDict(dic).AsTable("table1").WherePrimary("id").ExecuteAffrows();
//提示:List<Dictionary<string, object>> 为批量更新

9、乐观锁

更新整个实体数据时,在并发情况下极容易造成旧数据将新的记录更新。

乐观锁的原理,是利用实体某字段,如:long version,更新前先查询数据,此时 version1,更新时产生的 SQL 会附加 where version = 1,当修改失败时(即 Affrows == 0)抛出异常(DbUpdateVersionException)。

每个实体只支持一个乐观锁属性,在属性前标记特性:[Column(IsVersion = true)] 即可。

适用 SetSource 更新,每次更新 version 的值都会增加 1

10、悲观锁

var user = fsql.Select<User>()
    .ForUpdate(true)
    .Where(a => a.Id == 1)
    .ToOne();
//SELECT ... FROM User a for update nowait

ForUpdate 在 Oracle/PostgreSQL/MySql 是通用的写法,我们对 SqlServer 做了特别适配,执行的 SQL 语句大致如下:

SELECT ... FROM [User] a With(UpdLock, RowLock, NoWait)

11、ISelect.ToUpdate 高级更新

IUpdate 默认不支持导航对象,多表关联等。ISelect.ToUpdate 可将查询转为 IUpdate,以便使用导航对象更新数据,如下:

fsql.Select<T1>().Where(a => a.Options.xxx == 1)
    .ToUpdate()
    .Set(a => a.Title, "111")
    .ExecuteAffrows();

注意:此方法不是将数据查询到内存再更新,上面的代码产生如下 SQL 执行:

UPDATE `T1` SET Title = '111' WHERE id in (select a.id from T1 a left join Options b on b.t1id = a.id where b.xxx = 1)

复杂更新使用该方案的好处:

  • 更新前可预览测试数据,防止错误更新操作;
  • 支持复杂的更新操作,例如:ISelect 上使用 Limit(10) 更新附合条件的前 10 条记录;

12、联表更新 UpdateJoin

v3.2.692+(高风险操作,高风险操作,高风险操作,请谨慎谨慎谨慎使用,测试并核对 ToSql 返回的内容)

fsql.Update<T1>()
    .Join<T2>((a, b) => a.id == b.groupid)
    .Set((a, b) => a.bname == b.name) //其他表字段
    .Set((a, b) => a.bcode == b.id + a.code)
    .Set(a => a.flag, 1) //固定值
    .Where((a, b) => a.id > 0 && b.id > 0)
    .ExecuteAffrows();

不同数据库产生的 SQL 不一样,以 MySql 为例:

UPDATE `T1` a
INNER JOIN `T2` b ON (a.`id` = b.`groupid`)
SET a.`bname` = b.`name`, a.`bcode` = concat(b.`id`, a.`code`), a.`flag` = 1
WHERE a.`id` > 0 AND b.`id` > 0

更复杂的联表更新:

var query = fsql.Select<T2, T3>()
    .InnerJoin(...)
    .Where(...)
    .WithTempQuery((a, b) => new { item1 = a, item2 = b });

fsql.Update<T1>()
    .Join(query, (a, b) => a.id == b.item1.groupid)
    .Set((a, b) => a.bcode == b.item2.xcode)
    .ExecuteAffrows();
UPDATE `T1` a
INNER JOIN (
  SELECT ...
  FROM `t2` a
  INNER JOIN ...
  Where ...
) b ON (a.`id` = b.`groupid`)
SET a.`bcode` = b.`xcode`

13、高性能 BulkCopy

| 程序包 | 扩展方法 | 说明 (v3.2.693) | |

]]>
更新日志 https://freesql.net/reference/change-log.html https://freesql.net/reference/change-log.html 更新日志 更新日志 大约每月一次版本号,暂时以修复 bug 为主 v3.5.307 feat: add support for including ignored columns in MapEntityValue method. fix: Override Firebird&apos;s bitwise operators. v3.5.306 修复 ClickHouse... Fri, 16 Oct 2020 05:57:03 GMT 大约每月一次版本号,暂时以修复 bug 为主

v3.5.307

  • feat: add support for including ignored columns in MapEntityValue method.
  • fix: Override Firebird's bitwise operators.

v3.5.306

  • 修复 ClickHouse 读取到错误列导致插入失败问题;#2194
  • 补充 SqlExt.DateDiff 对其他数据库的支持;

v3.5.305

  • 增加 ISelect.WithoutJoin 多表查询时排除某个表;
  • 增加 CreateUnitOfWork 从外部事务创建;
  • 增加 TDengine 对Decimal类型支持;
  • 修复 UnitlOfWorkManager 绑定 DbContext 事务问题;
  • 修复 Select0Provider.Finalize 可能存在的问题;#2183
  • 修复 MySql InsertInto Enum MapType(int) 赋值未能解析为 int ;
  • 修复 Clickhouse 参数化处理 Enum 的问题;
  • 补充 ObjectPool Async CancellationToken 参数;#2177

v3.5.304

  • 修复 Contains 解决可能在 net10 出现的问题;#2144
  • 增加 Clickhouse DateOnly 支持;
  • 增加 GBase ExecuteInserted/ExecuteUpdated/ExecuteDeleted 方法;
  • 增加 GBase CodeFirst 可新增列;
  • 修复 南大通用使用 DateTime.Date 操作无法比较的问题;
  • 修复 南大通用对 text 写入报错的问题;
  • 修复 pgsql 驱动使用 UseMergeInto 语法在 InsertOrUpdate PgCopy 生成了错误的 SQL;
  • 修复 opengauss 使用 pg 驱动时 merge 语法错误和增加二进制字段值显式类型转换;
  • 修复 DataType 在 Custom 项目下的设置问题;#2153

v3.5.302

  • 升级到 .NET 10.0;
  • 添加 IUpdate.SetByPropertyName 动态指定属性更新;
  • 修复 MySql Enum MapType(int) 表达式值解析 bug;
  • 修复 Firebird 批量插入异常问题(BLOB SUB_TYPE BINARY 需要参数化命令);#2139
  • 修复 KingbaseES bool 类型解析为 true/false;
  • 优化 KingbaseES V9 支持和读取链接字符串中的 SearchPath 来确定架构模式;
  • 优化 强制索引(WithIndex)同时支持 MySql/Oracle/SqlServer/Sqlite;#2130
  • 优化 PG临时主键插入或更新时自增主键不插入;#2133
  • 修复 ClickHouseBulkCopy 没传 ColumnNames 与未调用 InitAsync 可能导致 Column names not initialized. Call InitAsync once to load column data;
  • 补充 IUnitOfWorkManager;#2127
  • 升级 DuckDB to v1.4.1;#2128
  • 升级 kdbndp.dll to V9.3.7.1030

v3.5.215

  • 修复 Clickhouse 批量新增因未指定 Columns 导致报错;
  • 修复 ClickHouse .LimitBy;#2114
  • 修复 AOP 优先级最高 Aop.ConfigEntity 无法获取 ModifyIndexResult;
  • 修复 ZeroDbContext WhereDyanmicFilter DataRange bug;
  • 修复 ZeroDbContext 多表 Schema 问题;
  • 修复 OracleDBFirst 生成实体 Schema 清空导致的 -match 无法匹配表;
  • 修复 子查询In ToList.Contains 开启参数化后可能丢失参数的 bug;UseGenerateCommandParameterWithLambda
  • 优化 WithLock(NoLock) 同时兼容 SqlServer、达梦;#2058
  • 优化 SqliteDbFirst 输入参数处理;
  • 优化 QuestDb IHttpClientFactory、IServiceCollection 相关逻辑;
  • 优化 QuestDb Insert Update 相关逻辑;
  • 调整 ClickHouse 驱动 ClickHouse.Client 改成 ClickHouse.Driver;

v3.5.213

  • 调整 ManyToMany 不一定非要双边设置;
  • 修复 pgsql WithTempQuery + ToList 对于 bool 类型处理导致的性能问题;#2093
  • 修复 非公开无参的实体无法获取默认值问题;#2085
  • 修复 ZeroDbEntity WhereDynamic 多表匹配问题;
  • 修复 Aop.AuditValue 值变化状态的判断问题;#2101
  • 修复 SqlServer DbFirst 大小写判断问题;
  • 修复 MySql 生成 DDL 会报错问题;
  • 补充 MySql DbFirst Index Name 值;
  • 补充 pgsql FreeSql.Generator 默认值 uuid_generate_v4() 生成;#2088

v3.5.212

  • 修复 Oracle InsertOrUpdate Bug;#2047

v3.5.211

  • 修复 ToChunkAsyncEnumerable 少读一条数据的 bug;#1982 #2056 #2016 #2015
  • 修复 System.MemoryExtensions.Contains 解析问题;#1993
  • 修复 Group 查询不支持 aot 问题;#2077
  • 修复 WithTempQuery + AsTreeCte 生成SQL错误;#2080
  • 修复 Oracle DbFirst.GetTableByName() 没有返回字段信息;#2074
  • 修复 GBase DbFirst 字典配置问题;#2078
  • 修复 GBase 时间类型精度问题;#2079
  • 修复 Sqlite CodeFirst 特殊表名问题;
  • 完善 ClickHouse/QuestDB/TDengine 支持 enum 映射;
  • 增加 SqlExt.Ntile ntile(5) over (order by ...);

v3.5.210

  • 增强 全局过滤器支持查询、更新、删除指定生效;#2061
  • 修复 SqliteCore AOT Expression 隐式将 decimal 转成 int 问题;#2065
  • 修复 WithMemory 首行为 NULL 值的情况;#2047
  • 修复 ExecuteSqlBulkCopy() 对于表名有空格的情况处理异常;#2055
  • 修复 pgsql + JsonMap 无法解析 poco 表达式树的问题;#2060
  • 优化 pgsql + JsonMap 自动映射为 jsonb;#2060

v3.5.209

  • 修复 AsTreeCte + InsertInto 语法错误问题;#2045

v3.5.208

  • 修复 MySql Enum MapType(int) .Contains 解析问题;
  • 修复 MySql JsonMap Enum 统一解析为 int;#2040

v3.5.207

  • 修复 Lambda .Contains 通配符解析为非 Like 问题;

v3.5.206

  • 增加 ISelect.ToChunkAsyncEnumerable 异步流功能;
  • 增加 FreeSql.Extensions.EFModel 从 efcore modelBuilder FluentApi 同步到 IFreeSql;
  • 增加 Firebird ForUpdate skiplocked 查询参数;
  • 修复 Firebird Inserted/Deleted 与 Repository 级联冲突 bug;#2023
  • 修复 Duckdb CodeFirst Create Index异常;#2027
  • 修复 ToList 子查询 bug;
  • 修复 InsertOrUpdate 更新时触发两次 AuditValue;#2020
  • 调整 Repository 状态管理支持 Ignore 属性(非副本);
  • 完善 低代码扩展 FreeSql.Extensions.ZeroEntity;

v3.5.203

  • 增加 ISelect.Clone() 克隆查询对象;
  • 增加 ZeroDbContext.LoadSchemaFromDatabase 方法从数据库中加载描述对象;
  • 修复 MySqlCodeFirst Primary 迁移逻辑;#2005
  • 修复 .In() 非表达式解析场景的 null 处理;
  • 忧化 Oracle DbFirst 表特别多的时候生成速度;
  • 忧化 GroupBySelf + ToList<Dto> 不自动映射;
  • 调整 SqlExt 聚合函数 Count 统一返回 int;

v3.5.202

  • 补充 Utils.IsStrict = false 可配置支持中间表有 Id 主键;(解决v3.5.107版本调整影响)

v3.5.201

  • 修复 .Contains in 表达式函数解析在ar-AE区域问题;#2002
  • 忧化 IDbFirst Columns 序列与数据库相同;
  • 补充 UnitOfWorkManager 支持 DbContext 托管工作单元;

v3.5.109

  • 忧化 GroupBySelf.Where 自动转 having;
  • 忧化 FreeSql.Provider.PostgreSQL IPostgreSQLProviderOptions 多态参数接口,设置 InsertOrUpdate 采用 MERGE INTO;

在 FreeSqlBuilder().Build() 之后设置如下:

(fsql as IPostgreSQLProviderOptions).UseMergeInto = true;

v3.5.108

  • 修复 DbSet/Repository CompareState 报错的问题;#1994
  • 增加 ISelect.GroupBySelf 返回 ISelect;

v3.5.107

  • 修复 ISelectGroupingAggregate 自定义解析方法报错的问题;
  • 修复 Custom/Odbc char CodeFirst 映射问题;
  • 修复 AggregateRoot CompareEntityValue 错误;
  • 优化 AggregateRoot MapEntityValue 支持将实体映射到字典;
  • 优化 AggregateRoot 不对比 CanUpdate = false 的字段 ;
  • 优化 多对多中间表主键配置错误的友好提醒;

v3.5.106

  • 增加 sqlite 批量插入方式 ExecuteSqliteBulkInsert;#1975
  • 修复 Oracle CodeFirst nclob cast 转换问题;
  • 修复 JsonMap Deserialize surpport JsonSerializerSettings;
  • 修复 Kingbase 判断 v8r3 版本号问题;#1976
  • 修复 TDengine DateTime 默认值读取异常;#1980 #1977

v3.5.102-3.5.105

  • 增加 ISelect.Join 与 InnerJoin 方式行为一致;
  • 增加 ToChunkAsync 异步 Api;#1952
  • 增加 SqlExt.AggregateCount/AggregateSum 等方法,区分 SqlExt.Count/SqlExt.Sum 开窗方法;
  • 修复 ExpressionTree 无法将 double -> 转成 int 的问题;
  • 修复 PgCopy DateTime 映射 date 失败的情况;
  • 修复 Npgsql postgis 条件编译问题;#1940
  • 修复 MySql.Data MySqlDateTime 读取为 null 的情况;#1960 #1948
  • 修复 Oracle CodeFirst clob/blob cast 转换问题;
  • 修复 KingBase .net8.0 等目标框架 dll 引用问题;
  • 修复 Firebird 表注释迁移问题(列注释正常);#1913
  • 修复 Firebird 批量插入类型转换问题;#1923
  • 修复 GBase IsNull 日期解析问题;#1953
  • 修复 DmPrivider 连接达梦主从集群会出错问题;#1955
  • 修复 达梦数据库上时间相减永远输出为0的问题;#1973
  • 修复 CodeFirst 修改表名时重建索引的问题;
  • 修复 GroupBy + WithTempQuery + 子查询参数化问题;#1965
  • 修复 DateOnly/TimeOnly AOT 裁剪默认报错的问题;
  • 修复 DateTime 相减时,出现 IIF 无法转换表达式的错误
  • 修复 AdoConnectionPool 默认不生效的问题;
  • 修复 CreateInstance 发生歧义的问题;#1962
  • 修复 In + MapType 无效的问题;
  • 修复 In + 子查询 别名问题;#1967
  • 优化 ToList 针对 bool 自动转成 CASE When;
  • 优化 SqlServer WithIndex 使用习惯;
  • 忧化 DbFirst Oracle 自动批量插入问题;(2020年方法忘记生效)
  • 忧化 DateTime.Subtract(date).TotalDays 表达式解析对应 datediff(day, date1, date2);
  • 优化 表达式解析 dArray.Select(p => p.Key).Contains(a.Id);
  • 优化 非 join 的多表查询的 SQL 格式;

v3.5.101

  • 更新 到 .NET9.0;
  • 增加 DuckDB 数据库支持; https://freesql.net/guide/freesql-provider-duckdb.html
  • 增加 TDengine 数据库支持(感谢Daily贡献); https://freesql.net/guide/freesql-provider-tdengine.html
  • 增加 KingBaseES 支持数组等类型(参考 PostgreSQL);
  • 增加 Pgsql/MySql DateOnly/TimeOnly 映射;#1868 #1763 #939 #991
  • 增加 OracleUs7ascii 写入处理特性;
  • 增加 JsonMap Poco Lambda 表达式成员解析;
  • 增加 FromQuery 参数由5个加到16个;
  • 优化 FromQuery/UnionAll 在子查中继续嵌套;
  • 优化 ISelect<object>.WithMemory 对匿名类型的支持;
  • 优化 Lambda Dto 指定 .Any() 自动转成 Case when;
  • 优化 ExpressionCall + DynamicInvoke 的解析;
  • 优化 LIKE 表达式解析 #1870;
  • 优化 Lambda Min/Max/First 针对 DateTime 类型不自动处理 IsNull 逻辑,因为各数据库没有统一默认值;
  • 优化 DateTime.Subtract(date).TotalSecods 等 lambda 表达式解析,对应 datediff;
  • 优化 DbSet/Repository 删除多主键分批300,防止 SQL AND OR 过长问题;
  • 优化 DM/KDB/ST 默认使用 ado.net 连接池;
  • 扩展 TypeHandler override FluentApi 设置;
  • 扩展 SqlExt.DateDiff 时间差值计算;
  • 扩展 a.Id.In(..) 自定义函数解析,或 new[]{ 1,2,3 }.Contains(a.Id) 相同;
  • 修复 Repository + AuditValue + Attach 问题;#1931 #1746
  • 修复 GBase serial8/bigserial 自增回填的问题;#1919
  • 修复 Xugu DataType 对应问题;#1934 #1933
  • 修复 Firebird 批量插入 varchar 追加空格的问题;#1923
  • 修复 Guid.NewGuid().ToString().Replace("-", "") 3.2.810 之后版本的解析问题;
  • 修复 UnionAll + OrderBy 未处理嵌套的问题;
  • 移除 TimeSpan 过度 Lambda 解析;
  • 移除 OdbcDameng/OdbcKingbaseES;
  • 移除 Resources 转换成 static class 静态类;#1917
  • 移除 fsql.GetGuidRepository 改用 fsql.GetRepository<T, Guid>();
  • 修复 DateOnly/TimeOnly 映射问题;#1868 #1855 #1763 #939 #991

v3.2.833

  • 添加 Aop.ConfigEntity 动态设置表名功能;#364 #1835 #1729 #1542 #1248 #1247 #407 #387
  • 增加 UpdateDict/DeleteDict 方法 Where(sql);#1840
  • 优化 GroupBy 聚合函数 Count(bool) 解析成 sum(case when);#1841

v3.2.832

  • 修复 */表达式解析问题;#1836
  • 修复 Extesions.Linq COUNT Distinct 生成sql错误;#1838
  • 优化 Ado读写分离存储过程默认使用从库;#1833
  • 增加 ZeroEntity 自定义异常及相关;

v3.2.830

  • 增加 ClickHouse 表分区支持;(感谢d4ilys贡献)
  • 修复 .Contains('b%') 部分数据库函数参数位置错误;#1826
  • 修复 ClickHouse 批量更新 DateTime 问题;
  • 修复 Cilckhouse CodeFirst主键问题;
  • 调整 命名空间 FreeSql.Provider.ClickHouse.Attributes -> FreeSql.DataAnnotations;#1815

v3.2.825

  • 修复 开发环境分层编译,可能导致XML注释文件被锁报错的问题;
  • 修复 聚合根仓储级联插入 OnToMany 雪花主键只插入一条记录;

v3.2.822

  • 增加 vb.net Lambda 解析 AddChecked/SubtractChecked/MultiplyChecked;
  • 修复 时枚举转 int 再转字符串报错;#1781

v3.2.821

  • 修复 clickhouse.client 7.2.2 批量插入失败问题;#1776
  • 修复 IInsertOrUpdate.ExecuteMySqlBulkCopyAsync 异步方法会出现空引用的异常;#1762
  • 修复 DynamicCompileBuilder Extend 方法设置基类后失效问题;#1774

v3.2.820

  • 增加 DynamicEntity TypeBuilder 只读属性;
  • 修复 .NET9 TimeSpan.FromSeconds Reflect bug;#1748
  • 修复 IBaseRepository.IncludePropertyName 可能报错的问题;#1740
  • 修复 弱类型 IBaseRepository<object> 级联操作问题;#1740
  • 修复 Where(HzyTuple) 参数为 null 时的错误;#1744
  • 修复 IUpdate.SetSource SqlServer 批量更新 Cast 问题;#1755
  • 修复 SqlServer 导航属性 .Any 布尔子条件解析问题;
  • 优化 DbSet/Repository Attach 与 CanUpdate AuditValue 状态不同步问题;#1746
  • 优化 QuestDb BulkCopy 重命名为:ExecuteQuestDbBulkCopy;#1758
  • 优化 Clickhouse BulkCopy 重命名为:ExecuteClickHouseBulkCopy;#1758

v3.2.815

  • 修复 MySqlEnum CHAR/BYTE 替换后前面多个空格的问题;#1737
  • 修复 Clickhouse ToInt32/ToInt64 解析错误;
  • 修复 WithMemory 对 InsertValueSql 属性无效的问题;
  • 修复 ZeroDbContext GroupBy BUG;
  • 修复 聚合根仓储 InsertOrUpdate OneToMany 只插入一条记录的 bug;
  • 优化 TypeHandlers 支持 Enum 枚举映射规则;#1634

v3.2.812

  • 增加 DbContextOptions.AuditValue 基于 Ioc Scoped 审计值;
  • 优化 ManyToMany 导航属性约定命名匹配嵌套类;
  • 修复 MySql AsTreeCte pathSelector 别名问题;
  • 修复 ClickHouse UpdateDict 报错问题;#1712
  • 修复 MsAccess 日期问题;#1724 #1725

v3.2.810

  • 修复 GroupBy + WithTempQuery 别名问题;
  • 修复 DbContext/Repository InsertOrUpdate 无变化时触发 Insert 逻辑报错;#1700
  • 修复 ClickHouse 数组类型 hasAny 与 In 解析冲突问题;#1699
  • 补充 WithMemory null/Empty 参数判断;
  • 优化 表达式树解析;

v3.2.808

  • 增加 低代码扩展包 FreeSql.Extensions.ZeroEntity
  • 增加 Clickhouse Bool/Array 类型适配;(感谢 d4ilys)#1568
  • 增加 Clickhouse BulkCopy;
  • 增加 IBaseRepository.AsTable 重载方法支持多表表名设置;
  • 增加 IAsTable 自动分表支持不规则时间间隔;
  • 增加 IAsTable.SetTableName 自动分表设置分表名;
  • 增加 IAsTable.SetDefaultAllTables 自动分表拦截未使用 where 条件的场景;
  • 增加 IUpdate.ExecuteUpdated 指定字段返回;#1681
  • 增加 IUpdate.SetDtoIgnore 忽略Dto更新方法;#1688
  • 增加 ICodeFirst.SyncStructure(TableInfo) 重载方法迁移表结构;
  • 增加 IUpdate/IDelete WhereDynamicFilter API;#1521
  • 增加 WithSql(..).WhereDynamicFilter 的支持;
  • 增加 pgsql 扩展方法 ISelect.DistinctOn;#1680
  • 修复 Column DbType 与 StringLength 多次替换问题;
  • 修复 Where(a => a.bool) 当使用了 MapType(int) 时出现 bug;
  • 修复 SqlServer 批量更新 bool? null 值生成多余的 case when SQL;
  • 修复 Xugu 批量插入/更新 SQL 报错;
  • 修复 OledbOracle 参数化处理 bug;
  • 修复 多表子查询参数化共享问题;
  • 修复 嵌套查询 DTO 映射可能触发循环引用 bug;
  • 补充 达梦 IDbFirst datetime 处理;
  • 补充 FluentApi AsTable 方法;
  • 优化 自动分表 join 分表(自动贴合);
  • 优化 UnionAll + WithTempQuery 嵌套的 SQL;
  • 优化 Update 无字段时不做任何处理;
  • 优化 IUpdate 未使用 .Set 时忽略执行;#1694
  • 优化 表达式解析变量或常量 ToString;

v3.2.805

  • 更新 支持到 .net8.0,FreeSql.DbContext 放弃 .netcoreapp2.1 依赖注入;
  • 调整 DataType.SqlServer/MySql/PostgreSQL 默认使用 Ado 连接池;
  • 增加 InsertDict ExecuteIdentity 重载指定自增列;
  • 修复 Expression 导致 AOT 运行失败的问题;
  • 修复 MySql 8.0 索引 DESC 的判断;#1664
  • 修复 Xugu Insert + AsTable + ExecuteIdentity bug;
  • 修复 虚谷日期格式化函数映射;
  • 优化 RereadSql 支持表的其他字段使用;#1655

v3.2.802

  • 增加 Aop.AuditDataReader 参数属性 PropertyInfo,实现自定义拦截;
  • 修复 v3.2.801 AsTreeCte cte_path 别名问题;
  • 修复 UpdateJoin SetIf 逻辑判断问题;
  • 修复 UpdateJoin IsVersion 字段的别名问题;
  • 修复 RawJoin 可能生成错误的 SQL;
  • 修复 Repository.InsertOrUpdate 更新未变化时的 SQL 错误;#1630
  • 修复 IncludeMany 变异 Where + ToList dto 无法级联;
  • 修复 Ado.net 扩展方法 SqliteCore 支持;#1638
  • 修复 Xugu 有关 bug;
  • 优化 TypeHandlers 支持 DateTime 映射规则;#1634
  • 优化 IUpdateJoin AsTable 同时对多表名设置;
  • 优化 多实例情况下 TableInfo 集合缓存;

v3.2.801

  • 增加 FreeSql.Internal.Utils.TypeHandlers 自定义类型映射;
  • 增加 IInsertOrUpdate BatchOptions 选项;
  • 修复 v3.2.700 oracle/达梦子查询 BUG;
  • 修复 FreeSql.Geneartor 无法生成人大金仓数据库问题;
  • 修复 ClickHouse CodeFirst 多主键问题;
  • 修复 QuestDB BulkCopy 自定义表名无效的问题;
  • 修复 QuestDb And Or 优先级解析问题;
  • 修复 DynamicEntity 特性构造函数实例化问题;
  • 修复 IInsertOrUpdate BulkCopy 临时表名的问题;#1603
  • 修复 IUpdateJoin + 全局过滤器 + 从表不生效问题;#1612
  • 修复 JsonMap 进行 null 或非 null 查询;#1613
  • 修复 GroupBy + WithTempQuery + .Key.xx + 特性名与实体不同 + 三元表达式解析问题;
  • 修复 MySql AsTreeCte cte_path 可能长度过短问题;
  • 修复 MySql + InsertOrUpdateDict + IfExistsDoNothing 错误;#1601
  • 优化 MySql/Sqlite InsertOrUpdate + IfExistsDoNothing 简化 SQL insert ignore into;#1601
  • 优化 WithLock/WithIndex 非 SqlServer 时忽略;
  • 优化 MySql ServerTime = DateTimeKind.Utc 不支持 utc_timestamp 默认值问题;#1604
  • 优化 ObjectPool 恢复可用的时机;

v3.2.700 (oracle/达梦子查询有 BUG)

  • 增加 GlobalFilter.Apply before 参数将过滤条件插入 where 之前;#1566 #1492
  • 增加 多表 As 设置别名的方法 fsql.Select<T1, T2>().As("t1", "t2");
  • 优化 WithTempQuery + SqlServer 未指定 OrderBy 的分页;
  • 优化 GetPropertiesDictIgnoreCase 不返回没有 get 的属性;
  • 修复 QuestDB Guid 类型的问题;
  • 修复 BaseEntity 子查询 .Where 问题;#1586

v3.2.698

  • 增加 FreeSqlBuilder UseAdoConnectionPool 更换连接池方案;#1524 #1343 #1283 #755
  • 增加 InsertOrUpdateDict WhereIdentityPrimary 方法;
  • 增加 pgsql 索引 B_Tree, Hash, GiST, GIN, SP_GiST, BRIN;
  • 修复 pgsql jsonb poco 方案联表查询问题;#1549 #1071
  • 修复 pgsql Geo 函数自定义解析深度问题;#1422
  • 修复 PgCopy 处理 jsonb 类型的问题;#1532
  • 修复 Enum.ToString() 即使 MapType=int 也应解析成 string;#1252 #806
  • 修复 repository/dbset AddRange AuditValue IEnumable 失败的 bug;#1545
  • 修复 达梦某情况连接串解析 user id 问题;
  • 修复 QuestDB 表达式解析 DateTime.UtcNow bug;
  • 修复 QuestDb BulkCopy在特定情况下无法解析表名问题;
  • 优化 QuestDB Guid 映射;
  • 优化 QuestDB 类型映射,表达式函数解析,新增 QuestFunc 函数解析;
  • 优化 uow + ISelect.ToUpdate 事务传播;#1562
  • 调整 QuestDB SampleUnits 命名,去掉 s;

v3.2.697

  • 增加 IInsertOrUpdate.UpdateSet 指定更新;
  • 修复 3.2.696 自动分表 bug;#1527 e79860164d5d12e4f7df747f0e29503dff301cb7

v3.2.696

  • 修复 <object> + AsType + (a as BaseEntity) 解析问题;#1427
  • 修复 IUpdateJoin 联表更新表达式赋值解析报错;
  • 修复 MySqlProvider .NetFramework 下可能报初始化类型错误;
  • 修复 自动分表 IUpdate/IDelete AsTable 不生效的问题;
  • 修复 FromQuery + WithTempQuery 嵌套查询多层后别名问题;#1510
  • 修复 WithTempQuery + Oracle 分页问题;#1519
  • 修复 Oracle 插入 LONG RAW 类型的问题;
  • 修复 BulkCopyUpdate 临时表名大小写问题;
  • 优化 IUpdate 批量 case when 同值的 SQL 生成;#1393

v3.2.695

  • 增加 虚谷数据库 FreeSql.Provider.Xugu;
  • 增加 IInsert IgnoreInsertValueSql 方法临时忽略 InsertValueSql 设置;
  • 增加 部分 LocalExecuteScalar Aop.CommandAfter 事件;#1481
  • 增加 QuestDB SampleBy 对准日历参数;
  • 增加 动态操作创建实体API,fsql.CodeFirst.DynamicEntity;(感谢 Daily 贡献)
  • 修复 OracleOledb 引起的读取类型问题;

v3.2.693

  • 添加 IInsertOrUpdate 高性能插入或更新 SqlServer/MySql/Oracle/Pgsql/达梦/人大金仓;
  • 增加 IUpdate 高性能批量更新 Oracle/达梦/人大金仓;
  • 增加 TableInfo.SetAsTable 自动分表的设置;
  • 修复 SqlServer 自增 BulkUpdate bug;#1476
  • 修复 表达式树解析 string.Concat 多于3个时的 bug;
  • 修复 UpdateJoin null 错误;
  • 优化 UpdateJoin Set 固定值解析;
  • 优化 .Select<object>().WithSql(sql).ToList() 体验;

v3.2.692

  • 增加 IUpdate.Join 联表更新功能;(高风险操作,请谨慎使用)
  • 修复 InsertOrUpdateDict Oracle byte[] 长度判断问题;#1462
  • 修复 InsertDict 等字典操作在 SqlServer 下的问题;
  • 修复 ISelect<T> FromQuery 多表重载可能出现 null 报错;

v3.2.691

  • 增加 ISelect<T> FromQuery 重载多表嵌套;
  • 增加 实体备注从 Description/Display/DisplayName 等特性兼容读取;
  • 增加 ISelectGrouping First 方法;
  • 增加 IInsertOrUpdate SetSource 重载方法;
  • 优化 WithTempQuery + ToList 使用子查询;
  • 优化 IInsertOrUpdate SetSource tempPrimary 自增的忽略插入;
  • 修复 Clickhouse 自动分表 where 条件分析问题;
  • 修复 DBFirst 延迟加载 ManyToOne 返回总是NULL;#1451
  • 修复 QuestDB CodeFirst AutoSubtableAttribute/ColumnAttribute判断bug

v3.2.690

  • 增加 [Navigate(xx, TempPrimary = xx)] 与非主键关联;(仅支持查询)
  • 修复 3.2.689/3.2.688 WithTempQuery DTO 映射查询遗留问题;
  • 优化 AsTable 自动分表,提供超始小时的设置;

v3.2.688

  • 增加 QuestDB 时序数据库支持;(感谢 Daily 贡献)
  • 修复 Array.Any OR 表达式树解析 bug;
  • 修复 GroupBy 之后 WithTempQuery 参数化时未传递的问题;
  • 修复 BulkUpdate 触发 Aop.AuditValue Insert 类型的问题;
  • 修复 Ado.ExecuteNonQuery 超时参数未生效的问题;#1441
  • 修复 Firebird CodeFirst 索引 DESC 问题;#1413
  • 优化 Firebird CodeFirst 迁移代码;
  • 优化 Firebird 表达式树位运算的适配解析;#1413
  • 优化 SqlServer timestamp 行版本的支持;#1412
  • 优化 pgsql15.0 部分类型兼容;#1436
  • 完善 Sqlite DbFirst 获取索引信息;#1425
  • 优化 适配达梦最新驱动;
  • 优化 IUpdate 组合更新方式;
  • 优化 IUpdate.Set(xx, null) 的情况;
  • 优化 ISelect<object> AsType 的父子类转换时的情况;#1427
  • 优化 支持动态操作 IncludeByPropertyName then.WhereDynamicFilter 操作;
  • 优化 WithTempQuery 场景的 DTO 映射查询;

v3.2.687

  • 修复 3.2.686 JsonMap bug;
  • 优化 参数化增加 ExpandoObject 支持;#1403
  • 优化 pgsql15.0 Version 版本号处理;
  • 优化 pgsql CodeFirst Syncsture CREATE INDEX code;

v3.2.686

  • 增加 FreeSqlBuilder UseQuoteSqlName 设置不使用 [] `` "" 限定符号;
  • 增加 Aop.CommandBefore 设置 .CommandText = null 可拦截某次执行;
  • 增加 IAdo.ExecuteNonQuery 重载方法(虚谷数据库);
  • 修复 UseSlaveWeight 异步下可能无效的问题;#1382
  • 修复 HzyTuple 嵌套解析问题;#1390
  • 修复 AsType 表达式解析问题;#1389
  • 修复 pgsql OnConflictDoUpdate 临时主键问题;#1393
  • 修复 ClickHouse 联表查询 GLOBAL left join、Ado.Query * 问题;#1383
  • 优化 ClickHouse like 为 positionCaseInsensitive #1386
  • 优化 JsonMap 对基础类型的无效判断;#1385
  • 优化 IUpdate 批量 case when 同值的 SQL 生成;#1393
  • 优化 IAdo.ConnectionString 使用 UseConnectionFactory 时值为 NULL 的问题;

v3.2.685

  • 增加 IUpdate<T> BulkCopy 批量更新扩展方法;(暂时支持 SqlServer/MySql/PostgreSQL)
  • 修复 ClickHouse CodeFirst 表结构同步问题;
  • 修复 ISelectGrouping ToSql AsProperty 别名无效问题;
  • 优化 UnitOfWork 与 ForUpdate 事务开启逻辑;
  • 优化 MySqlConnector MySqlDataTime 读取;
  • 优化 linq.dynamic Expression Invoke 解析;#1378

v3.2.684

  • 修复 Firebird CodeFirst.SyncStructure 自增主键错误;(受影响版本 3.2.666-3.2.683)
  • 修复 Firebird [Index("{tablename}_Name" 分表索引名未处理的 bug;#1346
  • 修复 InsertDict DBNull.Value 值无法插入的问题;#1336
  • 修复 fsql.InsertOrUpdate SetSource(sql) 无效的 bug;
  • 优化 string[].Any(..) 表达式树解析;ReplaceParameterVisitor
  • 优化 ISelect<T1, T2> 对象 .LeftJoin<T2> 表别名;#1348

v3.2.683

  • 修复 Repository/DbContext 同一实例执行报错后续无法再 CRUD;
  • 修复 UnitOfWorkManager + Repository.SaveMany 事务切换问题;
  • 修复 导航属性 PgArrayToMany 数组字段为空时的 bug;
  • 修复 导航属性 PgArrayToMany DTO 异步查询报错的 bug(同步方法正常);
  • 修复 SqlExt count/avg 开窗函数参数;#1321
  • 修复 Sqlite TimeSpan 类型映射错误;
  • 修复 Sqlite Attachs 多库索引 {tablename} 问题;
  • 修复 EndEdit 未触发 OnEntityChange;
  • 修复 fsql.InsertOrUpdate CanInsert=false,CanUpdate=true (mysql/pgsql)不生效的 bug;
  • 修复 聚合根仓储(实验) InsertOrUpdate 重复插入的 bug;
  • 修复 聚合根仓储(实验) Update 未更新自增状态管理的 bug;
  • 修复 SqlServer WithSql + WithLock bug;
  • 修复 ClickHouse Ado 参数化无效的问题;#1340
  • 修复 Odbc 自定义 N'' 无效;#1332
  • 修复 DbContext DbSet 属性初始并发时未触发 OnModelCreating;#1333
  • 修复 Utils.ReplaceSqlConstString bug;
  • 扩展 UseMessagePackMap 示例;
  • 优化 UpdateDict 支持 .IsVersion 乐观锁设置;
  • 优化 InsertDict DBNull.Value 值处理;#1336
  • 优化 表达式树解析 w => (w as TBase).Id;
  • 优化 导航属性集合 .Exists 效果与 .Any 相同;
  • 优化 MySql CodeFirst 判断表是否存在的查询语句性能;
  • 优化 SqlServer2005 TOP 1 查询;
  • 完善 SqlServer WithLock + WithIndex 扩展方法;

v3.2.682

  • 优化 人大金仓 KingbaseES 兼容 V8R3/V8R6;#931 #325

v3.2.681

  • 修复 3.2.666-3.2.680 子查询的特殊 bug;
  • 增加 fsql.Ado.QuerySingle(() => new {}) 扩展方法实现无表查询 SELECT now(), utc_timestamp()
  • 优化 min pool size 与 max pool size 连接串设置;
  • 优化 导航属性未配置关系先 Where 后 InnerJoin 的问题;

v3.2.680

  • 优化 WithTempQuery 多对象选择同别名问题; #1192
  • 修复 WithTempQuery + Column + GroupBy + Key 指定列名的问题;
  • 修复 GlobalFilter.ApplyIf + Repository.DataFilter.DisableAll 可能不生效的问题;
  • 修复 UpdateDict 不支持 SET NULL 的更新;#1257
  • 修复 ClickHuse DBFrist ExistsTable;
  • 增加 ClickHouse LimitBy/Sample 查询方法;
  • 增加 所有国产数据库支持 CustomMySql、CustomPostgreSQL、CustomOracle、CustomSqlServer 自定义适配;
  • 优化 DbSet/Repository 防止 Aop.AuditValue 触发两次;
  • 优化 表达式树解析为 NULL 的提示;
  • 优化 UseMappingPriority 与实体元数据逻辑;#1247
  • 优化 ServerTime 与 IUpdate.Set 指定更新的重复问题;#1251

v3.2.669

  • 修复 WithTempQuery + RereadSql 无别名的问题;
  • 修复 WithTempQuery + FromQuery + 子查询的 bug;
  • 修复 WithTempQuery + Column(Name 别名问题;
  • 优化 CheckAvailable retry 重试一次;

v3.2.666

  • 增加 WithTempQuery + FromQuery 嵌套查询功能; #1192
  • 增加 FreeSql.Provider.OracleOledb 解决 US7ASCII 中文乱码问题;
  • 增加 UnionALL 联合查询; #1106 #1104 #668 #478 #432 #213 #138
  • 增加 WithMemory 基于内存查询,对标 WithSql;
  • 增加 AuditValue ObjectAuditBreak 实现对象只触发一次审计事件;
  • 增加 IncludeByPropertyName 重载 then 参数;#1214
  • 增加 IInsertOrUpdate.SetSource(sql) 重载方法;
  • 增加 DynamicFilterCustom 增加支持 Expression 返回值;
  • 修复 UseGenerateCommandParameterWithLambda(true) 与 GroupBy 查询不生效 bug;
  • 修复 ToList 子查询开启参数化重复参数的 bug;#1205
  • 修复 string[] JsonMap bug; #653
  • 修复 ManyToMany 不会触发 AsTable 的 bug;
  • 修复 Clickhouse Insert AsTable 表名处理 bug;
  • 修复 ClickHouse 单条记录插入\t \n /失败的 bug;
  • 修复 子查询使用基类 + AsType 可能产生的 bug;#1215
  • 修复 SqlServer2005/2008 Skip 问题(未设置 Take 时);
  • 修复 MySql/SqlServer DbFirst 获取字段位置的问题;
  • 修复 DbContext/Repository Primary decimal 状态管理 key 精度处理 bug;
  • 修复 DbContext/Repository 无构造函数的实体对象不跟踪问题;
  • 修复 ToSql AsProperty 大小别名问题;
  • 优化 IncludeByPropertyName + AsType;
  • 优化 字典crud TableInfo 合并规则;#1180
  • 优化 IUpdate.Set 字符串累加本身为 NULL 的情况;#1209
  • 优化 WhereDynamicFilter DateRange 情况;
  • 优化 Oracle IN :ids 值传入 IList 时报错;
  • 优化 Dameng 单独适配 netcore3.1;#1094
  • 优化 PrevReheatConnectionPool 预热;
  • 优化 全局过滤器禁用时子查询传播问题;#1208
  • 优化 子查询别名为 a 的情况;#1201

v3.2.665

  • 增加 IsVersion string 字符串乐观锁;#1178
  • 增加 IUpdate.SetSource ignoreVersion 参数可实现忽略乐观锁;#1161
  • 增加 IInsertOrUpdate.SetSource(items, tempPrimarys) 指定临时主键参数;#1160
  • 增加 DbContext/Repository 审计日志 ChangeInfo 增加属性 EntityType;
  • 修复 SqlServer WithLock 子查询不生效的 bug;#1159
  • 修复 AsTreeCte + AsTable 无效的 bug;#1176
  • 修复 UseGenerateCommandParameterWithLambda(true) 问题;#1173 #900
  • 修复 SetSource 临时主键重载方法 + Column 设置 Name 后无效的 bug;
  • 修复 Dto 映射查询 Negate 表达式解析 bug;
  • 修复 pgsql OldName + XML 注释迁移代码顺序问题;
  • 优化 string Contains 模糊查找 % 的情况;

v3.2.664

  • 修复 UseGenerateCommandParameterWithLambda 子查询并发 bug;#1155 (重要)
  • 修复 pgsql Dto 映射使用常量 false 转换失败;
  • 修复 IIF 三元表达式树解析 bool HasValue 问题;
  • 修复 MySqlConnector BulkCopy 映射顺序问题;
  • 优化 XML 注释读取支持 interface;
  • support provider、Extensions Exceptions 多语言

v3.2.662

  • 调整 pgsql10 自增映射使用 GENERATED BY DEFAULT AS IDENTITY,低版本仍然使用 serial;
  • 增加 PgArrayToMany 专属导航属性;#1145
  • 增加 ObservableCollection 级联加载和保存;
  • 优化 FluentApi 继承关系可直接 ConfigEntity<BaseEntity> 生效;#1144
  • 修复 达梦 min pool size 预热数量匹配 bug;
  • 修复 v3.2.620 - v3.2.661 子查询 sum/min/max/avg 默认加 isnull 防止为 NULL 情况,日期类型处理错误 #1140 1b84a0069679c92ccaff9aa8c33023e4d34262cd
  • 修复 AsTable 子查询未传播的问题;#1103
  • 修复 IncludeByPropertyName fromFirstTable 判断错误;#278
  • 修复 GroupBy 特殊情况下 AsProperty 无效的 bug;#1141
  • 修复 MySql CodeFirst OldName + Comment 迁移问题;#1147
  • 修复 pgsql DbFirst 未正确获取 Position 值;#1154

v3.2.661

  • 增加 UseMappingPriority 指定映射优先级;#387 #69 #99
  • 增加 AuditValueEventArgs Object 参数;#1128
  • 修复 pgsql varchar(120) CodeFirst 迁移不修改长度;
  • 修复 ISelect.InsertInto 未执行自动迁移;
  • 修复 UseCommandParameterWithLambda IN 参数化判断 的逻辑 bug;#1137
  • 优化 连接池不可用、定时检查;
  • 优化 Limit + Sum/Avg/Max/Min 为嵌套查询;
  • 优化 GroupBy Page 未排序的查询;#1126

v3.2.651

  • 增加 DTO 映射非导航属性的子表查询 ToList,可直接返回集合;
  • 增加 Array.Any(x => x.id == a.Id && ..) 表达式树解析;#243
  • 增加 pgsql numeric -> BigInteger 映射;#1100
  • 增加 #1108 Exception 国际化;
  • 增加 DynamicFilterCustom 参数 object sender;#1113
  • 增加 Fluent API 以继承接口的形式配置实体;#937
  • 修复 Oracle AsTable 分表嵌套 SQL 拼错错误;#1098
  • 修复 AsTable ManyToMany IncludeMany 无效的问题;#1103
  • 修复 AsTable 分表 ToAggregate 无法得到汇总;#1115
  • 修复 Repository.DataFilter 对 GlobalFilter 控制无效的 bug;#1028 #846
  • 修复 IN 查询区分 varchar/nvarchar;
  • 修复 Oracle clob 参数化类型设置问题;#1116
  • 修复 MySql 子查询 Enum MapType(int) 表达式判断解析 bug;#1118
  • 优化 AsTable 自动分表 Where Equal 判断;#1104
  • 优化 子查询 sum/min/max/avg 默认加 isnull 防止为 NULL 情况;
  • 优化 EnableCascadeSave 级联保存执行逻辑,提升性能;
  • 优化 RawJoin 支持 FULL JOIN 等自定义联表映射;
  • 优化 IncludeMany 三级导航对象自动 _included;#1113

v3.2.100 - v3.2.640

  • 增加 InsertDict/UpdateDict/DeleteDict/InsertOrUpdateDict 针对字典的 CUD 方法;#481
  • 增加 UseSlaveWeight 读权重设置;#1046
  • 增加 [Table(AsTable = xx)] 自动分表特性,待完善;#1066
  • 增加 FreeSql.Provider.SqliteCore 支持 Sqlite 加密;
  • 增加 IList<T> IncludeByPropertyName 扩展方法,支持字符串参数;
  • 增加 DbSet/Repository DeleteCascadeByDatabase 级联删除(基于数据库)
  • 调整 DbSet/Repository EnableAddOrUpdateNavigateList 支持 OneToOne 级联保存、级联删除(基于对象);
  • 修复 Delete.Where in 查询为空时,异步操作仍然执行删除;#1068 【受影响版本 v3.2.302】
  • 修复 InsertOrUpdateDict 异常;#1067 【受影响版本 v3.2.301、v3.2.300、v3.2.200】
  • 修复 InsertDict 部分新功能遗留问题(特别是 Oracle);【受影响版本 v3.2.301、v3.2.300、v3.2.200】
  • 修复 InsertDict/UpdateDict 等字典操作在 DbContext.Orm 下无法使用的 bug;#1064 【受影响版本 v3.2.300、v3.2.200】
  • 修复 MapType 复杂表达式树解析 bug;#1062
  • 修复 UseGenerateCommandParameterWithLambda 对不可参数化的数据类型冲突的 bug;#1061 #900
  • 修复 MySql Set 类型空格处理问题;#1059
  • 修复 SaveManyAsync 多对多历史漏改的问题(同步无问题);
  • 修复 OR 表达式处理情况;#1047
  • 修复 ClickHouse 设置 NoneParameter 会报错问题;
  • 修复 Clickhouse 连接池使用问题;#646 #968 #969 #943
  • 修复 pgsql IList -> JArray 映射;#1092
  • 修复 pgsql DbFirst IsPrimary bug;
  • 修复 JsonMap 与导航属性的联表查询报错的 bug;#996
  • 修复 子查询 WhereIf 可能失败的 bug;
  • 修复 StringLength 设置后 IsNullable = false 无生效的问题;
  • 修复 UseConnectionFactory 参数化问题;
  • 修复 参数值为原始 DbParameter 时转换类型报错;
  • 修复 UseGenerateCommandParameterWithLambda 子查询 IN bug;#900
  • 修复 InsertValueSql 在仓储插入后不返回最新值;
  • 完善 SqlServer BulkCopy 插入 DateTime 最小值问题;
  • 优化 导航集合属性访问,可省略 AsSelect;
  • 优化 DbContext/Repository Update 实体有 ServerTime 既使无状态变化也必然更新的逻辑;
  • 优化 DbContext/Repository 插入非主键自增回填;
  • 优化 ToList<Dto> jsonb 映射;
  • 优化 dywhere IN 查询按 500 元素分割;#1091
  • 优化 IIF 表达式解析;

v3.0.100(2021/12/17)

  • 增加 南大通用 Gbase 国产数据库支持;
  • 增加 ClickHouse 数据库语法支持;
  • 增加 DbContext/Repository 比较变化方法 CompareState;
  • 增加 DynamicFilter Custom 自定义解析;
  • 增加 ToDataTableByPropertyName 动态查询功能;
  • 优化 兼容排序 OrderBy(a => new {}) 语法;
  • 优化 pgsql jsonb 映射,支持 List,mysql limit in 子查询;
  • 优化 InsertOrUpdate<> 使用 InsertOrUpdate<list<>>时,提示友好异常。
  • 修复 BulkCopy 与线程事务未传播的 bug;#962
  • 修复 AsTreeCte + RereadSql 不能同时使用的 bug;#964
  • 修复 FreeSql.Generator 工具生成 model 失败 #882

更多历史更新日志,请访问:https://github.com/dotnetcore/FreeSql/wiki/%e6%9b%b4%e6%96%b0%e6%97%a5%e5%bf%97

]]>
常见问题 https://freesql.net/reference/faq.html https://freesql.net/reference/faq.html 常见问题 常见问题 1、如何监视 SQL? 方法一:UseMonitorCommand + UseNoneCommandParameter 方法二:Aop.CurdBefore/CurdAfter 2、MySql Enum 映射 默认情况 c# 枚举会映射为 MySql Enum 类型,如果想映射为 int 在 FreeSqlBuilder Build 之后执行... Fri, 16 Oct 2020 05:57:03 GMT 1、如何监视 SQL?

方法一:UseMonitorCommand + UseNoneCommandParameter

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
  .UseConnectionString(FreeSql.DataType.MySql, "...")
  .UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}\r\n"))
  .UseNoneCommandParameter(true)
  .Build();

方法二:Aop.CurdBefore/CurdAfter

fsql.Aop.CurdAfter += (s, e) =>
{
  if (e.ElapsedMilliseconds > 200)
    Console.WriteLine($"Sql:{e.Sql}\r\n");
};
]]>
与 EntityFramework 比较 https://freesql.net/reference/vs-entity-framework.html https://freesql.net/reference/vs-entity-framework.html 与 EntityFramework 比较 与 EntityFramework 比较 为什么要写这篇文章? 2020 年 写下这篇完整一点的 .NET ORM 比较,为准备使用 FreeSql 的朋友解惑。 基础信息比较 FreeSql .NET ORM 支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/Firebird/达梦/人大金仓/神舟通用/南大通用/... Fri, 16 Oct 2020 05:57:03 GMT 为什么要写这篇文章?

2020 年 写下这篇完整一点的 .NET ORM 比较,为准备使用 FreeSql 的朋友解惑。

基础信息比较

| 功能项 | FreeSql | EFCore | |

]]>