简体   繁体   中英

IDENTITY_INSERT for multiple tables in .net core 2

I'm trying to migrate data from an existing database to a new one. The old database is very entangled, meaning most tables have relationships to many other tables based on foreign ids. I came across this solution for inserting ids:

using (var context = new EmployeeContext())
{
    context.Employees.Add(new Employee { EmployeeId = 100, Name = "John Doe" });
    context.Employees.Add(new Employee { EmployeeId = 101, Name = "Jane Doe" });

    context.Database.OpenConnection();
    try
    {
        context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT dbo.Employees ON");
        context.SaveChanges();
        context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT dbo.Employees OFF");
    }
    finally
    {
        context.Database.CloseConnection();
    }


    foreach (var employee in context.Employees)
    {
        Console.WriteLine(employee.EmployeeId + ": " + employee.Name);
    }
}

from this Microsoft guide: https://docs.microsoft.com/en-us/ef/core/saving/explicit-values-generated-properties

Is there a way to set IDENTITY_INSERT on multiple tables before applying context.SaveChanges(); ?

Nope. Look at the documentation of IDENTITY_INSERT. https://docs.microsoft.com/en-us/sql/t-sql/statements/set-identity-insert-transact-sql

It clearly states:

At any time, only one table in a session can have the IDENTITY_INSERT property set to ON. If a table already has this property set to ON, and a SET IDENTITY_INSERT ON statement is issued for another table, SQL Server returns an error message that states SET IDENTITY_INSERT is already ON and reports the table it is set ON for.

I had the same Problem here while seeding data from an object tree stored in a json file.

Example:

jsonData = System.IO.File.ReadAllText(@"Data\InputParameters.json");
var inputParameters = JsonConvert.DeserializeObject<List<ParameterCategory>> jsonData, settings);
context.AddRange(inputParameters);
context.SaveChanges();

After a look at the EFCore sources I came up with the following solution:

1. Create a new class "SqlServerUpdateSqlGeneratorInsertIdentity" which is responsible for turning Identity_Insert on and off for each insert Operation:

using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.EntityFrameworkCore.SqlServer.Update.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Update;

/// <summary>
/// SqlServerUpdateSqlGenerator with Insert_Identity.
/// </summary>
public class SqlServerUpdateSqlGeneratorInsertIdentity : SqlServerUpdateSqlGenerator
{
    /// <summary>
    /// Initializes a new instance of the <see cref="SqlServerUpdateSqlGeneratorInsertIdentity"/> class.
    /// </summary>
    /// <param name="dependencies">The dependencies.</param>
    public SqlServerUpdateSqlGeneratorInsertIdentity(UpdateSqlGeneratorDependencies dependencies)
        : base(dependencies)
    {
    }

    /// <inheritdoc/>
    public override ResultSetMapping AppendBulkInsertOperation(
        StringBuilder commandStringBuilder,
        IReadOnlyList<ModificationCommand> modificationCommands,
        int commandPosition)
    {
        var columns = modificationCommands[0].ColumnModifications.Where(o => o.IsWrite).Select(o => o.ColumnName)
            .ToList();
        var schema = modificationCommands[0].Schema;
        var table = modificationCommands[0].TableName;

        GenerateIdentityInsert(commandStringBuilder, table, schema, columns, on: true);
        var result = base.AppendBulkInsertOperation(commandStringBuilder, modificationCommands, commandPosition);
        GenerateIdentityInsert(commandStringBuilder, table, schema, columns, on: false);

        return result;
    }

    private void GenerateIdentityInsert(
        StringBuilder builder,
        string table,
        string schema,
        IEnumerable<string> columns,
        bool on)
    {
        var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));

        builder.Append("IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE").Append(" [name] IN (")
            .Append(string.Join(", ", columns.Select(stringTypeMapping.GenerateSqlLiteral)))
            .Append(") AND [object_id] = OBJECT_ID(").Append(
                stringTypeMapping.GenerateSqlLiteral(
                    Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema))).AppendLine("))");

        builder.Append("SET IDENTITY_INSERT ")
            .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)).Append(on ? " ON" : " OFF")
            .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
    }
}

2. Replace the original "SqlServerUpdateSqlGenerator" by the inherited new one:

In Startup.cs - ConfigureServices use the following code:

services.AddDbContext<YourDataContext>(options =>
{
    options.UseSqlServer(YourConnectionString);
    options.ReplaceService<ISqlServerUpdateSqlGenerator, SqlServerUpdateSqlGeneratorInsertIdentity>();
});

Or in YourDataContext.cs - OnConfiguring use this one (not tested):

options.ReplaceService<ISqlServerUpdateSqlGenerator, SqlServerUpdateSqlGeneratorInsertIdentity>();

It may be necessary to reset the service configuration to it´s original after seeding. In my case it wasn´t.

Hope that´ll help someone...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM