乐趣区

关于.net:设计过程NET-ORM-FreeSql-WhereDynamicFilter-动态表格查询功能

💻 前言

最近简直每天 40 度,越热越不想面对电脑,还好开源我的项目都比较稳定没那么多待解决问题,趁着寒假带着女儿学习游泳已略有小成。游泳益处太多了,倡议有孩子的都去学学,我是在岸边领导大概一周左右就学会了,目前可游 200 米。

FreeSql 有一个用户很迷的性能 WhereDynamicFilter 动静表格查问,本文解说它的设计初衷,如何高效了解,从此不再蛊惑。

小时候学习编程,老师常常教诲咱们,程序 = 数据结构 + 算法,明天就以我本身的认知解说该性能的残缺设计过程,其中蕴含数据结构和算法。


🌳 ORM 概念

对象关系映射(Object Relational Mapping,简称 ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的景象的技术。简略的说,ORM 是通过应用形容对象和数据库之间映射的元数据,将程序中的对象主动长久化到关系数据库中。

FreeSql 是 .Net ORM,能反对 .NetFramework4.0+、.NetCore、Xamarin、MAUI、Blazor、以及还有说不出来的运行平台,因为代码 绿色无依赖,反对新平台非常简单。目前单元测试数量:8500+,Nuget 下载数量:900K+。QQ 群:4336577(已满)、8578575(在线)、52508226(在线)

FreeSql 应用最宽松的开源协定 MIT https://github.com/dotnetcore/FreeSql,齐全能够商用,文档齐全,甚至拿去卖钱也能够。

FreeSql 次要劣势在于易用性上,根本是开箱即用,在不同数据库之间切换兼容性比拟好,整体的性能个性如下:

  • 反对 CodeFirst 比照构造变动迁徙;
  • 反对 DbFirst 从数据库导入实体类;
  • 反对 丰盛的表达式函数,自定义解析;
  • 反对 批量增加、批量更新、BulkCopy;
  • 反对 导航属性,贪心加载、延时加载、级联保留;
  • 反对 读写拆散、分表分库,租户设计;
  • 反对 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/Firebird/ 达梦 / 神通 / 人大金仓 / 翰高 /MsAccess Ado.net 实现包,以及 Odbc 的专门实现包;

8500+ 个单元测试作为基调,反对 10 少数数据库,咱们提供了通用 Odbc 实践上反对所有数据库,目前已知有群友应用 FreeSql 操作华为高斯、mycat、tidb 等数据库。装置时只须要抉择对应的数据库实现包:

dotnet add packages FreeSql.Provider.MySql


🎣 需要矛盾

尽管 ORM 有实践定义撑持,但理论开发过程中,不免遇到动静查问的需要,常见的有后盾管理系统用户自定义过滤查问,如:

鉴于理论与实践的矛盾,导致很多十分实用的性能类库让一些人诟病,指这是 SqlHelper,并非 ORM,在此不便实践,功过自在人心。


📰 数据结构

数据结构的定义,决定了性能的应用深度,这块也参考了一些竟品类似的性能,理论在 .NET ORM 畛域很少有完满并简略的事实,要么应用太简单,要么不反对深层级。

相似的性能其实市面产品利用挺宽泛,简直曾经造成了一套成熟的产品规定。如果不是亲身经历过相似产品,是很难定义出完满的数据结构的,作为一个公众开源我的项目,API 一旦确定再改是十分苦楚的决定,用户降级不兼容的状况不仅会影响 FreeSql 口碑,还会让使用者进退维谷,到底要不要降级?好在 FreeSql 从 2018 年最后理念放弃至今,对于前后破坏性降级简直没有。

最终依据对 SQL 逻辑表达式的了解,加上参考 JAVA 一个出名的后盾开源框架,舍短取长确定了最终数据结构。

说这么多无外乎三个重点:

1、本人不相熟的,多方面学习,接收更成熟的计划;

2、本人要是没想好怎么做,多察看再做;

3、多思考用户场景;

咱们须要思考的场景有以下几种:

1、WHERE id = 1

{
    "Field": "id",
    "Operator": "Equals",
    "Value": 1
}

2、WHERE id = 1 AND id = 2

{
    "Logic": "And",
    "Filters":
    [
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 1
        },
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 2
        }
    ]
}

3、WHERE id IN (1,2)

{
    "Field": "id",
    "Operator": "Contains",
    "Value": [1,2] // 或者 "1,2"
}

4、WHERE id = 1 OR id = 2

{
    "Logic": "Or",
    "Filters":
    [
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 1
        },
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 2
        }
    ]
}

5、WHERE id = 1 AND (id = 2 OR id = 3)

留神优先级,它不是 id = 1 AND id = 2 OR id = 3

{
    "Logic": "And",
    "Filters":
    [
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 1
        },
        {
            "Logic": "Or",
            "Filters":
            [
                {
                    "Field": "id",
                    "Operator": "Equals",
                    "Value": 2
                },
                {
                    "Field": "id",
                    "Operator": "Equals",
                    "Value": 3
                }
            ]
        }
    ]
}

第 5 个例子最特地,这也是为什么 WhereDynamicFilter 数据结构定义成树型的次要起因。

对于 Operator 咱们须要以下应用场景:

  • 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:自定义解析

最终残缺的 c# 数据结构类定义如下:

/// <summary>
/// 动静过滤条件
/// </summary>
[Serializable]
public class DynamicFilterInfo
{
    /// <summary>
    /// 属性名:Name<para></para>
    /// 导航属性:Parent.Name<para></para>
    /// 多表:b.Name<para></para>
    /// </summary>
    public string Field {get; set;}
    /// <summary>
    /// 操作符
    /// </summary>
    public DynamicFilterOperator Operator {get; set;}
    /// <summary>
    /// 值
    /// </summary>
    public object Value {get; set;}

    /// <summary>
    /// Filters 下的逻辑运算符
    /// </summary>
    public DynamicFilterLogic Logic {get; set;}
    /// <summary>
    /// 子过滤条件,它与以后的逻辑关系是 And<para></para>
    /// 留神:以后 Field 能够留空
    /// </summary>
    public List<DynamicFilterInfo> Filters {get; set;}
}

public enum DynamicFilterLogic {And, Or}
public enum DynamicFilterOperator
{
    /// <summary>
    /// like
    /// </summary>
    Contains,
    StartsWith,
    EndsWith,
    NotContains,
    NotStartsWith,
    NotEndsWith,

    /// <summary>
    /// =<para></para>
    /// Equal/Equals/Eq 成果雷同
    /// </summary>
    Equal,
    /// <summary>
    /// =<para></para>
    /// Equal/Equals/Eq 成果雷同
    /// </summary>
    Equals,
    /// <summary>
    /// =<para></para>
    /// Equal/Equals/Eq 成果雷同
    /// </summary>
    Eq,
    /// <summary>
    /// &lt;&gt;
    /// </summary>
    NotEqual,

    /// <summary>
    /// &gt;
    /// </summary>
    GreaterThan,
    /// <summary>
    /// &gt;=
    /// </summary>
    GreaterThanOrEqual,
    /// <summary>
    /// &lt;
    /// </summary>
    LessThan,
    /// <summary>
    /// &lt;=
    /// </summary>
    LessThanOrEqual,

    /// <summary>
    /// &gt;= and &lt;<para></para>
    /// 此时 Value 的值格局为逗号宰割:value1,value2 或者数组
    /// </summary>
    Range,

    /// <summary>
    /// &gt;= and &lt;<para></para>
    /// 此时 Value 的值格局为逗号宰割:date1,date2 或者数组 <para></para>
    /// 这是专门为日期范畴查问定制的操作符,它会解决 date2 + 1,比方:<para></para>
    /// 当 date2 抉择的是 2020-05-30,那查问的时候是 &lt; 2020-05-31<para></para>
    /// 当 date2 抉择的是 2020-05,那查问的时候是 &lt; 2020-06<para></para>
    /// 当 date2 抉择的是 2020,那查问的时候是 &lt; 2021<para></para>
    /// 当 date2 抉择的是 2020-05-30 12,那查问的时候是 &lt; 2020-05-30 13<para></para>
    /// 当 date2 抉择的是 2020-05-30 12:30,那查问的时候是 &lt; 2020-05-30 12:31<para></para>
    /// 并且 date2 只反对以上 5 种格局 (date1 没有限度)
    /// </summary>
    DateRange,

    /// <summary>
    /// in (1,2,3)<para></para>
    /// 此时 Value 的值格局为逗号宰割:value1,value2,value3... 或者数组
    /// </summary>
    Any,
    /// <summary>
    /// not in (1,2,3)<para></para>
    /// 此时 Value 的值格局为逗号宰割:value1,value2,value3... 或者数组
    /// </summary>
    NotAny,

    /// <summary>
    /// 自定义解析,此时 Field 为反射信息,Value 为静态方法的参数(string)<para></para>
    /// 示范:{Operator: "Custom", Field: "RawSql webapp1.DynamicFilterCustom,webapp1", Value: "(id,name) in ((1,'k'),(2,'m'))" }<para></para>
    /// 留神:使用者本人承当【注入危险】<para></para>
    /// 静态方法定义示范:<para></para>
    /// namespace webapp1<para></para>
    /// {<para></para>
    /// public class DynamicFilterCustom<para></para>
    /// {<para></para>
    /// [DynamicFilterCustom]<para></para>
    /// public static string RawSql(object sender, string value) => value;<para></para>
    /// }<para></para>
    /// }<para></para>
    /// </summary>
    Custom
}

/// <summary>
/// 受权 DynamicFilter 反对 Custom 自定义解析
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class DynamicFilterCustomAttribute : Attribute {}

📡 平安思考

因为 ISelect.WhereDynamicFilter 办法实现动静过滤条件(与前端交互),在 SQL 注入平安进攻这块肯定要进行到底,次要思考如下:

1、Field 只容许传递 c# 实体属性名(不反对应用数据库字段名,甚至间接应用 SQL 内容段);

2、Operator 只容许规定的枚举操作类型;

3、Value 必须依据 Operator 进行强制类型查看,比方 “1,2” + Any 检索进去的数据是 int[] { 1,2};

4、Operator Custom 类型反对用户自行扩大,可事实更自在的查问;


⚡ 算法

如果把数据结构定义成灵魂,那算法就是驱壳,实现 WhereDynamicFilter 的外围算法是递归树结构。

感兴趣的敌人能够间接去源码查看实现:https://github.com/dotnetcore/FreeSql


🌌 难了解

WhereDynamicFilter 性能 2020 年上线到当初,我集体都感觉其实蛮难了解的,更不要提很多使用者反馈。次要起因是数据结构为树结构,通常 80% 的人只是简略的一层 AND/OR 需要,他们很少会遇到深层级的自定义查问。

然而作为功能性 ORM 类库,应该满足更多适用范围,而不是斗争为求简略来做。

其实便于了解也不难,只有把握以下办法:

1、Logic 是设置 Filters 数组下的逻辑关系(这很重要,肯定要了解正确)

为了解决 WHERE id = 1 AND (id = 2 OR id = 3) 优先级问题,Filters 更像一对括号

{
    "Logic": "And",
    "Filters":
    [{ "Field": "id", "Operator": "Equals", "Value": 1},
        {
            "Logic": "Or",
            "Filters":
            [{ "Field": "id", "Operator": "Equals", "Value": 2},
                {"Field": "id", "Operator": "Equals", "Value": 3}
            ]
        }
    ]
}

2、Field/Operator/Value 与 Logic/Filters 不要同时设置(防止了解艰难)

3、删除 JSON 中不必要的内容

这个病不好治,因为强类型对象产生的默认 json 内容,即便无用的属性也序列化了。

{
    "Field": null,
    "Operator": "And",
    "Value": null,
    "Logic": "Or",
    "Filters":
    [
        {
            "Field": "Name-1",
            "Operator": "Equals",
            "Value": "ye-01",
            "Logic": "And",
            "Fitlers": null
        },
        {
            "Field": "Name-1",
            "Operator": "Equals",
            "Value": "ye-02",
            "Logic": "And",
            "Fitlers": null
        }
    ]
}

以上类型改成如下,是不是更好了解?

{
    "Logic": "Or",
    "Filters":
    [
        {
            "Field": "Name-1",
            "Operator": "Equals",
            "Value": "ye-01"
        },
        {
            "Field": "Name-1",
            "Operator": "Equals",
            "Value": "ye-02"
        }
    ]
}

🚀 最终性能

一个任意定制的高级查问性能预览如下:

前端只须要按要求组装好 DynamicFilterInfo 对应的 JSON 数据内容,后盾就可轻易实现高级过滤查问,有多轻易呢?

var dyfilter = JsonConvert.DeserializeObject<DynamicFilterInfo>(jsonText);
var list = fsql.Select<T>().WhereDynamicFilter(dyfilter).ToList();

⛳ 结束语

心愿这篇文章能帮忙大家从 WhereDynamicFilter 的设计初衷,轻松了解并熟练掌握它,为企业的我的项目研发贡献力量。

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


作者是什么人?

作者是一个入行 18 年的老批,他目前写的.net 开源我的项目有:

开源我的项目 形容 开源地址 开源协定
ImCore 架构最简略,扩展性最强的聊天零碎架构 https://github.com/2881099/im 最宽松的 MIT 协定,可商用
FreeRedis 最简略的 RediscClient https://github.com/2881099/Fr… 最宽松的 MIT 协定,可商用
csredis https://github.com/2881099/cs… 最宽松的 MIT 协定,可商用
FightLandlord 斗地主单机或网络版 https://github.com/2881099/Fi… 最宽松的 MIT 协定,学习用处
IdleScheduler 定时工作 https://github.com/2881099/Id… 最宽松的 MIT 协定,可商用
IdleBus 闲暇容器 https://github.com/2881099/Id… 最宽松的 MIT 协定,可商用
FreeSql 国产最好用的 ORM https://github.com/dotnetcore… 最宽松的 MIT 协定,可商用
FreeSql.Cloud 分布式事务 tcc/saga https://github.com/2881099/Fr… 最宽松的 MIT 协定,可商用
FreeSql.AdminLTE 低代码后盾治理我的项目生成 https://github.com/2881099/Fr… 最宽松的 MIT 协定,可商用
FreeSql.DynamicProxy 动静代理 https://github.com/2881099/Fr… 最宽松的 MIT 协定,学习用处

须要的请拿走,这些都是最近几年的开源作品,以前更早写的就不发了。

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

退出移动版