Schematy się zmieniają. Zaplanuj to.
Pierwsza wersja schematu nigdy nie jest ostatnia. Dochodzą kolumny, tabele się dzielą, indeksy są przemyślane na nowo. Pytanie nie brzmi, czy schemat się zmieni, tylko czy zmiana gładko trafi na każdy laptop, serwer i urządzenie użytkownika, które mają już starszą kopię bazy.
Do tego służą migracje: ciąg małych, uporządkowanych skryptów, które przenoszą bazę z wersji N do wersji N+1. Uruchamiaj je po kolei, a każda baza dogoni aktualny stan. Zrezygnuj z tej dyscypliny, a skończysz z błędami typu „u mnie działa”, których szukanie zajmuje całe popołudnie.
SQLite daje do tego dokładnie jedno wbudowane narzędzie: PRAGMA user_version. To 32-bitowa liczba całkowita, którą baza przechowuje za ciebie i której sam SQLite nie rusza. To ty decydujesz, co oznacza.
Świeża baza zaczyna od 0. Ustaw wartość na numer migracji, którą właśnie zastosowano. Odczytuj ją przy starcie, żeby wiedzieć, w jakim miejscu jesteś.
Minimalna pętla migracji
Model myślowy: każda migracja to numerowany skrypt SQL. Aplikacja odczytuje bieżące user_version, uruchamia po kolei każdy skrypt z wyższym numerem i po każdym aktualizuje user_version.
Oto migracja 1, która tworzy początkowy schemat:
Warto zauważyć dwie rzeczy. Całość jest opakowana w BEGIN; ... COMMIT;, więc jest atomowa: jeśli CREATE TABLE się nie powiedzie, user_version nie zostanie podbite i możesz poprawić błąd i spróbować ponownie. A PRAGMA user_version = 1 to ostatnia instrukcja przed zatwierdzeniem, więc wersja zmienia się tylko wtedy, gdy wszystko inne się udało.
Załóżmy teraz, że trzeba dodać kolumnę created_at. To migracja 2:
Baza w wersji 0 uruchamia obie. Baza w wersji 1 uruchamia tylko drugą. Baza w wersji 2 nie uruchamia niczego. Kolejność to umowa.
Co ALTER TABLE potrafi, a czego nie
ALTER TABLE w SQLite jest celowo wąskie. Obsługuje:
ADD COLUMN: dodanie nowej kolumny na końcu, z opcjonalną wartością domyślną.DROP COLUMN: usunięcie kolumny (od wersji 3.35).RENAME COLUMN: zmianę nazwy kolumny (od wersji 3.25).RENAME TO: zmianę nazwy samej tabeli.
To wszystko. Nie zmienisz typu kolumny, NOT NULL, ograniczenia CHECK ani nie dodasz FOREIGN KEY do istniejącej kolumny.
-- Nieobsługiwane:
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(255);
ALTER TABLE users ADD CONSTRAINT users_email_check CHECK (email LIKE '%@%');
Gdy potrzebujesz zmiany, której SQLite nie wykona bezpośrednio, oficjalny przepis to „przebuduj tabelę”. Jest bardziej rozwlekły, ale całkowicie niezawodny.
Przebudowa tabeli przy większych zmianach
Schemat jest taki: utwórz nową tabelę o pożądanym kształcie, skopiuj dane, usuń starą i zmień nazwę nowej, żeby zajęła jej miejsce. Wszystko w jednej transakcji.
Pełna dokumentacja SQLite nazywa to przepisem w 12 krokach i dodaje kilka ostrzeżeń dotyczących wyzwalaczy, widoków i odwołań kluczy obcych. Warto ją raz przeczytać, zanim zrobisz to na schemacie produkcyjnym. W większości przypadków wystarczy powyższa wersja w czterech krokach.
Uwaga: jeśli na przebudowywaną tabelę wskazują klucze obce, uruchom PRAGMA foreign_keys = OFF przed migracją i PRAGMA foreign_keys = ON po niej. W przeciwnym razie DROP TABLE może w trakcie złamać integralność referencyjną.
Sterowanie migracjami z aplikacji
Ta księgowość jest na tyle prosta, że możesz ją napisać sam. W Pythonie z biblioteką standardową:
Kluczowe niezmienniki:
- Migracje są numerowane kolejno od 1. Bez luk i bez zmiany kolejności.
- Każda migracja jest opakowana w transakcję razem z podbiciem
PRAGMA user_version = N. - Gdy migracja zostanie zatwierdzona i wydana, nigdy jej nie edytujesz. Nowe zmiany trafiają do nowej migracji.
Tę ostatnią zasadę zespoły łamią najczęściej. Jeśli zmienisz migrację 3 po tym, jak baza współpracownika już ją zastosowała, jego baza na zawsze i po cichu rozjedzie się z twoją.
Zapisywanie historii zmian
user_version mówi, gdzie jest baza. Nie mówi, kiedy uruchomiono każdy krok ani co zrobił. Naprawia to mała tabela ewidencyjna:
Masz teraz wiersz na każdą migrację, z nazwą i znacznikiem czasu. Przydaje się przy debugowaniu w stylu „dlaczego ta baza ma kolumnę, której kod się nie spodziewa?”.
PRAGMA user_version pozostaje źródłem prawdy dla pętli, a tabela jest dla ludzi.
Wycofywanie: co dają transakcje, a czego nie
DDL w SQLite jest transakcyjny. Jeśli migracja 5 zaczyna tworzyć tabelę, kopiować dane i podbijać user_version, a kopiowanie w połowie się nie uda, ROLLBACK cofa wszystko, łącznie z CREATE TABLE. Baza jest dokładnie taka jak przed BEGIN.
To obejmuje migracje nieudane. Nie obejmuje migracji, które zatwierdzono z powodzeniem, a których teraz żałujesz. Do nich piszesz osobną migrację w dół, czyli skrypt, który cofa zmianę. SQLite nie ma automatycznego odwrócenia. Jeśli migracja 7 dodała kolumnę, wersja w dół ją usuwa. Jeśli migracja 7 usunęła kolumnę, wersja w dół nie odzyska danych: najwyżej odtworzy pustą kolumnę.
W praktyce wiele małych projektów całkiem pomija migracje w dół i do „cofania” polega na kopiach zapasowych. To rozsądny wybór, o ile kopie faktycznie się robi.
Kilka nawyków, które oszczędzą kłopotów
- Jedna migracja na jedną logiczną zmianę. Migrację, która dodaje trzy niezwiązane kolumny, trudniej przejrzeć i trudniej cofnąć niż trzy osobne migracje.
- Testuj migracje na kopii produkcji. Zmiany schematu bywają wolne na dużych tabelach, a przekonywanie się o tym na produkcji nie jest przyjemne.
- Nigdy nie edytuj wydanej migracji. Dodaj nową.
- Najpierw kopia zapasowa. Szybkie
.backupw CLI albo skopiowanie pliku przy zamkniętej bazie to tanie ubezpieczenie przed każdą nietrywialną migracją. - Uważaj na
PRAGMA foreign_keys. Wyłącz je na czas przebudowy tabel i włącz z powrotem po niej.
W większych projektach sięgnij po dedykowane narzędzie: Alembic z SQLAlchemy, golang-migrate, Knex, Flyway. Obsługują kolejność, równoległe uruchomienia i konwencje zespołowe, które inaczej trzeba by wymyślać od nowa. Zasady są te same co w pętli powyżej, narzędzie po prostu usuwa powtarzalny kod.
Dalej: tryb WAL i współbieżność
Migracje zwykle działają, gdy aplikacja jest wyłączona albo trzyma blokadę na wyłączność. Przez resztę czasu baza obsługuje odczyty i zapisy z wielu połączeń naraz, a domyślny tryb dziennika w SQLite nie zawsze pasuje do tego najlepiej. Następna strona omawia tryb WAL, co zmienia i kiedy się na niego przełączyć.
Najczęściej zadawane pytania
Jak wersjonować schemat SQLite?
SQLite ma w każdej bazie wbudowane 32-bitowe pole liczbowe o nazwie user_version, dostępne przez PRAGMA user_version. Odczytaj je przy starcie, porównaj z numerem najnowszej migracji znanej twojemu kodowi i uruchom brakujące migracje po kolei. Dodatkowa tabela nie jest potrzebna, choć wiele aplikacji dodaje ją jako dziennik zmian.
Czy można wycofać migrację w SQLite?
Opakuj każdą migrację w BEGIN; ... COMMIT;. Jeśli coś w środku się nie powiedzie, ROLLBACK cofa cały krok, zarówno zmiany schematu, jak i danych, bo DDL w SQLite jest transakcyjny. Żeby wycofać migrację, która już została zatwierdzona, potrzebujesz osobnego skryptu cofającego, napisanego samodzielnie: SQLite go za ciebie nie wygeneruje.
Dlaczego ALTER TABLE w SQLite jest ograniczone?
SQLite obsługuje ALTER TABLE ADD COLUMN, RENAME TABLE, RENAME COLUMN i DROP COLUMN, ale nie dowolne zmiany, takie jak zmiana typu kolumny czy jej ograniczeń. Obejście to przepis w 12 krokach: utwórz nową tabelę o pożądanym kształcie, wykonaj INSERT INTO new_table SELECT ... FROM old_table, usuń starą i zmień nazwę nowej.
Używać narzędzia do migracji czy napisać własne?
W małych aplikacjach ręcznie napisana pętla po numerowanych plikach .sql sterowana przez PRAGMA user_version to może 30 linii kodu i działa bez zarzutu. W większych projektach narzędzia takie jak Alembic (Python), golang-migrate (Go) czy Knex (Node) obsługują kolejność, blokady i pracę zespołową, które inaczej trzeba by wymyślać od nowa.