Non è mai successo, eppure succede di nuovo!
In un nuovo progetto abbiamo deciso di utilizzare Liquibase sin dall'inizio per evitare problemi in futuro. Come si è rivelato, non tutti i membri più giovani del team sanno usarlo correttamente. Ho condotto un workshop interno che ho poi deciso di trasformare in un articolo.
L'articolo include consigli utili e una descrizione delle tre trappole più comuni in cui si può incappare lavorando con strumenti di migrazione per database relazionali, in particolare Liquibase. È destinato a sviluppatori Java di livello Junior e Middle; per sviluppatori più esperti potrebbe risultare interessante per strutturare e rinfrescare ciò che probabilmente conoscono già.

Liquibase e Flyway sono le principali tecnologie in concorrenza per la gestione delle versioni delle strutture relazionali nel mondo Java. La prima è completamente gratuita e nella pratica viene scelta più frequentemente, quindi Liquibase è stata scelta come protagonista della pubblicazione. Tuttavia, alcune delle pratiche descritte potrebbero essere universali, a seconda dell'architettura della tua applicazione.
Le migrazioni delle strutture relazionali sono emerse come un modo necessario per affrontare la scarsa flessibilità degli archivi di dati relazionali. Nell'era della programmazione orientata agli oggetti, il lavoro con i database implicava che una volta descritta una schema, non fosse più necessario modificarlo. Tuttavia, la realtà è che tutto cambia nel tempo, e le modifiche nella struttura delle tabelle sono necessarie piuttosto frequentemente. È naturale che questo processo possa essere doloroso e sgradevole.
Non entrerò nei dettagli della tecnologia e nelle istruzioni per aggiungere la libreria al proprio progetto, su questo tema sono stati scritti sufficienti articoli:
Inoltre, c'era già un ottimo articolo sui consigli utili:
Consigli
Desidero condividere i miei consigli e commenti, nati attraverso il sudore, il sangue e il dolore nella risoluzione dei problemi di migrazione.
1. Prima di iniziare, è consigliabile familiarizzare con la sezione delle migliori pratiche su Liquibase
vengono descritti aspetti semplici ma fondamentali, senza i quali l'uso della libreria può complicarti la vita. Ad esempio, un approccio non strutturato nella gestione dei changelog porterà inevitabilmente a confusione e migrazioni rotte. Se non si applicano cambiamenti strutturali e logici dei servizi contemporaneamente, c'è un'alta probabilità che si verifichino test falliti o un ambiente non funzionante. Inoltre, le raccomandazioni per l'uso di Liquibase sul sito ufficiale includono un punto relativo allo sviluppo e alla verifica di script di rollback insieme agli script di migrazione principali. Infine, nell'articolo troverai esempi di codice relativi alle migrazioni e al meccanismo di rollback.
2. Se inizi a utilizzare strumenti di migrazione, evita le correzioni manuali nella struttura del database.
Come si suol dire: «Una volta Persil, sempre Persil». Se il database della tua applicazione inizia a essere gestito tramite Liquibase, qualsiasi modifica manuale porta immediatamente a uno stato incoerente, e il livello di fiducia nei changelog scende a zero. I rischi potenziali includono ore spese per ripristinare il database, e nel peggiore dei casi, un server compromesso. Se nella tua squadra c'è un DBA Architect 'anziano', spiegagli pazientemente e con attenzione quanto sarà disastroso se decide di modificare il database a suo piacimento tramite SQL Developer.
3. Se il changelog è già stato pushato nel repository, evita di effettuare modifiche.
Se un altro sviluppatore ha fatto un pull e ha applicato un changelist che verrà successivamente modificato, sicuramente parlerà bene di te quando riceverà un errore all'avvio dell'applicazione. Se la modifica del changelist in qualche modo penetra nel dev, dovrà prendere la strada scivolosa degli hotfix. Il cuore del problema riguarda la validazione delle modifiche tramite hash — il meccanismo principale di Liquibase. Quando si modifica il codice del changelist, cambia anche l'hash. La modifica dei changelist è possibile solo quando c'è la possibilità di ripristinare l'intero database da zero senza perdita di dati. In tal caso, il refactoring del codice SQL o XML può, al contrario, semplificare la vita, rendendo le migrazioni più leggibili. Un esempio potrebbe essere la situazione in cui, all'avvio dell'applicazione, lo schema del database originale è stato concordato all'interno del team.
4. Tieni backup verificati dei database, se possibile
Qui penso sia tutto chiaro. Nel caso in cui la migrazione non andasse a buon fine, tutto può essere ripristinato. In Liquibase c'è uno strumento per il rollback delle modifiche, ma gli script per il rollback vengono scritti dallo stesso sviluppatore e possono presentare problemi con la stessa probabilità degli script del changelog principale. Questo significa che è sempre utile essere previdenti con i backup.
5. Utilizza backup di database verificati durante lo sviluppo, se possibile.
Se questo non contraddice i contratti e la privacy, la base dati non contiene dati personali ed è di dimensioni gestibili — prima di applicare migrazioni su server live, puoi testare come funziona sulla macchina dello sviluppatore e identificare quasi il 100% dei problemi potenziali durante la migrazione.
6. Comunica con gli altri sviluppatori del team.
In a well-organized development process, everyone in the team knows what each member is working on. In reality, this is often not the case, so if you are preparing changes to the database structure as part of your task, it's advisable to notify the entire team about it. If someone makes changes in parallel, you should organize yourselves carefully. It's important to communicate with colleagues even after the work is completed, not just at the start. Many potential issues with change sets can be resolved during the code review stage.
7. Think about what you are doing!
It may seem like an obvious piece of advice applicable to any situation. However, many problems could be avoided if the developer took a moment to analyze what they are doing and what impact it might have. Working with migrations always requires additional attention and care.
Traps
Now let's consider typical traps you might fall into if you don't follow the advice above, and what exactly to do?
Situation 1. Two developers are trying to add new change sets simultaneously.

Vasia e Petia vogliono creare un changset della versione 4, senza sapere l'uno dell'altro. Hanno effettuato modifiche nella struttura del database e hanno inviato una pull request, con diversi file del changset. Di seguito viene proposto il seguente meccanismo di azione:
Come procedere
- In qualche modo, i colleghi devono concordare l'ordine in cui devono essere applicati i loro changset; ad esempio, quello di Petia dovrebbe essere applicato per primo.
- Qualcuno deve includere il secondo nel proprio e contrassegnare il changset di Vasia come versione 5. Questo può essere fatto tramite Cherry Pick o un merge accurato.
- Dopo le modifiche, è fondamentale verificare la validità delle azioni effettuate.
In realtà, i meccanismi di Liquibase consentono di avere nel repository due changset della versione 4, quindi si può lasciare tutto com'è. Ovvero, avrete semplicemente due modifiche della versione 4 con nomi diversi. Con questo approccio, in seguito diventa molto difficile orientarsi nelle versioni del database.
Inoltre, Liquibase, come la dimora degli hobbit, custodisce molti segreti. Uno di questi è la chiave validCheckSum, introdotta con la versione 1.7, che consente di specificare un valore di checksum valido per un determinato changeSet, indipendentemente da ciò che è memorizzato nel database. La documentazione afferma quanto segue:
Aggiungi un checksum considerato valido per questo changeSet, indipendentemente da quello che è memorizzato nel database. Usato principalmente quando è necessario modificare un changeSet e non si vogliono errori nei database su cui è già stato eseguito (procedura non raccomandata)
Sì, questa procedura non è consigliata. Ma a volte un potente mago chiaro padroneggia anche tecniche oscure.
Situazione 2. Migrazione che dipende dai dati.

Supponiamo che non sia possibile utilizzare backup dei database dai server in produzione. Petya ha creato un changeSet, lo ha testato localmente e con piena fiducia nella sua correttezza ha fatto una pull request nello sviluppo. Il leader del progetto ha verificato, giusto per essere sicuro, se Petya l'aveva controllato, e poi ha fuso il tutto. Ma il deployment sul server di sviluppo è fallito.
In realtà, è possibile, e nessuno è immune da ciò. Questo accade quando le modifiche alla struttura delle tabelle sono in qualche modo legate a dati specifici del database. È ovvio che se il database di Piero è riempito solo con dati di test, potrebbe non coprire tutti i casi problematici. Ad esempio, quando si elimina una tabella, si scopre che ci sono registrazioni in altre tabelle collegate tramite chiavi esterne (Foreign Key) ai record eliminati. Oppure, quando si modifica il tipo di colonna, emerge che non il 100% dei dati può essere convertito nel nuovo tipo.
Come procedere
- Scrivere script speciali che verranno applicati una sola volta insieme alla migrazione e porteranno i dati nel formato corretto. Questo è un modo generale per risolvere il problema del trasferimento dei dati in nuove strutture già dopo l'applicazione delle migrazioni, ma qualcosa di simile può essere applicato anche prima, in casi particolari. Questo approccio, ovviamente, non è sempre disponibile, poiché modificare i dati su server attivi può essere pericoloso e persino disastroso.
- Un'altra soluzione complessa è modificare il changelog esistente. La difficoltà consiste nel fatto che tutte le basi dati in cui è stato applicato nella sua attuale forma dovranno essere ripristinate. È molto probabile che l'intero team backend sarà costretto a reinstallare la base dati localmente da zero.
- La soluzione più universale è trasferire il problema dei dati sull'ambiente dello sviluppatore, ricreando la stessa situazione e aggiungendo un nuovo changelog, che supererà il problema.

In generale, più la base di dati è simile a quella del server di produzione, minori saranno le probabilità che i problemi di migrazione si verifichino in modo significativo. E, naturalmente, prima di inviare il changelog nel repository, vale la pena pensarci su più volte per assicurarsi che non danneggi nulla.
Situazione 3. Liquibase inizia ad essere utilizzato solo dopo il passaggio in produzione.
Immaginiamo che il team leader abbia chiesto a Petyà di integrare Liquibase nel progetto, tuttavia il progetto è già in produzione e c'è già una struttura della base dati esistente.
Di conseguenza, il problema è che su qualsiasi nuovo server o macchina degli sviluppatori, i dati di queste tabelle devono essere ricreati da zero, mentre l'ambiente esistente deve rimanere in uno stato consistente, pronto ad accogliere nuovi changelog.
Come procedere
Qui ci sono anche diversi percorsi:
- Il primo e più ovvio è avere uno script separato che deve essere applicato manualmente durante l'inizializzazione di un nuovo ambiente.
- Il secondo, meno ovvio, è avere una migrazione di Liquibase che si trova in un altro contesto di Liquibase e applicarla. Maggiori dettagli su Liquibase Context possono essere letti qui: . In generale, è un meccanismo interessante che può essere applicato con successo, ad esempio, per il testing.
- Il terzo approccio consiste in diversi passaggi. Prima di tutto, è necessario creare una migrazione per le tabelle già esistenti. Successivamente, deve essere applicata in un ambiente specifico, in modo da ottenere il suo checksum. Il passaggio successivo consiste nell'inizializzare le tabelle di Liquibase vuote sul nostro server non vuoto, e nella tabella con la cronologia delle applicazioni dei changeset si può manualmente inserire un record di un changeset 'in quanto applicato' con le modifiche già presenti nel database. In questo modo, sulla server esistente, la cronologia inizierà dalla versione 2, mentre tutti i nuovi ambienti si comporteranno in modo identico.

Situazione 4. Le migrazioni diventano enormi e non riescono a essere completate in tempo.
All'inizio dello sviluppo del servizio, di solito si utilizza Liquibase come dipendenza esterna, e tutte le migrazioni vengono elaborate all'avvio dell'applicazione. Tuttavia, col passare del tempo, potresti incontrare i seguenti casi:
- Le migrazioni diventano enormi e richiedono molto tempo per essere completate.
- Si presenta la necessità di eseguire migrazioni in ambienti distribuiti, ad esempio, su più istanze di server di database contemporaneamente.
In questo caso, l'applicazione prolungata delle migrazioni porterà a un timeout all'avvio dell'applicazione. Inoltre, applicare le migrazioni per ogni istanza dell'applicazione separatamente può causare che diversi server si trovino in uno stato non sincronizzato.
Come procedere
In questi casi, il tuo progetto è già grande, forse anche maturo, e Liquibase inizia a funzionare come uno strumento esterno separato. Infatti, Liquibase come libreria viene assemblato in un file jar e può funzionare sia come dipendenza all'interno del progetto, sia in modo autonomo.
In modalità autonoma, puoi delegare l'applicazione delle migrazioni al tuo ambiente CI/CD o ai robusti sistemi dei tuoi amministratori di sistema o specialisti nel dispiegamento. Per questo, sarà necessaria la riga di comando di Liquibase. . In questa modalità è possibile avviare l'applicazione solo dopo che tutte le migrazioni necessarie sono state eseguite.
Risultato
In realtà, ci possono essere molte più insidie quando si lavora con le migrazioni del database, e molte di esse richiedono un approccio creativo. È importante capire che, se si utilizza correttamente lo strumento, la maggior parte di queste insidie può essere evitata. Personalmente, ho affrontato in vari modi tutti i problemi elencati, e alcuni di essi sono stati il risultato dei miei errori. Questo accade principalmente per distrazione, ma a volte anche per un'incapacità criminale nel utilizzare lo strumento.
Fonte: habr.com


