论如何直接用EF Core实现创建更新时间、用户审计,自动化乐观并发、软删除和树形查询(下)

前言

数据库并发,数据审计和软删除一直是数据持久化方面的经典问题。早些时候,这些工作需要手写复杂的SQL或者通过存储过程和触发器实现。手写复杂SQL对软件可维护性构成了相当大的挑战,随着SQL字数的变多,用到的嵌套和复杂语法增加,可读性和可维护性的难度是几何级暴涨。因此如何在实现功能的同时控制这些SQL的复杂度是一个很有价值的问题。而且这个问题同时涉及应用软件和数据库两个相对独立的体系,平行共管也是产生混乱的一大因素。

EF Core作为 .NET平台的高级ORM框架,可以托管和数据库的交互,同时提供了大量扩展点方便自定义。以此为基点把对数据库的操作托管后便可以解决平行共管所产生的混乱,利用LINQ则可以最大程度上降低软件代码的维护难度。

由于项目需要,笔者先后开发并发布了通用的基于EF Core存储的国际化服务基于EF Core存储的Serilog持久化服务,不过这两个功能包并没有深度利用EF Core,虽然主要是因为没什么必要。但是项目还需要提供常用的数据审计和软删除功能,因此对EF Core进行了一些更深入的研究。

起初有考虑过是否使用现成的ABP框架来处理这些功能,但是在其他项目的使用体验来说并不算好,其中充斥着大量上下文依赖的功能,而且这些依赖信息能轻易藏到和最终业务代码相距十万八千里的地方(特别是代码还是别人写的时候),然后在不经意间给你一个大惊喜。对于以代码正交性、非误导性,纯函数化为追求的一介码农(看过我发布的那两个功能包的朋友应该有感觉,一个功能笔者也要根据用途划分为不同的包,确保解决方案中的各个项目都能按需引用,不会残留无用的代码),实在是喜欢不起来ABP这种全家桶。

鉴于项目规模不大,笔者决定针对这些需求做一个专用功能,目标是尽可能减少依赖,方便将来复用到其他项目,降低和其他功能功能冲突的风险。现在笔者将用一系列博客做成果展示。由于这些功能没有经过大范围测试,不确定是否存在未知缺陷,因此暂不打包发布。

新书宣传

有关新书的更多介绍欢迎查看《C#与.NET6 开发从入门到实践》上市,作者亲自来打广告了!

正文

由于这些功能设计的代码量和知识点较多,为控制篇幅,本文介绍树形查询功能。

SqlServer原生支持分层数据,EF Core也提供了相应的支持,但是很遗憾,这又是一个独占功能。为了兼容其他数据库只能单独处理。由于EF Core的导航修复功能,使用ParentId的自关联结构能得到原生支持。这也是描述一棵树最简单且不会破坏数据完整性的方式(即这种描述方式永远满足树结构的所有判定约束)。但是在查询方面,这种结构确并不方便,因此为了简化查询,出现了其他存储树的设计方式,常见的有左右值编码、路径描述和额外的关系描述表等。这些描述方式能在一定程度上简化查询,但是确无法在物理上确保数据完整性,这就对数据维护提出了严峻的挑战。

在中篇我们用视图实现了全自动的级联软删除模拟,那么是否同样可以用视图来解决树形结构的查询问题呢?答案是肯定的,而这只有一个小小的前提条件------支持公用表表达式(SQL中的递归)。这样就能实现物理表中使用ParentId的自关联确保数据完整性,同时自动兼容EF Core的导航修复。而用于简化查询的其他信息则由视图自动计算生成。

生成树的视图功能其实已经在本文宣传的书中实现了,不过这次新增自动软删除后,树视图也需要考虑如何兼容软删除。一开始笔者想过在一个视图定义中实现,后来发现这种方式开发难度比较大,而且不利于复用已有的研究成果。最终决定使用独立的视图,这就涉及到视图数据源的选择,因为EF Core只能映射一个视图。经过一番思考发现树形视图永远只依赖其自身的表或视图,因此EF Core映射到树形视图,属性视图依赖软删除视图是最简单方便的。在之前介绍软删除的文章中已经出现了和树有关的代码,这些代码的一部分用处就是选择映射目标。

代码实现

基础接口

csharp 复制代码
/// <summary>
/// 树形数据接口
/// </summary>
/// <typeparam name="T">节点数据类型</typeparam>
public interface ITree<T>
{
    /// <summary>
    /// 父节点
    /// </summary>
    T? Parent { get; set; }

    /// <summary>
    /// 子节点集合
    /// </summary>
    IList<T> Children { get; set; }

    /// <summary>
    /// 节点深度,根的深度为0
    /// </summary>
    int Depth { get; }

    /// <summary>
    /// 是否是根节点
    /// </summary>
    bool IsRoot { get; }

    /// <summary>
    /// 是否是叶节点
    /// </summary>
    bool IsLeaf { get; }

    /// <summary>
    /// 是否有子节点
    /// </summary>
    bool HasChildren { get; }

    /// <summary>
    /// 节点路径(UNIX路径格式,以"/"分隔)
    /// </summary>
    string? Path { get; }
}

/// <summary>
/// 树形实体接口
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
public interface ITreeEntity<T> : IEntity, ITree<T>
{
}

/// <summary>
/// 树形实体接口
/// </summary>
/// <typeparam name="TKey">主键类型</typeparam>
/// <typeparam name="TEntity">实体类型</typeparam>
public interface ITreeEntity<TKey, TEntity> : ITreeEntity<TEntity>, IEntity<TKey>
    where TKey : struct, IEquatable<TKey>
    where TEntity : ITreeEntity<TKey, TEntity>
{
    /// <summary>
    /// 父节点Id
    /// </summary>
    TKey? ParentId { get; set; }
}

/// <summary>
/// 实体接口
/// </summary>
public interface IEntity;

/// <summary>
/// 实体接口
/// </summary>
/// <typeparam name="TKey">唯一标识的类型</typeparam>
public interface IEntity<TKey> : IEntity
    where TKey : struct, IEquatable<TKey>
{
    /// <summary>
    /// 实体的唯一标识
    /// </summary>
    TKey Id { get; set; }
}

本文的ITree<T>接口就是从前文软删除视图操作排序用的接口简化而来。

模型配置扩展

csharp 复制代码
/// <summary>
/// 树形实体模型配置扩展
/// </summary>
public static class TreeEntityModelBuilderExtensions
{
    private const string _queryViewAnnotationName = EntityModelBuilderExtensions._queryViewAnnotationName;

    /// <summary>
    /// 配置树形实体接口
    /// </summary>
    /// <typeparam name="TKey">主键类型</typeparam>
    /// <typeparam name="TEntity">树形实体类型</typeparam>
    /// <param name="builder">实体类型构造器</param>
    /// <param name="dummyValueSql">表用计算列的虚假值生成Sql</param>
    /// <returns>实体类型构造器</returns>
    public static EntityTypeBuilder<TEntity> ConfigureForITreeEntity<TKey, TEntity>(
        this EntityTypeBuilder<TEntity> builder,
        ITreeEntityDummyValueSql dummyValueSql
    )
        where TKey : struct, IEquatable<TKey>
        where TEntity : class, ITreeEntity<TKey, TEntity>
    {
        ArgumentNullException.ThrowIfNull(builder);

        builder.HasOne(e => e.Parent)
            .WithMany(pe => pe.Children)
            .HasForeignKey(e => e.ParentId);

        builder.Property(e => e.Depth)
            .HasComputedColumnSql(dummyValueSql.DepthSql);

        builder.Property(e => e.HasChildren)
            .HasComputedColumnSql(dummyValueSql.HasChildrenSql);

        builder.Property(e => e.Path)
            .HasComputedColumnSql(dummyValueSql.PathSql);

        ConfigQueryViewAnnotationForTreeEntity<TKey, TEntity>(builder);

        return builder;
    }

    /// <summary>
    /// 配置树形实体接口
    /// </summary>
    /// <param name="modelBuilder">模型构造器</param>
    /// <param name="dummyValueSql">表用计算列的虚假值生成Sql</param>
    /// <returns>模型构造器</returns>
    public static ModelBuilder ConfigureForITreeEntity(this ModelBuilder modelBuilder, ITreeEntityDummyValueSql dummyValueSql)
    {
        ArgumentNullException.ThrowIfNull(modelBuilder);
        ArgumentNullException.ThrowIfNull(dummyValueSql);

        foreach (var entity
            in modelBuilder.Model.GetEntityTypes()
                .Where(static e => e.ClrType.IsDerivedFrom(typeof(ITreeEntity<,>))))
        {
            var entityTypeBuilderMethod = GetEntityTypeBuilderMethod(entity);
            var treeEntityMethod = GetEntityTypeConfigurationMethod(
                nameof(ConfigureForITreeEntity),
                2,
                entity.FindProperty(nameof(TreeType.Id))!.ClrType,
                entity.ClrType);

            treeEntityMethod.Invoke(null, [entityTypeBuilderMethod.Invoke(modelBuilder, null), dummyValueSql]);
        }

        return modelBuilder;
    }

    /// <summary>
    /// 配置树形实体的查询视图注解
    /// </summary>
    /// <typeparam name="TKey">实体主键类型</typeparam>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <param name="builder">实体类型构造器</param>
    private static void ConfigQueryViewAnnotationForTreeEntity<TKey, TEntity>(EntityTypeBuilder<TEntity> builder)
        where TKey : struct, IEquatable<TKey>
        where TEntity : class, ITreeEntity<TKey, TEntity>
    {
        var annotationValue = builder.Metadata.FindAnnotation(_queryViewAnnotationName)?.Value;
        if (annotationValue is null)
        {
            builder.HasAnnotation(_queryViewAnnotationName, new List<Type>() { typeof(ITreeEntity<,>) });
        }
        else
        {
            var stringListAnnotationValue = annotationValue as List<Type>;
            if (stringListAnnotationValue is not null && stringListAnnotationValue.Find(static x => x == typeof(ITreeEntity<,>)) is null)
            {
                stringListAnnotationValue.Add(typeof(ITreeEntity<,>));
            }
        }
    }
}

/// <summary>
/// 仅用于内部辅助,无实际作用
/// </summary>
file sealed class TreeType : ITreeEntity<int, TreeType>
{
    public TreeType()
    {
        throw new NotImplementedException();
    }

    public int? ParentId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
    public TreeType? Parent { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
    public IList<TreeType> Children { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

    public int Depth => throw new NotImplementedException();

    public bool IsRoot => throw new NotImplementedException();

    public bool IsLeaf => throw new NotImplementedException();

    public bool HasChildren => throw new NotImplementedException();

    public string? Path => throw new NotImplementedException();

    public int Id { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
}

Sql模版(以SqlServer为例)

csharp 复制代码
/// <summary>
/// 树形实体的视图列在表中的临时值映射
/// <para>EF Core目前还不支持多重映射时分别配置表和视图的映射,因此需要在表中映射一个同名计算列</para>
/// </summary>
public interface ITreeEntityDummyValueSql
{
    /// <summary>
    /// 节点深度的SQL
    /// </summary>
    string DepthSql { get; }

    /// <summary>
    /// 节点是否有子树的SQL
    /// </summary>
    string HasChildrenSql { get; }

    /// <summary>
    /// 节点路径的SQL
    /// </summary>
    string PathSql { get; }
}

public class DefaultSqlServerTreeEntityDummyValueSql : ITreeEntityDummyValueSql
{
    public static DefaultSqlServerTreeEntityDummyValueSql Instance => new();

    private const string _depthSql = "-1";
    private const string _hasChildrenSql = "cast(0 as bit)";
    private const string _pathSql = "''";

    public string DepthSql => _depthSql;

    public string HasChildrenSql => _hasChildrenSql;

    public string PathSql => _pathSql;

    private DefaultSqlServerTreeEntityDummyValueSql() { }
}

/// <summary>
/// 树形实体的视图SQL模板
/// </summary>
public interface ITreeEntityDatabaseViewSqlTemplate : ITableOrColumnNameFormattable
{
    /// <summary>
    /// 创建视图的模板
    /// </summary>
    string CreateSqlTemplate { get; }

    /// <summary>
    /// 删除视图的模板
    /// </summary>
    string DropSqlTemplate { get; }
}

public class DefaultSqlServerTreeEntityViewSqlTemplate : ITreeEntityDatabaseViewSqlTemplate
{
    public static DefaultSqlServerTreeEntityViewSqlTemplate Instance => new();

    private const string _viewNameTemplate = $$"""{{EntityModelBuilderExtensions._treeQueryViewNamePrefixes}}{tableName}""";

    private const string _createSqlTemplate =
        $$"""
        --创建或重建树形实体查询视图
        {{_dropSqlTemplate}}
        CREATE VIEW {{_viewNameTemplate}}    --创建视图
        AS
        WITH [temp]({columns}, [Depth], [Path], [HasChildren]) AS
        (
            --初始查询(这里的 [ParentId] IS NULL 在数据中是最底层的根节点)
            SELECT {columns},
                0 AS [Depth],
                '/' + CAST([Id] AS nvarchar(max)) + '/' AS [Path], --如果Id使用Guid类型,可能会导致层数太深时出问题(大概100层左右,超过4000字之后的字符串会被砍掉,sqlserver 2005以后用 nvarchar(max)可以突破限制),Guid的字数太多了
                (CASE WHEN EXISTS(SELECT 1 FROM [{dataSourceName}] WHERE [{dataSourceName}].[ParentId] = [Root].[Id]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END) AS [HasChildren]
            FROM [{dataSourceName}] AS [Root]
            WHERE [Root].[ParentId] IS NULL

            UNION ALL
            --递归条件
            SELECT {child.columns},
                [Parent].[Depth] + 1,
                [Parent].[Path] + CAST([Child].[Id] AS nvarchar(max)) + '/' AS [Path],
                (CASE WHEN EXISTS(SELECT 1 FROM [{dataSourceName}] WHERE [{dataSourceName}].[ParentId] = [Child].[Id]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END) AS [HasChildren]
            FROM [{dataSourceName}] AS [Child] --3:这里的临时表和原始数据表都必须使用别名不然递归的时候不知道查询的是哪个表的列
            INNER JOIN [temp] AS [Parent]
            ON ([Child].[ParentId] = [Parent].[Id]) --这个关联关系很重要,一定要理解一下谁是谁的父节点
        )
        --4:递归完成后 一定不要少了这句查询语句 否则会报错
        SELECT *
        FROM [temp];
        GO
        """;

    private const string _dropSqlTemplate =
        $"""
        --删除可能存在的过时树形实体查询视图
        IF EXISTS(SELECT * FROM [sysobjects] WHERE [id] = OBJECT_ID(N'{_viewNameTemplate}') AND objectproperty(id, N'IsView') = 1)
        BEGIN
            DROP VIEW [{_viewNameTemplate}]
        END
        GO
        """;

    public string CreateSqlTemplate => _createSqlTemplate;

    public string DropSqlTemplate => _dropSqlTemplate;

    public string? FormatTableOrColumnName(string? name)
    {
        if(name is null) return null;

        return $"[{name}]";
    }

    private DefaultSqlServerTreeEntityViewSqlTemplate() { }
}

迁移扩展

csharp 复制代码
/// <summary>
/// 树形实体视图迁移扩展
/// </summary>
public static class TreeEntityMigrationBuilderExtensions
{
    private static readonly ImmutableArray<string> _properties = ["Depth", "Path", "HasChildren"];

    /// <summary>
    /// 自动扫描迁移模型并配置树形实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="thisVersion">当前版本的迁移</param>
    /// <param name="previousVersion">上一个版本的迁移</param>
    /// <param name="isUp">是否为升级迁移</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder ApplyTreeEntityQueryView(
        this MigrationBuilder migrationBuilder,
        Migration thisVersion,
        Migration? previousVersion,
        bool isUp,
        ITreeEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(thisVersion);
        ArgumentNullException.ThrowIfNull(sqlTemplate);

        var thisVersionEntityTypes = thisVersion.TargetModel.GetEntityTypes()
            .Where(static et =>
                (et.FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
                ?.Any(x => x == typeof(ITreeEntity<,>)) is true
            );

        var previousVersionEntityTypes = previousVersion?.TargetModel.GetEntityTypes()
            .Where(static et =>
                (et.FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
                ?.Any(x => x == typeof(ITreeEntity<,>)) is true
            );

        var pendingViewOperations = new List<(IEntityType? entity, string? tableName, bool isCreate)>();

        var tempViewOperationsDict = new Dictionary<string, List<(IEntityType? entity, string? tableName, bool isCreate)>>();
        foreach (var tableOperation in
            migrationBuilder.Operations.Where(static op =>
            {
                var opType = op.GetType();
                return opType.IsDerivedFrom<TableOperation>() || opType.IsDerivedFrom<DropTableOperation>();
            }))
        {
            if (tableOperation is CreateTableOperation createTable)
            {
                // 升级迁移创建表,同步创建视图
                if (isUp && thisVersionEntityTypes.Any(et => et.GetTableName() == createTable.Name))
                {
                    var entity = thisVersionEntityTypes.Single(en => en.GetTableName() == createTable.Name);
                    AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                }

                // 降级迁移创建表,如果上一个版本的模型是树形实体,用上一个版本的模型重建视图
                if (!isUp)
                {
                    EnsureMigrationOfPreviousVersion(previousVersion);
                    if (previousVersionEntityTypes!.Any(et => et.GetTableName() == createTable.Name) is true)
                    {
                        var entity = previousVersionEntityTypes!.Single(en => en.GetTableName() == createTable.Name);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                }
            }
            // 迁移操作修改表只对修改表名作出反应
            else if (tableOperation is AlterTableOperation alterTable)
            {
                // 升级迁移用当前版本的模型重建视图
                if (isUp)
                {
                    // 如果上一版本这个实体是树形实体,删除旧视图
                    if (previousVersionEntityTypes?.Any(en => en.GetTableName() == alterTable.OldTable.Name) is true)
                    {
                        pendingViewOperations.Add((null, alterTable.OldTable.Name, false));
                    }

                    if (thisVersionEntityTypes!.Any(en => en.GetTableName() == alterTable.Name))
                    {
                        var entity = thisVersionEntityTypes!.Single(en => en.GetTableName() == alterTable.Name);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                }
                // 回滚迁移用上一个版本的模型重建视图
                else
                {
                    // 如果当前版本这个实体是树形实体,删除旧视图
                    if (thisVersionEntityTypes.Any(en => en.GetTableName() == alterTable.OldTable.Name))
                    {
                        pendingViewOperations.Add((null, alterTable.OldTable.Name, false));
                    }

                    EnsureMigrationOfPreviousVersion(previousVersion);
                    if (previousVersionEntityTypes!.Any(en => en.GetTableName() == alterTable.Name))
                    {
                        var entity = previousVersionEntityTypes!.Single(en => en.GetTableName() == alterTable.Name);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                }
            }
            else if (tableOperation is DropTableOperation dropTable)
            {
                // 升级迁移删除表,如果在上一版本中这个实体是树形实体,删除视图
                if (isUp)
                {
                    EnsureMigrationOfPreviousVersion(previousVersion);
                    if (previousVersionEntityTypes!.Any(en => en.GetTableName() == dropTable.Name))
                    {
                        AddTableDropTableViewToTempDict(tempViewOperationsDict, dropTable.Name);
                    }
                }
                // 回滚迁移删除表,如果在当前版本中这个实体是树形实体,删除视图
                else if (thisVersionEntityTypes.Any(en => en.GetTableName() == dropTable.Name))
                {
                    AddTableDropTableViewToTempDict(tempViewOperationsDict, dropTable.Name);
                }
            }
        }

        foreach (var columnOperation in
            migrationBuilder.Operations.Where(static op =>
            {
                var opType = op.GetType();
                return opType.IsDerivedFrom<ColumnOperation>() || opType.IsDerivedFrom<DropColumnOperation>();
            }))
        {
            if (columnOperation is AddColumnOperation addColumn)
            {
                if (isUp && thisVersionEntityTypes!.Any(en => en.GetTableName() == addColumn.Table))
                {
                    var entity = thisVersionEntityTypes!.Single(en => en.GetTableName() == addColumn.Table);
                    AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                }

                if (!isUp)
                {
                    EnsureMigrationOfPreviousVersion(previousVersion);
                    if (previousVersionEntityTypes!.Any(en => en.GetTableName() == addColumn.Table))
                    {
                        var entity = previousVersionEntityTypes!.Single(en => en.GetTableName() == addColumn.Table);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                }
            }
            else if (columnOperation is AlterColumnOperation alterColumn/* && alterColumn.OldColumn.Name is not null && alterColumn.Name != alterColumn.OldColumn.Name*/)
            {
                if (isUp)
                {
                    if (thisVersionEntityTypes!.Any(et => et.GetTableName() == alterColumn.Table))
                    {
                        var entity = thisVersionEntityTypes!.Single(en => en.GetTableName() == alterColumn.Table);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                    else if (previousVersionEntityTypes?.Any(et => et.GetTableName() == alterColumn.Table) is true)
                    {
                        AddTableDropTableViewToTempDict(tempViewOperationsDict, alterColumn.Table);
                    }
                }
                else
                {
                    EnsureMigrationOfPreviousVersion(previousVersion);
                    if (previousVersionEntityTypes!.Any(en => en.GetTableName() == alterColumn.Table))
                    {
                        var entity = previousVersionEntityTypes!.Single(en => en.GetTableName() == alterColumn.Table);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                }
            }
            else if (columnOperation is DropColumnOperation dropColumn)
            {
                if (isUp)
                {
                    // 当前版本仍然是树形实体,说明被删除的列和树形无关,重建视图
                    if (thisVersionEntityTypes!.Any(et => et.GetTableName() == dropColumn.Table))
                    {
                        var entity = thisVersionEntityTypes!.Single(en => en.GetTableName() == dropColumn.Table);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                    // 被删除的列是树形相关列(上一版本的实体是树形,但当前版本不是),删除视图
                    else if (previousVersionEntityTypes?.Any(et => et.GetTableName() == dropColumn.Table) is true)
                    {
                        AddTableDropTableViewToTempDict(tempViewOperationsDict, dropColumn.Table);
                    }
                }

                if (!isUp)
                {
                    EnsureMigrationOfPreviousVersion(previousVersion);
                    // 上一版本是树形实体,说明被删除的列和树形无关,重建视图
                    if (previousVersionEntityTypes?.Any(et => et.GetTableName() == dropColumn.Table) is true)
                    {
                        var entity = previousVersionEntityTypes.Single(en => en.GetTableName() == dropColumn.Table);
                        AddEntityCreateEntityViewToTempDict(tempViewOperationsDict, entity);
                    }
                    // 被删除的列是树形(上一版本的实体不是树形,但当前版本是),删除视图
                    else if (thisVersionEntityTypes?.Any(et => et.GetTableName() == dropColumn.Table) is true)
                    {
                        AddTableDropTableViewToTempDict(tempViewOperationsDict, dropColumn.Table);
                    }
                }
            }
        }

        // 聚合所有操作,然后选择其中合理的一个作为最终操作
        foreach (var entityViewOperations in tempViewOperationsDict)
        {
            Debug.Assert(entityViewOperations.Value.All(x => x.isCreate == entityViewOperations.Value.First().isCreate));
            if (isUp)
            {
                // 如果当前版本的实体确实是树形实体,选择创建视图的命令
                if ((thisVersionEntityTypes
                        ?.SingleOrDefault(et => et.GetTableName() == entityViewOperations.Key)
                        ?.FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
                        ?.Any(x => x == typeof(ITreeEntity<,>)) is true)
                {
                    pendingViewOperations.Add(entityViewOperations.Value.First(o => o.entity is not null && o.isCreate));
                }
                else
                {
                    pendingViewOperations.Add(entityViewOperations.Value.First(o => !o.isCreate));
                }
            }
            else
            {
                // 当前迁移就是第一版,选择删除视图命令
                if (previousVersion is null)
                {
                    pendingViewOperations.Add(entityViewOperations.Value.First(o => !o.isCreate));
                }
                // 如果上一版本的实体确实是树形实体,选择创建视图的命令
                else if ((previousVersionEntityTypes
                            ?.Single(et => et.GetTableName() == entityViewOperations.Key)
                            .FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
                            ?.Any(x => x == typeof(IDependencyLogicallyDeletable)) is true)
                {
                    pendingViewOperations.Add(entityViewOperations.Value.First(o => o.entity is not null && o.isCreate));
                }
                else
                {
                    pendingViewOperations.Add(entityViewOperations.Value.First(o => !o.isCreate));
                }
            }
        }

        foreach (var (entity, tableName, isCreate) in pendingViewOperations)
        {
            if (isCreate) migrationBuilder.CreateTreeEntityQueryView(entity!, sqlTemplate);
            else if (entity is not null) migrationBuilder.DropTreeEntityQueryView(entity, sqlTemplate);
            else if (tableName is not null) migrationBuilder.DropTreeEntityQueryView(tableName, sqlTemplate);
            else throw new InvalidOperationException("迁移实体类型和迁移表名不能同时为 null。");
        }

        return migrationBuilder;

        /// <summary>
        /// 确保提供了上一版本的迁移
        /// </summary>
        static void EnsureMigrationOfPreviousVersion(Migration? previousVersion)
        {
            if (previousVersion is null) throw new InvalidOperationException($"回滚操作指出存在更早版本的迁移,但未提供上一版本的迁移。");
        }

        /// <summary>
        /// 向按表分组的临时操作存放字典添加创建实体视图命令
        /// </summary>
        static void AddEntityCreateEntityViewToTempDict(Dictionary<string, List<(IEntityType? entity, string? tableName, bool isCreate)>> tempViewOperationsDict, IEntityType entity)
        {
            if (!tempViewOperationsDict.TryGetValue(entity.GetTableName()!, out var result))
            {
                result ??= [];
                tempViewOperationsDict.Add(entity.GetTableName()!, result);
            }
            result.Add((entity, null, true));
        }

        /// <summary>
        /// 向按表分组的临时操作存放字典添加删除表视图命令
        /// </summary>
        static void AddTableDropTableViewToTempDict(Dictionary<string, List<(IEntityType? entity, string? tableName, bool isCreate)>> tempViewOperationsDict, string tableName)
        {
            if (!tempViewOperationsDict.TryGetValue(tableName, out var result))
            {
                result ??= [];
                tempViewOperationsDict.Add(tableName, result);
            }
            result.Add((null, tableName, false));
        }
    }

    /// <summary>
    /// 创建树形实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="entityType">实体类型</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder CreateTreeEntityQueryView(
        this MigrationBuilder migrationBuilder,
        IEntityType entityType,
        ITreeEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(entityType);
        ArgumentNullException.ThrowIfNull(sqlTemplate);

        var isTreeEntity = (entityType
            .FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
            ?.Any(static x => x == typeof(ITreeEntity<,>)) is true;

        if (!isTreeEntity) throw new InvalidOperationException($"{entityType.Name}不是树形实体或未配置视图生成。");

        var isDependencyLogicallyDeletableEntity = (entityType
            .FindAnnotation(EntityModelBuilderExtensions._queryViewAnnotationName)?.Value as List<Type>)
            ?.Any(static x => x == typeof(IDependencyLogicallyDeletable)) is true;

        var tableName = entityType.GetTableName()!;
        var dataSourceName = isDependencyLogicallyDeletableEntity
            ? $"{EntityModelBuilderExtensions._queryViewNamePrefixes}{tableName}"
            : tableName;

        var tableIdentifier = StoreObjectIdentifier.Table(tableName);

        var columnNames = entityType.GetProperties()
            .Where(static c => !_properties.Contains(c.Name))
            .Select(pro => sqlTemplate.FormatTableOrColumnName(pro.GetColumnName(tableIdentifier)));
        var childColumnNames = columnNames.Select(c => $@"{sqlTemplate.FormatTableOrColumnName("Child")}.{c}");

        migrationBuilder.Sql(sqlTemplate.CreateSqlTemplate
            .Replace("{tableName}", tableName)
            .Replace("{dataSourceName}", dataSourceName)
            .Replace("{columns}", string.Join(", ", columnNames))
            .Replace("{child.columns}", string.Join(", ", childColumnNames))
        );

        return migrationBuilder;
    }

    /// <summary>
    /// 删除树形实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="entityType">实体类型</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder DropTreeEntityQueryView(
        this MigrationBuilder migrationBuilder,
        IEntityType entityType,
        ITreeEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(entityType);
        ArgumentNullException.ThrowIfNull(sqlTemplate);

        return migrationBuilder.DropTreeEntityQueryView(entityType.GetTableName()!, sqlTemplate);
    }

    /// <summary>
    /// 删除树形实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="tableName">视图对应的表名</param>
    /// <param name="sqlTemplate">Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder DropTreeEntityQueryView(
        this MigrationBuilder migrationBuilder,
        string tableName,
        ITreeEntityDatabaseViewSqlTemplate sqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(sqlTemplate);
        if (string.IsNullOrEmpty(tableName))
        {
            throw new ArgumentException($""{nameof(tableName)}"不能为 null 或空。", nameof(tableName));
        }

        migrationBuilder.Sql(sqlTemplate.DropSqlTemplate.Replace("{tableName}", tableName));

        return migrationBuilder;
    }
}

public static class EntityMigrationBuilderExtensions
{
    /// <summary>
    /// 自动扫描迁移模型并配置实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="thisVersion">当前版本的迁移</param>
    /// <param name="previousVersion">上一个版本的迁移</param>
    /// <param name="isUp">是否为升级迁移</param>
    /// <param name="dependencyLogicallyDeletableEntityViewSqlTemplate">依赖项逻辑删除实体视图Sql模板</param>
    /// <param name="treeEntityViewSqlTemplate">树形实体视图Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder ApplyEntityQueryView(
        this MigrationBuilder migrationBuilder,
        Migration thisVersion,
        Migration? previousVersion,
        bool isUp,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate dependencyLogicallyDeletableEntityViewSqlTemplate,
        ITreeEntityDatabaseViewSqlTemplate treeEntityViewSqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(thisVersion);
        ArgumentNullException.ThrowIfNull(dependencyLogicallyDeletableEntityViewSqlTemplate);
        ArgumentNullException.ThrowIfNull(treeEntityViewSqlTemplate);

        migrationBuilder.ApplyDependencyLogicallyDeletableEntityQueryView(
            thisVersion,
            previousVersion,
            isUp,
            dependencyLogicallyDeletableEntityViewSqlTemplate);

        migrationBuilder.ApplyTreeEntityQueryView(
            thisVersion,
            previousVersion,
            isUp,
            treeEntityViewSqlTemplate);

        return migrationBuilder;
    }

    /// <summary>
    /// 创建树形实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="entityType">实体类型</param>
    /// <param name="dependencyLogicallyDeletableEntityViewSqlTemplate">依赖项逻辑删除实体视图Sql模板</param>
    /// <param name="treeEntityViewSqlTemplate">树形实体视图Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder CreateEntityQueryView(
        this MigrationBuilder migrationBuilder,
        IEntityType entityType,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate dependencyLogicallyDeletableEntityViewSqlTemplate,
        ITreeEntityDatabaseViewSqlTemplate treeEntityViewSqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(entityType);
        ArgumentNullException.ThrowIfNull(dependencyLogicallyDeletableEntityViewSqlTemplate);
        ArgumentNullException.ThrowIfNull(treeEntityViewSqlTemplate);

        migrationBuilder.CreateDependencyLogicallyDeletableEntityQueryView(entityType, dependencyLogicallyDeletableEntityViewSqlTemplate);
        migrationBuilder.CreateTreeEntityQueryView(entityType, treeEntityViewSqlTemplate);

        return migrationBuilder;
    }

    /// <summary>
    /// 删除实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="entityType">实体类型</param>
    /// <param name="dependencyLogicallyDeletableEntityViewSqlTemplate">依赖项逻辑删除实体视图Sql模板</param>
    /// <param name="treeEntityViewSqlTemplate">树形实体视图Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder DropEntityQueryView(
        this MigrationBuilder migrationBuilder,
        IEntityType entityType,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate dependencyLogicallyDeletableEntityViewSqlTemplate,
        ITreeEntityDatabaseViewSqlTemplate treeEntityViewSqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(entityType);
        ArgumentNullException.ThrowIfNull(dependencyLogicallyDeletableEntityViewSqlTemplate);
        ArgumentNullException.ThrowIfNull(treeEntityViewSqlTemplate);

        return migrationBuilder.DropEntityQueryView(
            entityType.GetTableName()!,
            dependencyLogicallyDeletableEntityViewSqlTemplate,
            treeEntityViewSqlTemplate);
    }

    /// <summary>
    /// 删除实体查询视图
    /// </summary>
    /// <param name="migrationBuilder">迁移构造器</param>
    /// <param name="tableName">视图对应的表名</param>
    /// <param name="dependencyLogicallyDeletableEntityViewSqlTemplate">依赖项逻辑删除实体视图Sql模板</param>
    /// <param name="treeEntityViewSqlTemplate">树形实体视图Sql模板</param>
    /// <returns>迁移构造器</returns>
    public static MigrationBuilder DropEntityQueryView(
        this MigrationBuilder migrationBuilder,
        string tableName,
        IDependencyLogicallyDeletableEntityDatabaseViewSqlTemplate dependencyLogicallyDeletableEntityViewSqlTemplate,
        ITreeEntityDatabaseViewSqlTemplate treeEntityViewSqlTemplate)
    {
        ArgumentNullException.ThrowIfNull(migrationBuilder);
        ArgumentNullException.ThrowIfNull(dependencyLogicallyDeletableEntityViewSqlTemplate);
        ArgumentNullException.ThrowIfNull(treeEntityViewSqlTemplate);
        if (string.IsNullOrEmpty(tableName))
        {
            throw new ArgumentException($""{nameof(tableName)}"不能为 null 或空。", nameof(tableName));
        }

        migrationBuilder.DropDependencyLogicallyDeletableEntityQueryView(tableName, dependencyLogicallyDeletableEntityViewSqlTemplate);
        migrationBuilder.DropTreeEntityQueryView(tableName, treeEntityViewSqlTemplate);

        return migrationBuilder;
    }
}

迁移脚本预览(节选)

sql 复制代码
CREATE VIEW QueryView_Tree_Entity2_1s    --创建视图
AS
WITH [temp]([Id], [DeletedAt], [DependencyDeletedAt], [Entity1_1_1Id], [Entity2Id], [Entity2_0Id], [IsLeaf], [IsRoot], [ParentId], [Text2_1], [Depth], [Path], [HasChildren]) AS
(
    --初始查询(这里的 [ParentId] IS NULL 在数据中是最底层的根节点)
    SELECT [Id], [DeletedAt], [DependencyDeletedAt], [Entity1_1_1Id], [Entity2Id], [Entity2_0Id], [IsLeaf], [IsRoot], [ParentId], [Text2_1],
        0 AS [Depth],
        '/' + CAST([Id] AS nvarchar(max)) + '/' AS [Path], --如果Id使用Guid类型,可能会导致层数太深时出问题(大概100层左右,超过4000字之后的字符串会被砍掉,sqlserver 2005以后用 nvarchar(max)可以突破限制),Guid的字数太多了
        (CASE WHEN EXISTS(SELECT 1 FROM [QueryView_Entity2_1s] WHERE [QueryView_Entity2_1s].[ParentId] = [Root].[Id]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END) AS [HasChildren]
    FROM [QueryView_Entity2_1s] AS [Root]
    WHERE [Root].[ParentId] IS NULL

    UNION ALL
    --递归条件
    SELECT [Child].[Id], [Child].[DeletedAt], [Child].[DependencyDeletedAt], [Child].[Entity1_1_1Id], [Child].[Entity2Id], [Child].[Entity2_0Id], [Child].[IsLeaf], [Child].[IsRoot], [Child].[ParentId], [Child].[Text2_1],
        [Parent].[Depth] + 1,
        [Parent].[Path] + CAST([Child].[Id] AS nvarchar(max)) + '/' AS [Path],
        (CASE WHEN EXISTS(SELECT 1 FROM [QueryView_Entity2_1s] WHERE [QueryView_Entity2_1s].[ParentId] = [Child].[Id]) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END) AS [HasChildren]
    FROM [QueryView_Entity2_1s] AS [Child] --3:这里的临时表和原始数据表都必须使用别名不然递归的时候不知道查询的是哪个表的列
    INNER JOIN [temp] AS [Parent]
    ON ([Child].[ParentId] = [Parent].[Id]) --这个关联关系很重要,一定要理解一下谁是谁的父节点
)
--4:递归完成后 一定不要少了这句查询语句 否则会报错
SELECT *
FROM [temp];

MySql 8.0和Sqlite 3支持查询所需功能,其他数据库请自行验证。

Tips

开发测试时发现,如果用命令行工具会导致无法下断点单步调试迁移扩展,这一度让笔者很难受。经过一番折腾,发现可以使用以下代码在程序中调用迁移生成。

csharp 复制代码
var modelInitializer = appDbContext.GetService<IModelRuntimeInitializer>();
var migrationsAssembly = appDbContext.GetService<IMigrationsAssembly>();
var modelDiffer = appDbContext.GetService<IMigrationsModelDiffer>();
var migrator = appDbContext.GetService<IMigrator>();

var firstModel = modelInitializer.Initialize(migrationsAssembly.CreateMigration(migrationsAssembly.Migrations.First().Value, appDbContext.Database.ProviderName!).TargetModel);
var snapshotModel = modelInitializer.Initialize(migrationsAssembly.ModelSnapshot!.Model);

var differences = modelDiffer.GetDifferences(
    migrationsAssembly.ModelSnapshot!.Model.GetRelationalModel(),
    firstModel.GetRelationalModel());

var script = migrator.GenerateScript(migrationsAssembly.Migrations.LastOrDefault().Key, "0"/*, migrationsAssembly.Migrations.FirstOrDefault().Key*/);

结语

经过3篇系列文,一个仅依赖EF Core,对业务代码0入侵,完全确保数据完整性的全自动审计、软删除和树形查询表就大功告成了!

本系列文的所需代码从构思到测试基本可用前后过了将近一个月,基本上可以说已经成为了项目这碟醋包了这个系列的一盘饺子了。包括之前的基于EF Core存储的国际化服务基于EF Core存储的Serilog持久化服务其实也是项目的一部分。不过经过这一系列折腾,以后可以直接拿来用了,也不亏。

示例代码:SoftDeleteDemo.rar。主页显示异常请在libman.json上右键恢复前端包。

QQ群

读者交流QQ群:540719365

欢迎读者和广大朋友一起交流,如发现本书错误也欢迎通过博客园、QQ群等方式告知笔者。

本文地址:https://www.cnblogs.com/coredx/p/18305284.html

相关推荐
数据的世界016 小时前
.NET开发人员学习书籍推荐
学习·.net
paixiaoxin8 小时前
CV-OCR经典论文解读|An Empirical Study of Scaling Law for OCR/OCR 缩放定律的实证研究
人工智能·深度学习·机器学习·生成对抗网络·计算机视觉·ocr·.net
19004317 小时前
.NET重点
.net
m0_6632340117 小时前
在 .NET 5.0 运行 .NET 8.0 教程:使用 ASP.NET Core 创建 Web API
前端·asp.net·.net
小码编匠1 天前
.NET 下 RabbitMQ 队列、死信队列、延时队列及小应用
后端·c#·.net
真想骂*2 天前
vmime.net_4.dll详解:它是什么,有何用途?
.net
lzhdim2 天前
2、C#基于.net framework的应用开发实战编程 - 设计(二、二) - 编程手把手系列文章...
开发语言·c#·.net
工业3D_大熊2 天前
HOOPS Communicator功能剖析:3D Web模型树交互的实用指南!
linux·windows·macos·3d·docker·c#·.net
小码编匠3 天前
C# 使用心跳机制实现TCP客户端自动重连
后端·c#·.net