2011-05-07 18 views
7

¿Cuál es la mejor forma de generar manualmente claves primarias en Entity Framework 4.1 Code First?¿Cuál es la mejor forma de generar manualmente claves primarias en Entity Framework 4.1 Code First

Estoy programando ASP.NET MVC 3 y utilizo un patrón de repositorio.

actualmente generar claves en un orden secuencial utilizando el código de abajo:

'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 

Mi ConSern es (aunque poco probable) de que dos (o más) usuarios tratarán de salvar una entidad Foo, al mismo tiempo y, por lo tanto, obtendrá la misma ID y la inserción fallará.

¿Es esta una buena manera, o hay alguna mejor?

¡Por favor no que NO PUEDO (y no usaré) un campo de identidad generado por la base de datos!

+0

¿Por qué no tiene que usar la identidad? –

Respuesta

2

Esto es lo que terminé usando. Este código está basado en la publicación de Ladislav Mrnka, pero modificado para trabajar con DbContext.

Modelo para almacenar información de secuencia (no olvide agregarlo como un DBSet en su contexto).

<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 

base de datos Initilize y crear un procedimiento almacenado para obtener y secuencias de incremento automático.

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 

obtener una nueva clave (iNewKey) para la Entidad Foo mediante la ejecución del procedimiento almacenado.

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

¿Se puede usar un GUID en lugar de un INT? Si es así, sólo puede utilizar

System.Guid.NewGuid().ToString() 

Si no es así, que necesita para bloquear el hilo o la mesa para evitar dos insertos utilizando el mismo ID.

+0

No puedo usar Guid. ¿Podría proporcionar una muestra de código de cómo bloquear el hilo o la tabla? – Thomas

+1

Ese es un tema amplio, Google es tu amigo al conocerlo. También puede hacer que la columna sea Única (si no lo es) y observar si hay algún error al insertarla. Si dos hilos intentan a la vez, obtendrá un error en uno de ellos. El que tiene el error puede intentar nuevamente con el siguiente entero hasta que tenga éxito. Este no es un buen diseño, pero funcionaría. – Mikecito

5

Su preocupación es válida: en el sitio web de uso frecuente es más probable que suceda y la solución no es muy fácil. Puede usar el lado del cliente Guid como lo describe @Mikecito pero tiene un rendimiento significativo y creo que no desea usarlo.

La forma en que está haciendo esto en este momento es muy mala porque la única solución es envolver su código en una transacción serializable única: la transacción debe contener tanto seleccionar Id como guardar el registro. Esto hará que el acceso a su InventoryObjects sea secuencial porque cada select max bloqueará toda la tabla hasta que se haya confirmado la transacción; nadie más podrá leer o escribir datos en la tabla durante la transacción de inserción. No tiene por qué ser un problema en un sitio poco visitado, pero puede ser NO IR en un sitio visitado con frecuencia. No hay forma de hacerlo de manera diferente en su configuración actual.

La mejora parcial consiste en utilizar una tabla separada para mantener el valor máximo + el procedimiento almacenado para obtener el siguiente valor e incrementar el valor almacenado en la operación atómica (en realidad simula secuencias de Oracle). Ahora la única complicación es si necesita la secuencia sin espacios. Por ejemplo, si algo sale mal con el guardado de InventoryObject, el ID seleccionado se perderá y creará un espacio en la secuencia del identificador. Si necesita secuencia sin huecos, debe volver a usar la transacción para obtener el siguiente Id y guardar el registro, pero esta vez solo bloqueará un solo registro en la tabla de secuencia. Recuperar el Id de la tabla de secuencia debe ser lo más cercano posible a guardar los cambios para minimizar el tiempo cuando la grabación de la secuencia está bloqueada.

Aquí es muestra de la tabla de secuencia y el procedimiento para la secuencia de servidor 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 

La tabla no necesita ser mapeada por código primero - que nunca acceder a él directamente. Debe crear un inicializador de base de datos personalizado para agregar la tabla y el procedimiento almacenado para usted cuando EF crea una base de datos. Puede intentar un enfoque similar al described here. También debe agregar un registro de inicialización para su secuencia con el valor inicial.

Ahora sólo tiene que llamar a procedimiento almacenado para obtener un valor antes de que se va a guardar el registro:

// 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(); 
} 
Cuestiones relacionadas