如何使用 Entity Framework 核心 运行 迁移 SQL 脚本

How to run migration SQL script using Entity Framework Core

我遇到了一个问题,我无法访问 SQL 脚本来应用迁移。 这是我的迁移代码:

 public partial class AddSomethingMigration : Migration
{
    private const string MIGRATION_SQL_SCRIPT_FILE_NAME = @"Migrations\Scripts170710123314_AddSomethingMigration.sql";

    protected override void Up(MigrationBuilder migrationBuilder)
    {
        string sql = Path.Combine(Directory.GetParent(Directory.GetCurrentDirectory()).FullName, MIGRATION_SQL_SCRIPT_FILE_NAME));
        migrationBuilder.Sql(File.ReadAllText(sql));
    }
}

因此,当我在本地计算机上使用程序包管理器控制台时,一切正常。但是当我部署到环境时,我发现文件不一致。

我是否可以通过 EF 迁移自动 运行 我的静态 SQL 脚本,或者我应该在代码中内联粘贴 SQL 查询?

我找到了这个问题的几个答案。

  1. 将脚本添加为项目资源并像这样使用它:

    string sql = Resources._20170630085940_AddMigration;
    migrationBuilder.Sql(sql);
    

这个选项不太好,因为 .sql 将嵌入到程序集中。

  1. 如果您使用.csproj结构的Net Core项目,您可以将项目组添加到xml:

    <ItemGroup> <Content Include="Migrations\**\*.sql" CopyToPublishDirectory="PreserveNewest" /><!-- CopyToPublishDirectory = { Always, PreserveNewest, Never } --></ItemGroup>
    

然后指定文件路径,如:

Path.Combine(AppContext.BaseDirectory, relativePath)

我喜欢做的是将SQL脚本作为资源嵌入到程序集中,使程序集不依赖于任何外部文件。我已经使用 Visual Studio Community 2019 16.4.2 测试了这种方法。 在我的例子中,DbContext 保存在 .NET Standard 2.0 库中,我的 Web 应用程序是 运行 .NET Core 2.2.

首先你需要创建一个迁移文件:

  1. 在 Visual Studio 中确保将 Web 应用程序设置为启动项目。
  2. 在 Visual Studio 中打开 PMC:查看 -> 其他 Windows -> 程序包管理器控制台 (PMC)
  3. 在 PMC 中将默认项目设置为包含 DbContext 的项目(在我的例子中是 .NET 标准 2.2 库)
  4. 添加新迁移:

    Add-Migration RunSqlScript

在迁移文件夹中添加一个Sql脚本(为了方便,我将其命名为与迁移文件相同的前缀)

在文件属性中 window 确保构建操作是 "Embedded Resource" 请注意,我们不需要复制到输出文件夹,因为 sql 脚本将嵌入到程序集中。

更新 RunSqlScript 迁移中的 Up 方法

var assembly = Assembly.GetExecutingAssembly();
string resourceName = typeof(RunSqlScript).Namespace + ".20191220105024_RunSqlScript.sql";
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
{
  using (StreamReader reader = new StreamReader(stream))
  {
    string sqlResult = reader.ReadToEnd();
    migrationBuilder.Sql(sqlResult);
  }
}

在我的应用程序中,我将这段代码重新分解为一个实用方法。为了简洁起见,我发布时没有进行这种重构。

更新:

我上面提到的重构代码:

public static class MigrationUtility
{
  /// <summary>
  /// Read a SQL script that is embedded into a resource.
  /// </summary>
  /// <param name="migrationType">The migration type the SQL file script is attached to.</param>
  /// <param name="sqlFileName">The embedded SQL file name.</param>
  /// <returns>The content of the SQL file.</returns>
  public static string ReadSql(Type migrationType, string sqlFileName)
  {
    var assembly = migrationType.Assembly;
    string resourceName = $"{migrationType.Namespace}.{sqlFileName}";
    using (Stream stream = assembly.GetManifestResourceStream(resourceName))
    {
      if (stream == null)
      {
        throw new FileNotFoundException("Unable to find the SQL file from an embedded resource", resourceName);
      }

      using (var reader = new StreamReader(stream))
      {
        string content = reader.ReadToEnd();
        return content;
      }
    }
  }
}

用法示例:

string sql = MigrationUtility.ReadSql(typeof(RunSqlScript), "20191220105024_RunSqlScript.sql");
migrationBuilder.Sql(sql);

这是使用 EmbeddedResource 的方法升级。主要思想是使用抽象 class 和一个与迁移同名的 sql 文件。

public abstract class SqlMigration : Migration
{
    protected sealed override void Up(MigrationBuilder migrationBuilder)
    {
        var assembly = Assembly.GetExecutingAssembly();
        var type = GetType();
        var regex = new Regex($@"{Regex.Escape(type.Namespace)}\.\d{{14}}_{Regex.Escape(type.Name)}\.sql");

        var resourceName = assembly.GetManifestResourceNames().FirstOrDefault(x => regex.IsMatch(x));
        using var stream = assembly.GetManifestResourceStream(resourceName);
        using var reader = new StreamReader(stream);
        var sqlResult = reader.ReadToEnd();
        migrationBuilder.Sql(sqlResult);
    }
}

正则表达式只使用真实类型的名称和命名空间。 继承 class 看起来像:

public partial class RunSqlScript : SqlMigration
{
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // Down code here
    }
}

项目将类似于:

.

我根据 的回答创建了一个扩展方法。这假设您的文件以 .sql 扩展名结尾,并且它是您 运行 从中迁移的任何项目中的嵌入式资源。您当然可以将 .sql 部分移动到迁移的 Up 中,但这对我来说似乎更干净。

public static class MigrationExtensions
{
    public static void RunSqlScript(this MigrationBuilder migrationBuilder, string script)
    {
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = assembly.GetManifestResourceNames().FirstOrDefault(x => x.EndsWith($"{script}.sql"));
        using var stream = assembly.GetManifestResourceStream(resourceName);
        using var reader = new StreamReader(stream);
        var sqlResult = reader.ReadToEnd();
        migrationBuilder.Sql(sqlResult);
    }
}

用作

public partial class AddViews : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.RunSqlScript("nameOfMyFile");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {

    }
}

我发现根据提供的 MigrationAttribute 评估 sql 文件名是最好的方法。

  public class EmbeddedSqlFileMigration : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            var assembly = Assembly.GetExecutingAssembly();
            var type = GetType();
            var migrationAttribute = type.GetCustomAttribute<MigrationAttribute>();
            if (migrationAttribute == null)
                throw new InvalidOperationException("A migration requires a MigrationAttribute.");

            var sqlResourceFilename = $"{type.Namespace}.{migrationAttribute.Id}.sql";
            var resourceName = assembly.GetManifestResourceNames().FirstOrDefault(r => r == sqlResourceFilename);
            if (resourceName == null)
            {
                throw new FileNotFoundException(
                    $"Embedded resource '{sqlResourceFilename}' was not found in assembly '{assembly.FullName}'.");
            }

            using var stream = assembly.GetManifestResourceStream(resourceName);
            if (stream == null)
            {
                throw new InvalidOperationException(
                    $"Unable to get stream for embedded resource '{sqlResourceFilename}' in assembly '{assembly.FullName}'.");
            }

            using var reader = new StreamReader(stream);
            var sqlResult = reader.ReadToEnd();
            migrationBuilder.Sql(sqlResult);
        }
    }

我和我的技术主管试图弄清楚为什么我还没有找到插入语句的简单答案。双方都不对对方感到沮丧,都对我们 运行 缺乏简单性感到沮丧。

我们发现:

  1. https://www.learnentityframeworkcore.com/raw-sql
  2. https://mycodingtips.com/2021/9/20/how-to-run-sql-scripts-in-a-file-using-ef-core-migrations 3.https://www.codeproject.com/Articles/1173837/BulkInsert-with-the-Entity-Framework
  3. https://www.yogihosting.com/insert-records-entity-framework-core/

一切都有效或一个好的开始,NONE就这么简单:

  1. 生成所需的插入语句
  2. 在每个之间使用 GO 分隔符
  3. 在控制台项目中解析文件,最多计算 500 个 Go 分隔符以生成 500 条语句的块
  4. 运行 每个块制作每个块的字符串并使用 DbSet.FromRawSQL(插入块)

我会在完成后编写方法和 post 代码。