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

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


 public partial class AddSomethingMigration : Migration
{
    private const string MIGRATION_SQL_SCRIPT_FILE_NAME = @"Migrations\Scripts\20170710123314_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查询嵌入到代码中?


请问我们能否使用Code First Migrations运行SQL脚本? - Steve Greene
谢谢你的回答,Steve。但是对我来说不起作用。因为我的本地机器和托管之间有不同的路径。 - shkapo
问题Entity Framework Code-First:如何手动更新数据库?可能是相关的。 - Michael Freidgeim
8个回答

32

我发现了这个问题的几个答案。

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

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

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

  1. 如果您使用带有.csproj结构的Net Core项目,您可以在xml中添加itemgroup:

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

然后像这样指定文件路径:

Path.Combine(AppContext.BaseDirectory, relativePath)

“<!-- CopyToPublishDirectory = { Always, PreserveNewest, Never } -->” 代码有什么用途? - Jeroen
1
这些是您在CopyToPublishDirectory中拥有的选项吗? - Jeroen
1
@shkapo,如果我理解正确,方法1是将SQL查询放入resx文件中。首先,我不喜欢这样做,因为.resx文件不容易编辑:我更喜欢在Visual Studio中有一个.sql文件。其次,我不明白为什么你说将SQL嵌入程序集是不好的。我更喜欢程序集不依赖于SQL文件,你能详细说明一下吗?请查看我提交的答案,让我知道你的想法。 - Yanal-Yves Fargialla
@Yanal-YvesFargialla 你需要将该文件作为项目资源添加。因此,它仍然是一个独立的.sql文件,然后你将该文件添加为一个资源。 - Darth Scitus

19
我喜欢将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:View->Other Windows->Package Manager Console (PMC)
  3. 在PMC中将默认项目设置为保存DbContext的项目(在我的情况下是.NET标准2.2库)
  4. 添加新的迁移:

    Add-Migration RunSqlScript

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

Migration folder in the Solution Explorer

在文件属性窗口中,确保生成操作为"嵌入的资源"。请注意,我们不需要复制到输出文件夹,因为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);

谢谢您提供的这个示例。不过,您的代码片段与屏幕截图并不一致: string resourceName = typeof(RunSqlScript).Namespace + ".20191220105024_RunSqlScript.sql"; - Umar3x
@Umar3x 好发现!我最初回答了这个SO:https://dev59.com/questions/f1wY5IYBdhLWcg3w36-G#48539230,针对EF 4。当我回答EF core的等效问题时,在发布之前我在VS 2019 16.4.2上进行了测试并复制/粘贴了实际代码。但是,我很懒,重复使用了我的初始答案的屏幕截图。 - Yanal-Yves Fargialla
1
我真的很喜欢这种方法,我想看看你重构后的方法。 - c0y0teX
1
@c0y0teX,我已经更新了我的答案,并进行了重构代码。希望这可以帮到你。 - Yanal-Yves Fargialla
非常感谢您的更新。 - c0y0teX

9
您可以先在同一项目中创建一个辅助方法:
public static class SqlFileTrigger
{
    public static string GetRawSql(string sqlFileName)
    {
        var baseDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Migrations");
        var path = Path.Combine(baseDirectory, sqlFileName);
        return File.ReadAllText(path);
    }
}

接下来添加你的迁移。假设你添加了以下内容:add-migration AddDefaultUser,然后生成结果如下:

enter image description here

现在添加2个SQL文件(包括要运行的SQL语句,例如插入记录等),并使用相同的名称,并在其后加上_Up和_Down的后缀。它将是这样的:

enter image description here

然后在您的迁移文件UP和DOWN方法中,使用MigrationBuilder对象调用它们。因此,您的迁移文件将如下所示:

public partial class AddDefaultUser : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    { 
        migrationBuilder.Sql(SqlFileTrigger.GetRawSql("20220918043843_AddDefaultUser_Up.sql")); 
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {  
        migrationBuilder.Sql(SqlFileTrigger.GetRawSql("20220918043843_AddDefaultUser_Down.sql"));
    }
}

PS:请确保将您的SQL文件属性设置为COPY ALWAYS,以确保它们被部署。

enter image description here

希望对您有所帮助。


什么是“SqlFileTrigger”,VS找不到适合这个名称的类命名空间。 - EgoPingvina
1
@EgoPingvina 这只是静态类的名称,其中定义了 GetRawSql 方法。您可以根据自己的需要进行命名。创建一个名为 SqlFileTrigger 的静态类,并将 GetRawSql 方法放入其中。我已经为您更新了答案。 - curiousBoy
要在.csproj文件中直接包含文件,可以使用以下代码: PreserveNewest - Andy Raddatz

4

这是一个使用EmbeddedResource升级的方法。主要思路是使用抽象类和一个与迁移同名的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);
    }
}

它只是使用实际类型的名称和命名空间来进行正则表达式匹配。继承类将看起来像:

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

项目将与以下类似:

enter image description here.


2

我基于 4lexKislitsyn 的答案创建了一个扩展方法。这假定你的文件以 .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)
    {

    }
}

1
我发现从提供的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

根据@shkapo的被接受的答案,我添加了以下代码到我的.csproj文件中

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

并且我自己编写了扩展方法:

using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using System.Diagnostics;

namespace MigrationExtensions;

public static class MigrationExtensions
{
    public enum MigrationDirection
    {
        Up,
        Down
    }

    /// <summary>
    /// Execute a .sql file on the a EF Migration
    /// </summary>
    /// <param name="direction">Optional parameter, it add a .Up or a .Down at the end of the file name Ex.: "20221227004545_Initial.Up.sql"</param>
    /// <param name="fileName">Optional parameter, if not informed get the name of the caller class as name of the Sql file.</param>
    /// <param name="filesPath">Change the relative path where the sql files will be looked for</param>
    /// <param name="onWrongFilesPathThrowException">true: throw a exception if the file is not found, false: try to find sql file by the file name</param>
    /// <returns></returns>
    /// <exception cref="FileNotFoundException"></exception>
    public static OperationBuilder<SqlOperation> ExecuteSqlFile(
        this MigrationBuilder migrationBuilder,
             MigrationDirection? direction = null,
             string? fileName = null,
             string filesPath = "Migrations/SqlFiles",
             bool onWrongFilesPathThrowException = true)
    {
        if (fileName == null)
        {
            //Get stack to get the name of the calling Migration
            var frame = new StackFrame(1);
            string className = frame.GetMethod()!.DeclaringType!.Name;
            fileName = $"{className}{(direction != null ? $".{direction}" : "")}.sql";
        }
        else if (!Path.HasExtension(fileName))
        {
            fileName = $"{fileName}.sql";
        }

        string fileFullPath = Path.Combine(AppContext.BaseDirectory, filesPath, fileName);
        if (!File.Exists(fileFullPath))
        {
            string? alternativePath = Directory.EnumerateFiles(path: AppContext.BaseDirectory, fileName, SearchOption.AllDirectories).FirstOrDefault();
            if (alternativePath != null)
            {
                fileFullPath = onWrongFilesPathThrowException
                    ? throw new FileNotFoundException($"\"{Path.Combine(filesPath, fileName)}\" does not exists. There a file with the same name in {alternativePath.Replace(AppContext.BaseDirectory, "")}", fileFullPath)
                    : alternativePath;
            }
            else
                throw new FileNotFoundException($"\"{fileName}\" was not found in any directory.", fileFullPath);
        }

        return migrationBuilder.Sql(fileFullPath);
    }
}

只需调用migrationBuilder.ExecuteSqlFile();即可。


-1

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

我们发现:

  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/

所有的方法都是有效或良好的起点,但没有一个像这样简单:

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

我将写出具体步骤并在完成后发布代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接