Migrazioni Code First in ambienti team

Nota

Questo articolo presuppone che si sappia come usare Migrazioni Code First in scenari di base. In caso contrario, sarà necessario leggere Migrazioni Code First prima di continuare.

Prendi un caffè, devi leggere l'intero articolo

I problemi negli ambienti del team riguardano principalmente l'unione delle migrazioni quando due sviluppatori hanno generato migrazioni nella codebase locale. Anche se i passaggi per risolverli sono piuttosto semplici, è necessario avere una conoscenza approfondita del funzionamento delle migrazioni. Non passare direttamente alla fine. Leggere l'intero articolo per assicurarsi di avere esito positivo.

Alcune linee guida generali

Prima di esaminare come gestire l'unione delle migrazioni generate da più sviluppatori, di seguito sono riportate alcune linee guida generali per configurare correttamente le migrazioni.

Ogni membro del team deve avere un database di sviluppo locale

Le migrazioni usano la tabella __MigrationsHistory per archiviare le migrazioni applicate al database. Se si hanno più sviluppatori che generano migrazioni diverse durante il tentativo di scegliere come destinazione lo stesso database (e quindi condividere una tabella di __MigrationsHistory ) le migrazioni andranno molto confuse.

Naturalmente, se si hanno membri del team che non generano migrazioni, non c'è alcun problema con cui condividono un database di sviluppo centrale.

Evitare migrazioni automatiche

La linea finale è che le migrazioni automatiche inizialmente sembrano buone negli ambienti del team, ma in realtà non funzionano. Se si vuole sapere perché, continuare a leggere, in caso contrario, è possibile passare alla sezione successiva.

Le migrazioni automatiche consentono di aggiornare lo schema del database in modo che corrisponda al modello corrente senza la necessità di generare file di codice (migrazioni basate su codice). Le migrazioni automatiche funzionano in modo ottimale in un ambiente team se sono state usate solo e non sono mai state generate migrazioni basate su codice. Il problema è che le migrazioni automatiche sono limitate e non gestiscono una serie di operazioni: ridenominazione di proprietà/colonna, spostamento dei dati in un'altra tabella e così via. Per gestire questi scenari, si generano migrazioni basate su codice (e si modifica il codice con scaffolding) miste tra le modifiche gestite dalle migrazioni automatiche. Ciò rende impossibile unire le modifiche quando due sviluppatori controllano le migrazioni.

Informazioni sul funzionamento delle migrazioni

La chiave per usare correttamente le migrazioni in un ambiente team è una conoscenza di base del modo in cui le migrazioni tengono traccia e usano informazioni sul modello per rilevare le modifiche del modello.

La prima migrazione

Quando si aggiunge la prima migrazione al progetto, si esegue un'operazione simile a Add-Migration First nella console di Gestione pacchetti. I passaggi generali distribuiti da questo comando sono illustrati di seguito.

Prima migrazione

Il modello corrente viene calcolato dal codice (1). Gli oggetti di database necessari vengono quindi calcolati dal modello diversi (2): poiché questa è la prima migrazione, il modello usa solo un modello vuoto per il confronto. Le modifiche necessarie vengono passate al generatore di codice per compilare il codice di migrazione richiesto (3) che viene quindi aggiunto alla soluzione di Visual Studio (4).

Oltre al codice di migrazione effettivo archiviato nel file di codice principale, le migrazioni generano anche alcuni file code-behind aggiuntivi. Questi file sono metadati usati dalle migrazioni e non sono elementi da modificare. Uno di questi file è un file di risorse (con estensione resx) che contiene uno snapshot del modello al momento della generazione della migrazione. Si noterà come viene usato nel passaggio successivo.

A questo punto è probabile che si esegua Update-Database per applicare le modifiche al database e quindi si procede all'implementazione di altre aree dell'applicazione.

Migrazioni successive

In seguito si tornerà e si apportano alcune modifiche al modello. Nell'esempio verrà aggiunta una proprietà URL a Blog. Si eseguirà quindi un comando, ad esempio Add-Migration AddUrl , per eseguire lo scaffolding di una migrazione per applicare le modifiche del database corrispondenti. I passaggi generali distribuiti da questo comando sono illustrati di seguito.

Seconda migrazione

Proprio come l'ultima volta, il modello corrente viene calcolato dal codice (1). Tuttavia, questa volta sono presenti migrazioni in modo che il modello precedente venga recuperato dalla migrazione più recente (2). Questi due modelli vengono disattivati per trovare le modifiche necessarie al database (3) e quindi il processo viene completato come prima.

Questo stesso processo viene usato per eventuali altre migrazioni aggiunte al progetto.

Perché preoccuparsi dello snapshot del modello?

Ci si potrebbe chiedere perché EF si preoccupa dello snapshot del modello, perché non solo esaminare il database. In tal caso, leggere. Se non si è interessati, è possibile ignorare questa sezione.

Ef mantiene lo snapshot del modello per diversi motivi:

  • Consente al database di derivare dal modello ef. Queste modifiche possono essere apportate direttamente nel database oppure è possibile modificare il codice con scaffolding nelle migrazioni per apportare le modifiche. Ecco alcuni esempi di questo comportamento in pratica:
    • Si vuole aggiungere una colonna Inserita e Aggiornata a una o più tabelle, ma non si vogliono includere queste colonne nel modello di Entity Framework. Se le migrazioni esaminate nel database, tentano continuamente di eliminare queste colonne ogni volta che si esegue lo scaffolding di una migrazione. Usando lo snapshot del modello, Ef rileverà solo le modifiche legittime apportate al modello.
    • Si vuole modificare il corpo di una stored procedure usata per gli aggiornamenti per includere alcune registrazioni. Se le migrazioni esaminate questa stored procedure dal database, proverebbero continuamente a ripristinarla nella definizione prevista da EF. Usando lo snapshot del modello, Ef eseguirà lo scaffolding del codice solo per modificare la stored procedure quando si modifica la forma della routine nel modello ef.
    • Questi stessi principi si applicano all'aggiunta di indici aggiuntivi, incluse tabelle aggiuntive nel database, il mapping di Entity Framework a una vista di database che si trova su una tabella e così via.
  • Il modello di Entity Framework contiene più semplicemente la forma del database. La presenza dell'intero modello consente alle migrazioni di esaminare le informazioni sulle proprietà e le classi nel modello e su come eseguono il mapping alle colonne e alle tabelle. Queste informazioni consentono alle migrazioni di essere più intelligenti nel codice di cui esegue lo scaffolding. Ad esempio, se si modifica il nome della colonna mappata da una proprietà alle migrazioni, è possibile rilevare la ridenominazione verificando che si tratti della stessa proprietà, ovvero un elemento che non può essere eseguito se si dispone solo dello schema del database. 

Cosa causa problemi negli ambienti del team

Il flusso di lavoro trattato nella sezione precedente funziona perfettamente quando si è un singolo sviluppatore che lavora su un'applicazione. Funziona anche bene in un ambiente team se si è l'unica persona che apporta modifiche al modello. In questo scenario è possibile apportare modifiche al modello, generare migrazioni e inviarle al controllo del codice sorgente. Altri sviluppatori possono sincronizzare le modifiche ed eseguire Update-Database per applicare le modifiche dello schema.

I problemi iniziano a verificarsi quando si hanno più sviluppatori che apportano modifiche al modello ef e si inviano contemporaneamente al controllo del codice sorgente. Ciò che EF manca è un modo di primo livello per unire le migrazioni locali alle migrazioni inviate da un altro sviluppatore al controllo del codice sorgente dall'ultima sincronizzazione.

Esempio di conflitto di unione

Si esamini innanzitutto un esempio concreto di un conflitto di unione. Si continuerà con l'esempio esaminato in precedenza. Come punto di partenza si supponga che le modifiche apportate alla sezione precedente siano state archiviate dallo sviluppatore originale. Verranno monitorati due sviluppatori man mano che apportano modifiche alla codebase.

Verranno monitorati il modello ef e le migrazioni tramite diverse modifiche. Per un punto di partenza, entrambi gli sviluppatori sono stati sincronizzati con il repository del controllo del codice sorgente, come illustrato nell'immagine seguente.

Punto di partenza

Lo sviluppatore 1 e lo sviluppatore #2 apportano ora alcune modifiche al modello ef nella codebase locale. Developer #1 aggiunge una proprietà Rating a Blog e genera una migrazione AddRating per applicare le modifiche al database. Developer #2 aggiunge una proprietà Reader a Blog e genera la migrazione di AddReaders corrispondente. Entrambi gli sviluppatori eseguono Update-Database, per applicare le modifiche ai database locali e quindi continuare a sviluppare l'applicazione.

Nota

Le migrazioni sono precedute da un timestamp, quindi il grafico rappresenta che la migrazione di AddReaders da Developer #2 viene eseguita dopo la migrazione di AddRating da Developer #1. Indipendentemente dal fatto che lo sviluppatore n. 1 o il numero 2 abbia generato prima di tutto la migrazione non fa alcuna differenza per i problemi di lavoro in un team o per il processo di unione che verranno esaminati nella sezione successiva.

Modifiche locali

È un giorno fortunato per developer #1 quando si verificano prima di inviare le modifiche. Poiché nessun altro utente ha eseguito l'archiviazione perché ha sincronizzato il repository, può inviare le modifiche senza eseguire alcuna unione.

Invia modifiche

È ora possibile inviare developer #2. Non sono così fortunati. Poiché un altro utente ha inviato modifiche dopo la sincronizzazione, dovrà eseguire il pull delle modifiche e unire. Il sistema di controllo del codice sorgente sarà probabilmente in grado di unire automaticamente le modifiche a livello di codice perché sono molto semplici. Lo stato del repository locale di Developer #2 dopo la sincronizzazione è illustrato nell'immagine seguente. 

Eseguire il pull dal controllo del codice sorgente

In questa fase Developer #2 può eseguire Update-Database che rileverà la nuova migrazione addRating (che non è stata applicata al database di Developer #2) e applicarla. La colonna Rating viene ora aggiunta alla tabella Blogs e il database è sincronizzato con il modello.

Esistono alcuni problemi, tuttavia:

  1. Anche se Update-Database applicherà la migrazione AddRating verrà generato anche un avviso: Non è possibile aggiornare il database in modo che corrisponda al modello corrente perché sono presenti modifiche in sospeso e la migrazione automatica è disabilitata... Il problema è che lo snapshot del modello archiviato nell'ultima migrazione (AddReader) manca la proprietà Rating nel blog (poiché non fa parte del modello quando la migrazione è stata generata). Code First rileva che il modello nell'ultima migrazione non corrisponde al modello corrente e genera l'avviso.
  2. L'esecuzione dell'applicazione genera un'eccezione InvalidOperationException che indica che "Il modello che esegue il backup del contesto "BloggingContext" è cambiato dopo la creazione del database. È consigliabile usare Migrazioni Code First per aggiornare il database..." Di nuovo, il problema è lo snapshot del modello archiviato nell'ultima migrazione non corrisponde al modello corrente.
  3. Infine, si prevede che l'esecuzione di Add-Migration generi una migrazione vuota (poiché non sono presenti modifiche da applicare al database). Tuttavia, poiché le migrazioni confrontano il modello corrente con quello dell'ultima migrazione (che manca la proprietà Rating ) verrà effettivamente scaffoldato un'altra chiamata AddColumn per aggiungere nella colonna Rating . Naturalmente, questa migrazione avrà esito negativo durante Update-Database perché la colonna Valutazione esiste già.

Risoluzione del conflitto di unione

La buona notizia è che non è troppo difficile gestire manualmente l'unione, a condizione che si abbia una comprensione del funzionamento delle migrazioni. Quindi, se hai ignorato prima di questa sezione... scusa, devi tornare indietro e leggere prima il resto dell'articolo!

Esistono due opzioni, la più semplice consiste nel generare una migrazione vuota con il modello corrente corretto come snapshot. La seconda opzione consiste nell'aggiornare lo snapshot nell'ultima migrazione per avere lo snapshot del modello corretto. La seconda opzione è un po'più difficile e non può essere usata in ogni scenario, ma è anche più pulita perché non comporta l'aggiunta di una migrazione aggiuntiva.

Opzione 1: Aggiungere una migrazione "merge" vuota

In questa opzione viene generata una migrazione vuota esclusivamente allo scopo di assicurarsi che la migrazione più recente abbia lo snapshot del modello corretto archiviato in esso.

Questa opzione può essere usata indipendentemente da chi ha generato l'ultima migrazione. Nell'esempio che abbiamo seguito Developer #2 sta prendendo cura dell'unione ed è successo che generano l'ultima migrazione. Tuttavia, questi stessi passaggi possono essere usati se Developer #1 ha generato l'ultima migrazione. I passaggi si applicano anche se sono presenti più migrazioni coinvolte: è stato appena esaminato due per mantenere semplice la migrazione.

Il processo seguente può essere usato per questo approccio, a partire dal momento in cui si hanno modifiche che devono essere sincronizzate dal controllo del codice sorgente.

  1. Assicurarsi che le modifiche del modello in sospeso nella code base locale siano state scritte in una migrazione. Questo passaggio garantisce di non perdere modifiche legittime quando si tratta del momento di generare la migrazione vuota.
  2. Sincronizzazione con il controllo del codice sorgente.
  3. Eseguire Update-Database per applicare le nuove migrazioni che altri sviluppatori hanno eseguito l'archiviazione. Nota:se non vengono visualizzati avvisi dal comando Update-Database, non sono state eseguite nuove migrazioni da altri sviluppatori e non è necessario eseguire ulteriori unione.
  4. Eseguire Add-Migration pick_a_name> -IgnoreChanges (ad esempio, Add-Migration <Merge –IgnoreChanges). In questo modo viene generata una migrazione con tutti i metadati (incluso uno snapshot del modello corrente), ma verranno ignorate le modifiche rilevate durante il confronto tra il modello corrente e lo snapshot nelle ultime migrazioni ( ovvero si ottiene un metodo Up and Down vuoto).
  5. Eseguire Update-Database per applicare nuovamente la migrazione più recente con i metadati aggiornati.
  6. Continuare a sviluppare o inviare al controllo del codice sorgente (dopo aver eseguito i unit test naturalmente).

Ecco lo stato della code base locale di Developer #2 dopo aver usato questo approccio.

Migrazione unione

Opzione 2: Aggiornare lo snapshot del modello nell'ultima migrazione

Questa opzione è molto simile all'opzione 1, ma rimuove la migrazione vuota aggiuntiva, perché è possibile affrontare, chi vuole file di codice aggiuntivi nella soluzione.

Questo approccio è fattibile solo se la migrazione più recente esiste solo nella base di codice locale e non è ancora stata inviata al controllo del codice sorgente (ad esempio, se l'ultima migrazione è stata generata dall'utente che esegue la merge). La modifica dei metadati delle migrazioni che altri sviluppatori potrebbero aver già applicato al database di sviluppo, o anche peggio applicati a un database di produzione, possono causare effetti collaterali imprevisti. Durante il processo verrà eseguito il rollback dell'ultima migrazione nel database locale e riapplicarlo con metadati aggiornati.

Anche se l'ultima migrazione deve trovarsi solo nella code base locale non esistono restrizioni per il numero o l'ordine delle migrazioni che lo procede. Esistono più migrazioni da più sviluppatori diversi e gli stessi passaggi si applicano: abbiamo appena esaminato due per mantenere semplice la migrazione.

Il processo seguente può essere usato per questo approccio, a partire dal momento in cui si hanno modifiche che devono essere sincronizzate dal controllo del codice sorgente.

  1. Assicurarsi che le modifiche del modello in sospeso nella code base locale siano state scritte in una migrazione. Questo passaggio garantisce di non perdere modifiche legittime quando si tratta del momento di generare la migrazione vuota.
  2. Sincronizzare con il controllo del codice sorgente.
  3. Eseguire Update-Database per applicare le nuove migrazioni che altri sviluppatori hanno eseguito l'archiviazione. Nota:se non vengono visualizzati avvisi dal comando Update-Database, non sono state eseguite nuove migrazioni da altri sviluppatori e non è necessario eseguire ulteriori unione.
  4. Eseguire Update-Database –TargetMigration <second_last_migration> (nell'esempio che si è verificato questo problema sarà Update-Database –TargetMigration AddRating). Questo esegue il rollback del database allo stato dell'ultima migrazione, in modo efficace ,annullando l'applicazione dell'ultima migrazione dal database. Nota:questo passaggio è necessario per rendere sicuro modificare i metadati della migrazione poiché i metadati vengono archiviati anche nella __MigrationsHistoryTable del database. Questo è il motivo per cui è consigliabile usare questa opzione solo se l'ultima migrazione è presente solo nella code base locale. Se altri database avevano applicato l'ultima migrazione, è necessario eseguire il rollback e riapplicare l'ultima migrazione per aggiornare i metadati. 
  5. Eseguire l'full_name_including_timestamp_of_last_migration Add-Migration (nell'esempio seguente si tratta di un elemento simile a Add-Migration <201311062215252_AddReaders>). Nota:è necessario includere il timestamp in modo che le migrazioni sappiano che si vuole modificare la migrazione esistente anziché scaffolding di una nuova. Verrà aggiornato i metadati per l'ultima migrazione in modo che corrisponda al modello corrente. Si otterrà l'avviso seguente al termine del comando, ma è esattamente ciò che si vuole. "Solo il codice di progettazione per la migrazione '201311062215252_AddReaders' è stato ri-scaffolded. Per ricartare l'intera migrazione, usare il parametro -Force.
  6. Eseguire Update-Database per applicare nuovamente la migrazione più recente con i metadati aggiornati.
  7. Continuare a sviluppare o inviare al controllo del codice sorgente (dopo aver eseguito i unit test naturalmente).

Ecco lo stato della code base locale di Developer #2 dopo aver usato questo approccio.

Metadati aggiornati

Riepilogo

Esistono alcuni problemi quando si usano Migrazioni Code First in un ambiente team. Tuttavia, una conoscenza di base del funzionamento delle migrazioni e alcuni semplici approcci per la risoluzione dei conflitti di unione semplificano la risoluzione di questi problemi.

Il problema fondamentale non è corretto dei metadati archiviati nella migrazione più recente. In questo modo code first rileva in modo errato che lo schema corrente e lo schema del database non corrispondono e per eseguire lo scaffolding del codice non corretto nella migrazione successiva. Questa situazione può essere superata generando una migrazione vuota con il modello corretto o aggiornando i metadati nella migrazione più recente.