Ответ 1
Ясно, что вы не можете убежать от этого catch-22, играя с DatabaseGeneratedOption
s.
Лучшим вариантом, как вы предположили, является установка DatabaseGeneratedOption.None
и получение следующего значения из последовательности (например, как в этот вопрос) перед сохранением новый рекорд. Затем назначьте его значению Id и сохраните. Это concurrency -безопасно, потому что вы будете единственным рисунком, которое имеет конкретное значение из последовательности (пусть никто не сбрасывает последовательность).
Однако есть возможный взлом...
Плохой, и я должен остановиться здесь...
EF 6 представил API-интерфейс перехватчика команд. Он позволяет вам манипулировать командами EF SQL и их результатами до и после выполнения команд. Конечно, мы не должны вмешиваться в эти команды, не так ли?
Ну... если мы посмотрим на команду insert, которая выполняется при установке DatabaseGeneratedOption.Identity
, мы видим примерно следующее:
INSERT [dbo].[Person]([Name]) VALUES (@0)
SELECT [Id]
FROM [dbo].[Person]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
Команда SELECT
используется для извлечения сгенерированного значения первичного ключа из базы данных и установки нового свойства идентификации объекта на это значение. Это позволяет EF использовать это значение в последующих операторах вставки, которые ссылаются на этот новый объект внешним ключом в той же транзакции.
Когда первичный ключ генерируется по умолчанию, принимая его значение из последовательности (как и вы), очевидно, что нет scope_identity()
. Однако существует текущее значение последовательности, которое может быть найдено с помощью команды типа
SELECT current_value FROM sys.sequences WHERE name = 'PersonSequence'
Если бы мы могли заставить EF выполнить эту команду после вставки вместо scope_identity()
!
Ну, мы можем.
Сначала мы должны создать класс, реализующий IDbCommandInterceptor
, или наследуемый от реализации по умолчанию DbCommandInterceptor
:
using System.Data.Entity.Infrastructure.Interception;
class SequenceReadCommandInterceptor : DbCommandInterceptor
{
public override void ReaderExecuting(DbCommand command
, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
}
}
Мы добавляем этот класс в контекст перехвата командой
DbInterception.Add(new SequenceReadCommandInterceptor());
Команда ReaderExecuting
запускается непосредственно перед выполнением command
. Если это команда INSERT
с столбцом идентификации, ее текст выглядит как команда выше. Теперь мы можем заменить часть scope_identity()
на запрос, получая текущее значение последовательности:
command.CommandText = command.CommandText
.Replace("scope_identity()",
"(SELECT current_value FROM sys.sequences
WHERE name = 'PersonSequence')");
Теперь команда будет выглядеть как
INSERT [dbo].[Person]([Name]) VALUES (@0)
SELECT [Id]
FROM [dbo].[Person]
WHERE @@ROWCOUNT > 0 AND [Id] =
(SELECT current_value FROM sys.sequences
WHERE name = 'PersonSequence')
И если мы запустим это, самое смешное: оно работает. Сразу после команды SaveChanges
новый объект получил свое постоянное значение Id.
Я действительно не думаю, что это готово к производству. Вам нужно будет изменить команду, когда она будет вставлять команду, выбрать правильную последовательность, основанную на вставленном объекте, все путем грязной манипуляции строк в довольно неясном месте. И я не знаю, получится ли с тяжелым concurrency правильное значение последовательности. Но кто знает, может быть, следующая версия EF поддержит это из коробки.