Проблема с блокировкой при попытке изменить данные столбца при миграции вверх

Я пытаюсь добавить оператор sql к методу миграции для моего текущего проекта. База данных представляет собой базу данных Ms Access. Миграции применяются во время выполнения. Ситуация следующая: у меня есть базовая Initial-create миграция, которая в моем случае считается уже примененной. Из-за характера этого приложения у нас есть таблица A, которая содержит какой-то внешний ключ, но без каких-либо ограничений sql. Это означает, что отношение внешнего ключа разработано с помощью программного кода, а не в sql означает отношение внешнего ключа. Ключ представляет собой строку, и если нет внешнего элемента, значение пусто. Теперь мы хотим добавить новую миграцию, которая применяет эту связь через sql-ограничения. Это прекрасно работает с помощью стандартного кода миграции ef-core, но проблема возникает, когда миграция применяется к непустой базе данных. Для внешнего ключа sql все пустые строки в таблице A должны быть нулевыми (в противном случае мы получим исключение)

Казалось бы, простым решением было добавить следующий оператор в метод up новой миграции:

UPDATE A SET ForeignKeyColumn = NULL WHERE ForeignKeyColumn & \"\" = \"\""

Но это приводит к следующему исключению:

System.Data.OleDb.OleDbException (0x80040E14): The database engine could not lock table 'A' because it is already in use by another person or process.
   at System.Data.OleDb.OleDbCommand.ExecuteCommandTextErrorHandling(OleDbHResult hr)
   at System.Data.OleDb.OleDbCommand.ExecuteCommandTextForSingleResult(tagDBPARAMS dbParams, Object& executeResult)
   at System.Data.OleDb.OleDbCommand.ExecuteCommandText(Object& executeResult)
   at System.Data.OleDb.OleDbCommand.ExecuteCommand(CommandBehavior behavior, Object& executeResult)
   at System.Data.OleDb.OleDbCommand.ExecuteReaderInternal(CommandBehavior behavior, String method)
   at System.Data.OleDb.OleDbCommand.ExecuteNonQuery()
   at EntityFrameworkCore.Jet.Data.JetCommand.ExecuteNonQueryCore()
   at EntityFrameworkCore.Jet.Data.JetCommand.<>c.<ExecuteNonQuery>b__40_0(Int32 _, JetCommand command)
   at System.Linq.Enumerable.Aggregate[TSource,TAccumulate](IEnumerable`1 source, TAccumulate seed, Func`3 func)
   at EntityFrameworkCore.Jet.Data.JetCommand.ExecuteNonQuery()
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteNonQuery(RelationalCommandParameterObject parameterObject)
   at Microsoft.EntityFrameworkCore.Migrations.MigrationCommand.ExecuteNonQuery(IRelationalConnection connection, IReadOnlyDictionary`2 parameterValues)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationCommandExecutor.ExecuteNonQuery(IEnumerable`1 migrationCommands, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.Migrate(String targetMigration)
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.Migrate(DatabaseFacade databaseFacade)
   at X.Infrastructure.Setup.Migrate(IFactory`1 pDatabaseContextFactory, String pDatabasePath)
   at X.COM.XCOMWrapper.Setup(ISettingsProvider pSettingsProvider)

Однако, если мы удалим этот оператор sql из кода миграции и выполним его следующим образом, перед вызовом context.Database.Migrate():

 var dbConnection = context.Database.GetDbConnection();
                     dbConnection.Open();
                     using (var transaction = dbConnection.BeginTransaction())
                     {
                         var updateForeignKeyReferences= dbConnection.CreateCommand();
                         updateForeignKeyReferences.CommandText = "UPDATE A SET ForeignKeyColumn = NULL WHERE ForeignKeyColumn & \"\" = \"\"";
                         updateForeignKeyReferences.ExecuteNonQuery();
                         transaction.Commit();
                     }
                     dbConnection.Close();

Это работает просто отлично. Является ли мой подход к использованию кода sql в методе up совершенно неправильным? Каковы возможные причины этого? И самое главное, как я могу это исправить? Второй подход — это мой текущий обходной путь для этой проблемы, но я боюсь, что это означает, что в долгосрочной перспективе я не смогу использовать механизм миграции и должен использовать собственное решение (или другую структуру). Я бы предпочел просто придерживаться ef core.

Важно: это приложение работает с устаревшим приложением, и мы должны вставить историю приложения с помощью кода sql при первоначальном запуске. Для этого мы создаем транзакцию и просто создаем таблицу истории и вставляем изначально созданную таблицу. Это работает просто отлично, и транзакции, а также команды должны быть закрыты. Эта функция никогда не затрагивает таблицу A.


person Tobias    schedule 31.05.2021    source источник


Ответы (1)


Использование migrationBuilder.Sql("UPDATE `A` SET `ForeignKeyColumn` = NULL WHERE `ForeignKeyColumn` = ''") является правильной процедурой.

Он должен выполняться нормально.

К сожалению, похоже, существует проблема, из-за которой Jet по-прежнему удерживает блокировку таблицы, используемой в команде UPDATE, при выполнении оператора CREATE INDEX (который был сгенерирован для вашего нового свойства навигации и является частью метода миграции Up()).

Это проблема только в том случае, если оба оператора выполняются внутри одной и той же транзакции (что по умолчанию имеет место для миграций). В противном случае блокировка не удерживается и оператор CREATE INDEX выполняется успешно.


Самый простой способ исправить эту проблему — установить для параметра migrationBuilder.Sql() suppressTransaction значение true.

Это выполнит оператор вне остальной части транзакции и не заблокирует таблицу:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Sql(
        "UPDATE `A` SET `ForeignKeyColumn` = NULL WHERE `ForeignKeyColumn` = ''",
        suppressTransaction: true);
    
    migrationBuilder.CreateIndex(/* ... */);
    migrationBuilder.AddForeignKey(/* ... */);
}

Другой способ, позволяющий выполнить оператор UPDATE внутри транзакции, заключается в выполнении команды в отдельной выделенной миграции:

  • Добавьте пустую миграцию. Добавьте вызов migrationBuilder.Sql() в пустой метод Up() этой миграции.
  • Добавьте фактическую миграцию (содержащую операции CreateIndex() и AddForeignKey()).
  • Примените обе миграции к своей базе данных.
person lauxjpn    schedule 01.06.2021