E 'chiaro che non puoi sfuggire a questo catch-22 giocando con DatabaseGeneratedOption
s.
L'opzione migliore, come suggerito, è impostare DatabaseGeneratedOption.None
e ottenere il valore successivo dalla sequenza (ad esempio come this question) proprio prima di salvare un nuovo record. Quindi assegnarlo al valore Id e salvare. Questo è sicuro per la concorrenza, perché sarai l'unico a tracciare quel valore specifico dalla sequenza (supponiamo che nessuno resetta la sequenza).
Tuttavia, v'è un possibile mod ...
uno cattivo, e mi dovrebbe fermarsi qui ...
EF 6 ha introdotto il comando intercettore API. Permette di manipolare i comandi SQL di EF e i loro risultati prima e dopo l'esecuzione dei comandi. Ovviamente non dovremmo manomettere questi comandi, dovremmo?
Beh ... se guardiamo a un comando di inserimento che viene eseguito quando DatabaseGeneratedOption.Identity
è impostato, si vede qualcosa di simile:
INSERT [dbo].[Person]([Name]) VALUES (@0)
SELECT [Id]
FROM [dbo].[Person]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
Il comando SELECT
viene utilizzato per recuperare il valore della chiave primaria generata dal database e impostare la proprietà Identity del nuovo oggetto su questo valore. Ciò consente a EF di utilizzare questo valore nelle istruzioni di inserimento successive che fanno riferimento a questo nuovo oggetto da una chiave esterna nella stessa transazione.
Quando la chiave primaria viene generata da un valore predefinito che acquisisce il suo valore da una sequenza (come si fa), è evidente che non esiste scope_identity()
.V'è tuttavia un valore corrente della sequenza, che si trova da un comando come
SELECT current_value FROM sys.sequences WHERE name = 'PersonSequence'
Se solo potessimo fare EF eseguire questo comando dopo l'inserto al posto di scope_identity()
!
Bene, possiamo.
In primo luogo, dobbiamo creare una classe che implementa IDbCommandInterceptor
, o eredita dalla implementazione di default DbCommandInterceptor
:
using System.Data.Entity.Infrastructure.Interception;
class SequenceReadCommandInterceptor : DbCommandInterceptor
{
public override void ReaderExecuting(DbCommand command
, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
}
}
Aggiungiamo questa classe al contesto intercettazione dal comando
DbInterception.Add(new SequenceReadCommandInterceptor());
Il comando ReaderExecuting
viene eseguito immediatamente prima che venga eseguito command
. Se si tratta di un comando INSERT
con una colonna Identity, il suo testo è simile al comando precedente. Ora abbiamo potuto sostituire la parte scope_identity()
dalla query ottenendo il valore della sequenza corrente:
command.CommandText = command.CommandText
.Replace("scope_identity()",
"(SELECT current_value FROM sys.sequences
WHERE name = 'PersonSequence')");
Ora il comando sarà simile
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')
E se corriamo questo, la cosa divertente è : Funziona. Subito dopo il comando SaveChanges
il nuovo oggetto ha ricevuto il suo valore Id persistito.
Realmente non penso che questo sia pronto per la produzione. Dovresti modificare il comando quando si tratta di un comando di inserimento, scegliere la sequenza corretta in base all'entità inserita, tutto tramite la manipolazione di stringhe sporche in un luogo piuttosto oscuro. E Non so se con una concorrenza pesante si otterrà sempre il giusto valore di sequenza. Ma chissà, forse una prossima versione di EF supporterà questo fuori dagli schemi.
Forse il ripping dell'intero 'select' e l'inserimento di una clausola' output inserted.Id' appena prima dei 'valori'potrebbero invece eseguire il trucco, in un modo più robusto. –
@Frederic Interessante, ma l'istruzione select viene utilizzata anche per ottenere i valori delle colonne calcolate (se presenti). E la clausola 'output' deve avere una variabile' in' se la tabella ha trigger, rendendo molto più complesso (se possibile) ottenere il valore id inserito. –
L'istruzione 'output' può restituire anche le colonne calcolate. Buon punto per supportare il trigger, dobbiamo usare 'into'. Quindi la modifica dovrebbe essere aggiunta come prima riga 'declare @InsId table (Id int)', inserendo 'output inserted.Id in @ InsId', mantenendo' select' ma aumentato con 'inner join @InsId ii su [Person] . [Id] = ii.Id', e infine rimuovendo la condizione 'e [Id] = ...'. Inizia ad essere un po 'pesante per un hack già brutto. –