【问题标题】:How does one override the default SQL migration generator?如何覆盖默认的 SQL 迁移生成器?
【发布时间】:2020-04-17 01:56:52
【问题描述】:

我正在尝试覆盖 SQL 迁移生成器的默认行为,以便我可以指定自定义外键约束名称,如 here 所述。我已经按照建议连接了配置。

不幸的是,进展并不顺利。

一个快速的日志语句显示GetFkName() 函数从未被命中。

我尝试了另一种配置结构,如 herehere 所讨论的那样,但是当我尝试生成迁移时出现此错误:

在程序集“ConsoleApp1”中发现了多个迁移配置类型。指定要使用的名称。

我觉得这个结果有点奇怪,因为我只有一个配置类、一个 SQL 生成类和一个上下文类(下面的代码没有反映这一点,但我在实际测试中注释掉了额外内容)。在命令行指定配置类型,如here所示,错误如下:

System.InvalidOperationException:“ConsoleApp1.Db.CustomDbConfiguration2”类型不继承自“System.Data.Entity.DbConfiguration”。基于实体框架代码的配置类必须继承自“System.Data.Entity.DbConfiguration”。

所有这一切都让我们回到了here,然后,由于上述原因,这不起作用(GetFkName() 永远不会被击中)。所以看来我在追我的尾巴(直到今天才知道我有一个)。

我应该怎么做才能让这个覆盖正常工作?


配置

Imports System.Data.Entity
Imports System.Data.Entity.Migrations
Imports System.Data.Entity.SqlServer

Namespace Db
  Friend Class CustomDbConfiguration
    Inherits DbConfiguration

    Public Sub New()
      Me.SetMigrationSqlGenerator(SqlProviderServices.ProviderInvariantName, Function() New CustomSqlGenerator)
    End Sub
  End Class

  Friend Class CustomDbConfiguration2
    Inherits DbMigrationsConfiguration(Of Context)

    Public Sub New()
      Me.SetSqlGenerator(SqlProviderServices.ProviderInvariantName, New CustomSqlGenerator2(Me.GetSqlGenerator(SqlProviderServices.ProviderInvariantName)))
      Me.ContextType = GetType(Context)
    End Sub
  End Class
End Namespace

SQL 生成器

Imports System.Data.Entity.Migrations.Model
Imports System.Data.Entity.Migrations.Sql
Imports System.Data.Entity.SqlServer

Namespace Db
  Friend Class CustomSqlGenerator
    Inherits SqlServerMigrationSqlGenerator

    Protected Overrides Sub Generate(AddForeignKeyOperation As AddForeignKeyOperation)
      AddForeignKeyOperation.Name = GetFkName(AddForeignKeyOperation.PrincipalTable, AddForeignKeyOperation.DependentTable, AddForeignKeyOperation.DependentColumns.ToArray())
      MyBase.Generate(AddForeignKeyOperation)
    End Sub

    Protected Overrides Sub Generate(DropForeignKeyOperation As DropForeignKeyOperation)
      DropForeignKeyOperation.Name = GetFkName(DropForeignKeyOperation.PrincipalTable, DropForeignKeyOperation.DependentTable, DropForeignKeyOperation.DependentColumns.ToArray())
      MyBase.Generate(DropForeignKeyOperation)
    End Sub

    Private Shared Function GetFkName(PrimaryKeyTable As String, ForeignKeyTable As String, ParamArray ForeignTableFields As String()) As String
      IO.File.WriteAllText("D:\Logs\FkNameTest.log", $"{Now.ToString}{vbCrLf}")

      Return $"FK_{ForeignKeyTable}_{PrimaryKeyTable}"
    End Function
  End Class

  Friend Class CustomSqlGenerator2
    Inherits MigrationSqlGenerator

    Public Sub New(Generator As MigrationSqlGenerator)
      Me.Generator = Generator
    End Sub

    Public Overrides Function Generate(MigrationOperations As IEnumerable(Of MigrationOperation), ProviderManifestToken As String) As IEnumerable(Of MigrationStatement)
      Return Me.Generator.Generate(MigrationOperations, ProviderManifestToken)
    End Function

    Private ReadOnly Generator As MigrationSqlGenerator
  End Class
End Namespace

上下文

Imports System.Data.Common
Imports System.Data.Entity
Imports System.Data.SqlClient
Imports System.Reflection

Namespace Db
  <DbConfigurationType(GetType(CustomDbConfiguration2))>
  Friend Class Context
    Inherits DbContext

    Public Sub New()
      MyBase.New(DbConnection.ConnectionString)
    End Sub

    Private Sub New(Connection As DbConnection)
      MyBase.New(Connection, True)

      Database.SetInitializer(New CreateDatabaseIfNotExists(Of Context))
      Database.SetInitializer(New MigrateDatabaseToLatestVersion(Of Context, Migrations.Configuration))

      Me.Database.Initialize(False)
    End Sub

    Public Shared Function Create() As Context
      Return New Context(DbConnection)
    End Function

    Private Shared ReadOnly Property DbConnection As SqlConnection
      Get
        Return New SqlConnection(Utils.DbConnectionString)
      End Get
    End Property

    Protected Overrides Sub OnModelCreating(Builder As DbModelBuilder)
      Builder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly)
      MyBase.OnModelCreating(Builder)
    End Sub

    Public Property Documents As DbSet(Of Document)
    Public Property Sections As DbSet(Of Section)
  End Class
End Namespace

【问题讨论】:

  • 迁移生成后简单修改会更容易。
  • @DavidBrowne-Microsoft 〜谢谢,但如果有几百个'em ;-)
  • @DavidBrowne-Microsoft ~ 你知道怎么做吗?
  • 我从来没有这样做过,我也不会在代码优先中尝试这样做。如果您关心外键名称,您可能应该直接在数据库中维护您的架构,然后从中逆向工程 EF 模型。
  • @DavidBrowne-Microsoft ~ 明白了,谢谢。

标签: vb.net entity-framework-6 ef-code-first entity-framework-migrations


【解决方案1】:

免责声明:我已经很多年没有用 VB 编码了,这些代码示例是我将 C# 中的工作示例转换为 OP 原生 VB 的微弱尝试。请随时更新我的​​语法;)

您可以手动编辑迁移脚本,为每个ForeignKey 指定一个自定义名称,方法是在创建过程中为Tablebuilder.ForeignKey 调用中的可选Name 参数指定一个值表语句:

CreateTable(
  "dbo.CorporationVariety",
  Function(c) New With
    {
      .Id = c.Int(nullable: false, identity:= true),
      .CorporationId = c.Int(nullable:= false),
      .VarietyId = c.Int(nullable:= false),
    }) _
  .PrimaryKey(Function(t) t.Id)
  .ForeignKey("dbo.Corporation", Function(t) t.CorporationId, name := "FKCorporatationCorporationVarietyCorporationId")
  .ForeignKey("dbo.Variety", Function(t) t.VarietyId, name := "FKVarietyCorporationVarietyVarietyId")
  .Index(Function(t) t.CorporationId)
  .Index(Function(t) t.VarietyId)

或作为DbMigration.AddForeignKey 声明的一部分:

AddForeignKey("dbo.CorporationVariety", "CorporationId", "dbo.Corporation", name := "FKCorporatationCorporationVarietyCorporationId")
AddForeignKey("dbo.CorporationVariety", "VarietyId", "dbo.Variety", name := "FKVarietyCorporationVarietyVarietyId")

如果你的模型中有很多键,并且你想实现一个特定的约定,(就像你想在给定的规则或代码序列中应用的那样场景),那么通常首先要寻找解决方案的地方是EF Code First Conventions

不幸的是,这里既没有标准约定可以为您提供帮助,也无法使用流畅的表示法为外键定义自定义名称...

通常我们会继续创建一个Custom Code First Convention 来定义您的自定义逻辑,这通常有两种方式:

  1. 您的约定通过 Fluent Notation 执行标准配置
    • 我们已经注意到此选项对我们不可用...
  2. 您的约定逻辑通过注释将自定义元数据存储到模型中

PrimaryForeign 键似乎是 EF Code First Runtime 中的异常情况,似乎没有一种方法可以轻松地从 @987654331 访问注释@ 尽管它们相对容易定义。

我很惊讶地发现这一点并偶然发现了这篇进一步证实了这一点的帖子:https://stackoverflow.com/a/54369685/1690217

更新我开始这篇文章时假设 Conventions 是正确的方法,因为我将它用于多年来我需要应用的许多其他自定义.如果您希望实现其他类似类型的自定义,请先查看约定

我们仍然可以轻松地覆盖生成迁移代码文件的标准VisualBasicMigrationCodeGenerator,所以让我们直接跳进去。 coapply 为您的 ForeignKey 自定义名称,然后实现自定义 MigrationCodeGenerator 来处理您的约定的输出。

  1. 创建自定义VisualBasicMigrationCodeGenerator
  2. 注册 代码生成器,以便 EF 使用它来生成下一次迁移

注意:这不会强制重命名数据库中的现有键。为此,您需要强制删除每个密钥并重新添加回来。对于大型模型,一旦上述步骤到位,请考虑使用 T4 模板创建自定义一次性迁移逻辑来实现此目的。

首先将您的自定义VisualBasicMigrationCodeGenerator 视为您的个人EF 代码酸面团文化,您可以分享它并将其重新用于每个新应用程序,添加新的每次迭代的功能和改进。但是 Conventions 是您可能不希望在每个项目中使用的配置选项,(这就是为什么使用 _Conventions_ for OPs 解决方案是我的第一个方向。)

1。创建自定义 VisualBasicMigrationCodeGenerator

创建一个继承自 EF 的新类 VisualBasicMigrationCodeGenerator,我们需要做的最少的事情是覆盖 AddForeignKeyOperation 并修改键的 Name 并调用基础实施。这将影响添加到模型中的所有新键。

要作为 CreateTable 的一部分添加的目标键,我们必须重写 GenerateInline(AddForeignKeyOperation...),但是基本实现(在 C# 生成器中...)不遵守自定义 名称 所以我们必须完全替换实现。

  • 执行此操作时,请转到 EF project on GitHub 并从原始实现开始,然后根据需要注入您的自定义设置。

请原谅这个 C#,我没有时间翻译它,但它确实生成了正确的 VB 代码;)

public class CustomVBMigrationCodeGenerator : System.Data.Entity.Migrations.Design.VisualBasicMigrationCodeGenerator
{

    protected override void Generate(AddForeignKeyOperation addForeignKeyOperation, IndentedTextWriter writer)
    {
        ApplyCustomFKName(addForeignKeyOperation);
        base.Generate(addForeignKeyOperation, writer);
    }

    private void ApplyCustomFKName(ForeignKeyOperation operation)
    {
        // expecting FK without scheme or underscores: "FK{DependentTable}{PrincipalTable}{FKField}"
        operation.Name = $"FK{StripSchemeFromName(operation.DependentTable)}{StripSchemeFromName(operation.PrincipalTable)}{String.Join("", operation.DependentColumns)}";
    }
    private string StripSchemeFromName(string dbObjectName)
    {
        return dbObjectName.Split(new[] { '.' }, 2).Last();
    }

    /// <summary>
    /// Generates code to perform an <see cref="AddForeignKeyOperation" /> as part of a <see cref="CreateTableOperation" />.
    /// </summary>
    /// <param name="addForeignKeyOperation"> The operation to generate code for. </param>
    /// <param name="writer"> Text writer to add the generated code to. </param>
    protected virtual void GenerateInline(AddForeignKeyOperation addForeignKeyOperation, IndentedTextWriter writer)
    {
        // sourced from https://github.com/aspnet/EntityFramework6/blob/master/src/EntityFramework/Migrations/Design/VisualBasicMigrationCodeGenerator.cs
        Check.NotNull(addForeignKeyOperation, "addForeignKeyOperation");
        Check.NotNull(writer, "writer");

        writer.WriteLine(" _");
        writer.Write(".ForeignKey(" + Quote(addForeignKeyOperation.PrincipalTable) + ", ");
        Generate(addForeignKeyOperation.DependentColumns, writer);

        // Our Custom logic
        ApplyCustomFKName(addForeignKeyOperation);

        // Insert our custom name if provided
        if (!addForeignKeyOperation.HasDefaultName)
        {
            writer.Write(", name := " + Quote(addForeignKeyOperation.Name));
        }

        if (addForeignKeyOperation.CascadeDelete)
        {
            writer.Write(", cascadeDelete := True");
        }

        writer.Write(")");
    }
}

2。注册代码生成器,以便 EF 使用它来生成下一次迁移

在您的项目中找到 Configuration.vb,在构造函数中将 CodeGenerator 设置为您的 CustomVBMigrationCodeGenerator 的实例:

Public Sub New()

  AutomaticMigrationsEnabled = false
  CodeGenerator = new CustomVBMigrationCodeGenerator()

End Sub

现在执行add-migration 以生成新的迁移,您将看到迁移脚本中定义的新自定义名称。

如果您需要从此配置降级_或_如果 alter table 命令要求首先删除密钥,您可能也需要类似地覆盖 Generate(DropForeignKeyOperation...) 方法。

【讨论】:

  • 是的,这是史诗 :-) 仅供参考,我真的真的喜欢在生成的迁移中这样做,而不是在生成的 SQL 中。不错的触感。 (顺便说一句,不用担心 VB/C#。任何一个都适合我。你的转换看起来不错。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多