Каков наилучший способ создания первичных ключей вручную в Entity Framework 4.1 Code First? - PullRequest
7 голосов
/ 08 мая 2011

Каков наилучший способ создания первичных ключей вручную в Entity Framework 4.1 Code First?

Я программирую ASP.NET MVC 3 и использую шаблон репозитория.

В настоящее время я генерирую ключи в последовательном порядке, используя код ниже:

'Code First Class
Public Class Foo
    <Key()>
    <DatabaseGenerated(DatabaseGeneratedOption.None)>
    Public Property iId As Integer

    Public Property sBar As String
End Class

'Context Class
 Public Class FooBarContext : Inherits DbContext
     Public Property Foos As DbSet(Of Foo)
 End Class

'Get the current Id

'Part of code in repository that stores Entity Foo.
Dim iCurrId as Integer = (From io In context.Foo
                         Select io.iId()).Max

Dim iNewId as Integer = iCurrId + 1

Foo.iId = iNewId

Мое согласие (хотя и маловероятное) состоит в том, что два (или более) пользователя будут пытаться сохранить сущность Foo одновременно и, следовательно, получат одинаковый идентификатор, и вставка завершится неудачей.

Это хороший способ или есть что-то лучше?

Обратите внимание, что я НЕ МОГУ (и не буду) использовать поле идентификатора, созданное базой данных!

Ответы [ 3 ]

5 голосов
/ 08 мая 2011

Ваша проблема действительна - на часто используемом веб-сайте это может произойти, и решение не очень простое. Вы можете использовать клиентскую сторону Guid, как описано в @Mikecito, но она значительно снижает производительность, и я думаю, вы не хотите ее использовать.

То, как вы делаете это в данный момент, очень плохо, потому что единственное решение - заключить ваш код в одну сериализуемую транзакцию - транзакция должна содержать как выбор Id, так и сохранение записи. Это сделает доступ к вашей InventoryObjects последовательной, потому что каждый выбор max будет блокировать всю таблицу до фиксации транзакции - никто больше не сможет читать или записывать данные в таблицу во время транзакции вставки. Это не должно быть проблемой на редко посещаемых сайтах, но на часто посещаемых сайтах это НЕ БУДЕТ. Невозможно сделать это по-другому в вашей текущей настройке.

Частичное улучшение заключается в использовании отдельной таблицы для хранения максимального значения + хранимой процедуры, чтобы получить следующее значение и увеличить хранимое значение в атомарной операции - (на самом деле она имитирует последовательности из Oracle). Теперь единственное осложнение, если вам нужна последовательность без пробелов. Например, если что-то пойдет не так с сохранением нового InventoryObject, выбранный идентификатор будет потерян, и это создаст пробел в последовательности идентификатора. Если вам нужна последовательность без пробелов, вы должны снова использовать транзакцию, чтобы получить следующий Id и сохранить запись, но на этот раз вы заблокируете только одну запись в таблице последовательности. Извлечение идентификатора из таблицы последовательности должно быть максимально приближено к сохранению изменений, чтобы минимизировать время, когда запись последовательности заблокирована.

Вот пример таблицы последовательности и процедуры последовательности для сервера SQL:

CREATE TABLE [dbo].[Sequences]
(
    [SequenceType] VARCHAR(20) NOT NULL, /* Support for multiple sequences */
    [Value] INT NOT NULL
)

CREATE PROCEDURE [dbo].[GetNextSequenceValue]
    @SequenceType VARCHAR(20)
AS
BEGIN
    DECLARE @Result INT

    UPDATE [dbo].[Sequences] WITH (ROWLOCK, UPDLOCK)
    SET @Result = Value = Value + 1
    WHERE SequenceType = @SequenceType

    RETURN @Result
END

Таблица не должна сначала отображаться кодом - вы никогда не получите к ней доступ напрямую. Вы должны создать собственный инициализатор базы данных, чтобы добавить таблицу и хранимую процедуру для вас, когда EF создает базу данных. Вы можете попробовать подобный подход как , описанный здесь . Вы также должны добавить запись инициализации для вашей последовательности с начальным значением.

Теперь вам нужно только вызвать хранимую процедуру, чтобы получить значение, прежде чем вы собираетесь сохранить запись:

// Prepare and insert record here

// Transaction is needed only if you don't want gaps
// This whole can be actually moved to overriden SaveChanges in your context
using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew, 
    new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
   record.Id = context.Database.ExecuteStoreCommand("dbo.GetNextSequenceValue @SequenceType", 
       new SqlParameter("SequenceType", "InventoryObjects"));
   context.SaveChanges();
}
2 голосов
/ 14 мая 2011

Вот что я в итоге использовал.Этот код основан на публикации Ладислава Мрнки, но изменен для работы с DbContext.

Модель для хранения информации о последовательности (не забудьте добавить ее как DBSet в вашем контексте).

<Table("tSequences")>
Public Class Sequence
    <Key()>
    <DatabaseGenerated(DatabaseGeneratedOption.None)>
    <Display(Name:="Model name", Order:=1)>
    Public Property sModelName As String

    <Required()>
    <Display(Name:="Current Primary key value", AutoGenerateField:=False, Order:=2)>
    Public Property iCurrentPKeyValue As Integer
End Class

Инициализация базы данных и создание хранимой процедуры для получения и автоматического увеличения последовательностей.

Public Class DBInitializer
    Inherits CreateDatabaseIfNotExists(Of Context)

    Protected Overrides Sub Seed(context As Context)
        'Create stored procedure to hold
        Dim sStoredProcSQL As String = "CREATE PROCEDURE [dbo].[spGetNextSequenceValue]" & vbCrLf & _
                                        "@sModelName VARCHAR(30)" & vbCrLf & _
                                        "AS BEGIN" & vbCrLf & _
                                        "DECLARE" & vbCrLf & _
                                        "@Result INT" & vbCrLf & _
                                        "UPDATE [dbo].[tSequences] WITH (ROWLOCK, UPDLOCK)" & vbCrLf & _
                                        "SET @Result = iCurrentPKeyValue = iCurrentPKeyValue + 1" & vbCrLf & _
                                        "WHERE sModelName = @sModelName" & vbCrLf & _
                                        "RETURN @Result" & vbCrLf &
                                        "END"

        context.Database.ExecuteSqlCommand(sStoredProcSQL)
    End Sub
End Class

Получите новый ключ (iNewKey) для Entity Foo, запустив хранимую процедуру.

Dim iNewKey As Integer

Using scope = New TransactionScope(TransactionScopeOption.RequiresNew, New TransactionOptions() With { _
    .IsolationLevel = IsolationLevel.ReadCommitted _
    })
    iNewKey = context.Database.SqlQuery(Of Integer)("DECLARE @return_value int" & vbCrLf & _
                                                    "EXEC @return_value = [dbo].[spGetNextSequenceValue]" & vbCrLf & _
                                                    "@sModelName = 'Foo'" & vbCrLf & _
                                                    "SELECT 'Return Value' = @return_value").ToList().First()
'Indicate that all operations are completed.
    scope.Complete()

    context.SaveChanges()
End Using
0 голосов
/ 08 мая 2011

Можете ли вы использовать GUID вместо INT?Если это так, вы можете просто использовать

System.Guid.NewGuid().ToString()

Если нет, вам нужно заблокировать поток или таблицу, чтобы избежать двух вставок, использующих один и тот же идентификатор.

...