Obsah
1. Migrace databázového schématu v ekosystému programovacího jazyka Go
2. Balíček github.com/pressly/goose
4. Kontrola instalace nástroje Goose
5. Vytvoření migračního skriptu s příkazem pro vytvoření tabulky
7. Provedení migrace nástrojem Goose
10. Povýšení nebo naopak snížení schématu databáze na zvolenou verzi
11. Zavolání funkcí nástroje Goose přímo z jazyka Go
12. Výpis aktuální verze databáze a stavu migrací
13. Provedení migrace přímo z jazyka Go
14. Zavolání libovolného příkazu nástroje Goose z jazyka Go
15. Zápis migračních skriptů přímo v jazyce Go
16. Rozšíření příkazu goose o realizaci migrací zapsaných v jazyce Go
17. Provedení migrace z nativní aplikace napsané v jazyce Go
18. Krátké doplnění: migrace nad databází uloženou v PostgreSQL
19. Repositář s demonstračními příklady
1. Migrace databázového schématu v ekosystému programovacího jazyka Go
V prakticky každé aplikaci, komplexní službě nebo naopak mikroslužbě, která ukládá data do relační databáze, je nutné nějakým způsobem řešit problematiku migrací této databáze na nové schéma popř. naopak migrací zpět na starší schéma (a to v naprosté většině případů zahrnuje i migraci dat, která jsou již v databázi uložena). Pro tyto účely již bylo vytvořeno poměrně velké množství nástrojů. Příkladem takového nástroje aplikovaného především v ekosystému programovacího jazyka Python je nástroj nazvaný Alembic, jenž byl vytvořen autory známého a dosti často používaného databázového toolkitu SQLAlchemy.
V dnešním článku se ovšem zaměříme na ekosystém programovacího jazyka Go. I pro Go pochopitelně existují nástroje zajišťující migraci databáze. S jedním z těchto nástrojů, který se jmenuje Goose, se seznámíme v dnešním článku. A v článku navazujícím se pak podíváme na nástroj s všeříkajícím názvem Migrate (ten je opět určený pro Go a v mnohém se podobá nástroji Goose).
2. Balíček github.com/pressly/goose
Jak již bylo napsáno v úvodní kapitole, slouží nástroj Goose k podpoře migrace databázového schématu a dat. Určen je primárně pro použití s relačními databázemi, mezi nimiž nechybí PostgreSQL, MySQL či SQLite. Jednotlivé kroky migrace – ty jsou pochopitelně verzovány – přitom mohou obsahovat jak krok nutný pro povýšení schématu o jeden krok, tak i o jeho snížení o jeden krok. Tím pádem může být zajištěna situace, kdy je nutné migraci vrátit o krok či o více kroků zpět (což se v našem konkrétním případě stalo několikrát). Jednotlivé kroky migrace přitom mohou být zapsány přímo v programovacím jazyku Go, což na jednu stranu může vypadat poněkud zvláštně, ovšem umožňuje nám to například import číselníků z různých souborových formátů atd.
Goose podporuje migraci většího množství databází (pokud má k dispozici příslušné ovladače), například:
- postgres
- mysql
- sqlite3
- mssql
- redshift
- tidb
- clickhouse
- vertica
3. Instalace balíčku Goose
Samotná instalace balíčku Goose je snadná. Předpokladem pochopitelně je, že máte nainstalovány a nakonfigurovány základní nástroje programovacího jazyka Go:
$ go install github.com/pressly/goose/v3/cmd/goose@latest
Goose má poměrně velké množství závislostí, což je patrné z následujícího výpisu:
go: downloading github.com/pressly/goose v2.7.0+incompatible go: downloading github.com/pressly/goose/v3 v3.10.0 go: downloading github.com/go-sql-driver/mysql v1.7.0 go: downloading github.com/denisenkom/go-mssqldb v0.12.3 go: downloading github.com/ClickHouse/clickhouse-go/v2 v2.7.0 go: downloading github.com/jackc/pgx/v4 v4.18.1 go: downloading github.com/vertica/vertica-sql-go v1.3.1 go: downloading github.com/ziutek/mymysql v1.5.4 go: downloading modernc.org/sqlite v1.21.0 go: downloading github.com/jackc/pgconn v1.14.0 go: downloading github.com/jackc/pgtype v1.14.0 go: downloading github.com/jackc/pgio v1.0.0 go: downloading github.com/jackc/pgproto3/v2 v2.3.2 go: downloading github.com/ClickHouse/ch-go v0.53.0 go: downloading github.com/andybalholm/brotli v1.0.5 go: downloading github.com/pkg/errors v0.9.1 go: downloading go.opentelemetry.io/otel/trace v1.14.0 go: downloading github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 go: downloading github.com/golang-sql/sqlexp v0.1.0 go: downloading golang.org/x/crypto v0.7.0 go: downloading github.com/jackc/chunkreader/v2 v2.0.1 go: downloading github.com/jackc/pgpassfile v1.0.0 go: downloading github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a go: downloading go.opentelemetry.io/otel v1.14.0 go: downloading golang.org/x/text v0.8.0 go: downloading github.com/google/uuid v1.3.0 go: downloading github.com/paulmach/orb v0.9.0 go: downloading github.com/shopspring/decimal v1.3.1 go: downloading gopkg.in/yaml.v3 v3.0.1 go: downloading github.com/go-faster/city v1.0.1 go: downloading github.com/go-faster/errors v0.6.1 go: downloading github.com/klauspost/compress v1.16.0 go: downloading github.com/pierrec/lz4/v4 v4.1.17 go: downloading github.com/segmentio/asm v1.2.0 go: downloading github.com/elastic/go-sysinfo v1.9.0 go: downloading golang.org/x/sys v0.6.0 go: downloading github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 go: downloading howett.net/plist v1.0.0 go: downloading github.com/prometheus/procfs v0.9.0 go: downloading modernc.org/libc v1.22.3 go: downloading github.com/mattn/go-isatty v0.0.17 go: downloading github.com/dustin/go-humanize v1.0.1 go: downloading modernc.org/mathutil v1.5.0 go: downloading modernc.org/memory v1.5.0 go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
4. Kontrola instalace nástroje Goose
Po instalaci se přesvědčíme, do jakého adresáře byl nástroj Goose nainstalován (tento adresář by měl být uložen na $PATH, jak je to obvyklé):
$ whereis goose goose: /home/ptisnovs/go/bin/goose
A nakonec se přesvědčíme, zda je možné Goose spustit:
$ goose Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND or Set environment key GOOSE_DRIVER=DRIVER GOOSE_DBSTRING=DBSTRING Usage: goose [OPTIONS] COMMAND Drivers: postgres mysql sqlite3 mssql redshift tidb clickhouse vertica Examples: goose sqlite3 ./foo.db status goose sqlite3 ./foo.db create init sql goose sqlite3 ./foo.db create add_some_column sql goose sqlite3 ./foo.db create fetch_user_data go goose sqlite3 ./foo.db up goose postgres "user=postgres dbname=postgres sslmode=disable" status goose mysql "user:password@/dbname?parseTime=true" status goose redshift "postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db" status goose tidb "user:password@/dbname?parseTime=true" status goose mssql "sqlserver://user:password@dbname:1433?database=master" status goose clickhouse "tcp://127.0.0.1:9000" status goose vertica "vertica://user:password@localhost:5433/dbname?connection_load_balance=1" status GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose status GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose create init sql GOOSE_DRIVER=postgres GOOSE_DBSTRING="user=postgres dbname=postgres sslmode=disable" goose status GOOSE_DRIVER=mysql GOOSE_DBSTRING="user:password@/dbname" goose status GOOSE_DRIVER=redshift GOOSE_DBSTRING="postgres://user:password@qwerty.us-east-1.redshift.amazonaws.com:5439/db" goose status Options: -allow-missing applies missing (out-of-order) migrations -certfile string file path to root CA's certificates in pem format (only support on mysql) -dir string directory with migration files (default ".") -h print help -no-color disable color output (NO_COLOR env variable supported) -no-versioning apply migration commands with no versioning, in file order, from directory pointed to -s use sequential numbering for new migrations -ssl-cert string file path to SSL certificates in pem format (only support on mysql) -ssl-key string file path to SSL key in pem format (only support on mysql) -table string migrations table name (default "goose_db_version") -v enable verbose mode -version print version Commands: up Migrate the DB to the most recent version available up-by-one Migrate the DB up by 1 up-to VERSION Migrate the DB to a specific VERSION down Roll back the version by 1 down-to VERSION Roll back to a specific VERSION redo Re-run the latest migration reset Roll back all migrations status Dump the migration status for the current DB version Print the current version of the database create NAME [sql|go] Creates new migration file with the current timestamp fix Apply sequential ordering to migrations
5. Vytvoření migračního skriptu s příkazem pro vytvoření tabulky
Kostra migračního skriptu (napsaného přímo v jazyce SQL) se vytvoří následujícím příkazem:
$ goose create users sql 2023/07/08 11:39:41 Created new file: 20230708113941_users.sql
Povšimněte si, že jméno souboru se skriptem obsahuje časové razítko, čímž je stanoveno jeho pořadí.
Podívejme se nyní na strukturu tohoto skriptu. Obsahuje několik „strukturovaných poznámek“, které určují, který blok SQL příkazů se použije pro migraci databáze na vyšší verzi (goose Up) a který blok naopak na verzi nižší (goose Down):
-- +goose Up -- +goose StatementBegin SELECT 'up SQL query'; -- +goose StatementEnd -- +goose Down -- +goose StatementBegin SELECT 'down SQL query'; -- +goose StatementEnd
Nyní je nutné skript upravit, tj. přidat do něj příkaz/příkazy pro migraci na vyšší verzi a pokud je to možné, tak i příkaz/příkazy pro migraci na verzi nižší. Náš první migrační skript vytvoří tabulku a při přechodu na nižší verzi ji zase odstraní (což znamená, že se můžeme pohybovat vpřed i zpět na časové ose s verzemi databáze):
-- +goose Up -- +goose StatementBegin CREATE TABLE users ( id INTEGER NOT NULL, name VARCHAR NOT NULL, surname VARCHAR NOT NULL, PRIMARY KEY (id) -- +goose StatementEnd -- +goose Down -- +goose StatementBegin DROP TABLE users; -- +goose StatementEnd
6. Výchozí stav databáze a migrace
Výchozí stav databáze a migrace zjistíme snadno – příkazý status a version. Povšimněte si, že nástroji Goose musíme předat jak informaci o typu databáze (v našem případě se bude jednat o databázi SQLite), tak i řetězec zajišťující připojení k databázi. Ten se liší podle typu databáze, ovšem v SQLite je to snadné – jedná se o cestu k souboru s databází. Pokud soubor neexistuje (což je i náš případ) je při prvním přístupu k němu vytvořen:
$ goose sqlite3 ./test.db status 2023/07/08 11:45:45 Applied At Migration 2023/07/08 11:45:45 ======================================= 2023/07/08 11:45:45 Pending -- 20230708113941_users.sql
a:
$ goose sqlite3 ./test.db version 2023/07/09 09:10:21 goose: version 0
7. Provedení migrace nástrojem Goose
Nyní by v aktuálním adresáři měla existovat dvojice souborů:
test.db 20230708113941_users.sql
První soubor obsahuje vlastní databázi (SQLite) a druhý migrační skript.
Migraci na nejnovější verzi databáze provedeme následujícím příkazem:
$ goose sqlite3 ./test.db up 2023/07/08 11:46:59 OK 20230708113941_users.sql (3.61ms) 2023/07/08 11:46:59 goose: no migrations to run. current version: 20230708113941
Podívejme se nyní, zda migrace skutečně proběhla. Otevřeme si konzoli databáze SQLite s námi používanou databází:
$ sqlite3 test.db SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints. sqlite>
Dále si vypíšeme tabulky, které se v databázi nachází. Měla by se vypsat tabulka users vytvořená v rámci migrace a taktéž tabulka goose_db_version obsahující informaci o verzi databáze:
sqlite> .tables goose_db_version users
Samozřejmě nás budou zajímat i schémata obou tabulek:
sqlite> .schema goose_db_version CREATE TABLE goose_db_version ( id INTEGER PRIMARY KEY AUTOINCREMENT, version_id INTEGER NOT NULL, is_applied INTEGER NOT NULL, tstamp TIMESTAMP DEFAULT (datetime('now')) ); sqlite> .schema users CREATE TABLE users ( id INTEGER NOT NULL, name VARCHAR NOT NULL, surname VARCHAR NOT NULL, PRIMARY KEY (id) );
Tabulka users by měla být prázdná (v rámci migrace jsme do ní nepřidali žádné záznamy), ovšem tabulka goose_db_version by měla obsahovat verzi/verze databáze:
sqlite> select * from goose_db_version ; 1|0|1|2023-07-08 09:46:57 2|20230708113941|1|2023-07-08 09:46:59
Nyní je tedy databáze povýšena na verzi 20230708113941.
8. Návrat zpět
Velkou předností použití migračních nástrojů oproti ah-hoc změnám v databázi (například přes její SQL konzoli) je fakt, že pokud je migrační skript napsán korektně, umožňuje nám návrat zpět do stavu před migrací. A v případě, že jsou takto napsány všechny migrační skripty, můžeme databázi připravit do libovolného stavu – verze (je to sice zpočátku pracnější, ovšem většinou se nám tato časová investice vyplatí).
Návrat databáze do prvotní verze zajišťuje příkaz down, který si můžeme ihned otestovat:
$ goose sqlite3 ./test.db down 2023/07/08 11:50:43 OK 20230708113941_users.sql (1.7ms)
Opět se přesvědčíme o stavu databáze. Otevřeme si její konzoli:
$ sqlite3 test.db SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints.
Následně si necháme vypsat všechny tabulky. Měla by se vypsat jediná tabulka – goose_db_version:
sqlite> .tables goose_db_version sqlite> select * from goose_db_version ; 1|0|1|2023-07-08 09:46:57
Databáze se tedy nyní nachází v počátečním stavu (až na existenci výše zmíněné tabulky spravované přímo nástrojem Goose).
9. Vícekroková migrace
Typicky se v průběhu vývoje aplikace migrační skripty rozrostou, mnohdy i na několik desítek (možná i stovek). Zkusme si nyní vytvořit druhý migrační skript, v němž do existující tabulky přidáme nový sloupec:
$ goose create role-column sql 2023/07/08 11:55:39 Created new file: 20230708115539_role_column.sql
Původní obsah tohoto skriptu je stále stejný, ovšem upravíme ho do takové podoby, aby se do tabulky přidal nový sloupec popř. při návratu zpět na nižší verzi aby se tento sloupec naopak odstranil:
-- +goose Up -- +goose StatementBegin ALTER TABLE users ADD COLUMN role VARCHAR; -- +goose StatementEnd -- +goose Down -- +goose StatementBegin ALTER TABLE users DROP COLUMN role; -- +goose StatementEnd
Aktuální stav migrací zjistíme příkazem status:
$ goose sqlite3 ./test.db status 2023/07/08 11:58:30 Applied At Migration 2023/07/08 11:58:30 ======================================= 2023/07/08 11:58:30 Pending -- 20230708113941_users.sql 2023/07/08 11:58:30 Pending -- 20230708115539_role_column.sql
Nyní provedeme migraci na nejvyšší verzi databáze:
$ goose sqlite3 ./test.db up 2023/07/08 11:58:34 OK 20230708113941_users.sql (10ms) 2023/07/08 11:58:34 OK 20230708115539_role_column.sql (477.42µs) 2023/07/08 11:58:34 goose: no migrations to run. current version: 20230708115539
A při pohledu na schéma tabulky users bychom měli vidět, že do tabulky byl skutečně přidán nový sloupec nazvaný role:
sqlite> .schema users CREATE TABLE users ( id INTEGER NOT NULL, name VARCHAR NOT NULL, surname VARCHAR NOT NULL, role VARCHAR, PRIMARY KEY (id) );
10. Povýšení nebo naopak snížení schématu databáze na zvolenou verzi
S využitím příkazů up-to nebo down-to je možné provést migraci databáze na zvolenou verzi, protože za tímto příkazem se zadává ta verze databáze, na kterou se má provést její povýšení či naopak snížení. V našem konkrétním případě si můžeme zvolit tři verze databáze:
- 1
- 20230708113941
- 20230708115539
Pokud například budeme chtít snížit verzi databáze na 20230708113941, provedeme to následujícím způsobem:
$ goose sqlite3 ./test.db down-to 20230708113941 2023/07/08 12:02:58 OK 20230708115539_role_column.sql (7.95ms) 2023/07/08 12:02:58 goose: no migrations to run. current version: 20230708113941
A pochopitelně je opět vhodné se podívat na aktuální stav databáze a ověřit si, zda a jak migrace proběhla:
$ sqlite3 test.db SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints. sqlite> .schema users CREATE TABLE users ( id INTEGER NOT NULL, name VARCHAR NOT NULL, surname VARCHAR NOT NULL, PRIMARY KEY (id) );
11. Zavolání funkcí nástroje Goose přímo z jazyka Go
Samotný nástroj Goose je naprogramovaný v jazyce Go. To by pro koncového uživatele nebyla nijak zásadní a vlastně ani potřebná informace, ovšem pro programátora to může mít poměrně velký význam, protože prakticky všechny funkce nabízené nástrojem Goose je možné volat přímo z jazyka Go. To například znamená, že je možné relativně snadno realizovat migrace přímo z vyvíjené aplikace, pochopitelně za předpokladu, že je aplikace vyvíjena právě v jazyce Go. A navíc je možné i samotné migrace zapisovat přímo v jazyce Go, tj. nejsme omezeni na použití SQL skriptů. Tato druhá vlastnost se může zdát neužitečná, ale můžeme ji ocenit při importech dat prováděných v rámci migrace (takovou migrací může být například naplnění tabulky s číselníkem nebo číselníky apod.). Obě výše zmíněné možnosti si ukážeme v navazujících kapitolách.
12. Výpis aktuální verze databáze a stavu migrací
První demonstrační příklad naprogramovaný v jazyce Go zavolá příkaz status nástroje Goose. Povšimněte si, že je nejprve nutné zajistit připojení k databázi volbou typu databáze a řetězce s vhodně naformátovanými informacemi potřebnými k připojení k databázi (connection string). Tento řetězec se pochopitelně liší podle typu databáze a v případě SQLite má podobu řetězce s názvem souboru s databází. Po připojení je možné zavolat funkci goose.Run a předat jí libovolný příkaz nástroje Goose (a samozřejmě případné další parametry vyžadované zvoleným příkazem). My v tomto příkladu použijeme příkaz status bez dalších parametrů:
package main import ( "log" "github.com/pressly/goose/v3" _ "modernc.org/sqlite" ) const databaseType = "sqlite" const databaseFile = "./test.db" const command = "status" const migrationScriptsDirectory = "./" func main() { db, err := goose.OpenDBWithDriver(databaseType, databaseFile) if err != nil { log.Fatalf("goose: failed to open DB: %v\n", err) } defer func() { if err := db.Close(); err != nil { log.Fatalf("goose: failed to close DB: %v\n", err) } }() arguments := []string{} err = goose.Run(command, db, migrationScriptsDirectory, arguments...) if err != nil { log.Fatalf("goose %v: %v", command, err) } }
Výsledek by měl vypadat následovně (pochopitelně s rozdílnými časovými razítky):
2023/07/08 19:02:45 Applied At Migration 2023/07/08 19:02:45 ======================================= 2023/07/08 19:02:45 Sat Jul 8 10:00:59 2023 -- 20230708113941_users.sql 2023/07/08 19:02:45 Pending -- 20230708115539_role_column.sql
13. Provedení migrace přímo z jazyka Go
Naprosto stejným způsobem, pouze modifikací příkazu posílaného do funkce goose.Run, můžeme přímo z programovacího jazyka Go provést migraci. Opět se podívejme na demonstrační příklad, který tuto činnost provádí:
package main import ( "log" "github.com/pressly/goose/v3" _ "modernc.org/sqlite" ) const databaseType = "sqlite" const databaseFile = "./test.db" const command = "up" const migrationScriptsDirectory = "./" func main() { db, err := goose.OpenDBWithDriver(databaseType, databaseFile) if err != nil { log.Fatalf("goose: failed to open DB: %v\n", err) } defer func() { if err := db.Close(); err != nil { log.Fatalf("goose: failed to close DB: %v\n", err) } }() arguments := []string{} err = goose.Run(command, db, migrationScriptsDirectory, arguments...) if err != nil { log.Fatalf("goose %v: %v", command, err) } }
Po prvním spuštění této (přeložené) aplikace by se na terminálu měly objevit informace o provedených migracích:
2023/07/08 19:03:11 Applied At Migration 2023/07/08 19:03:11 ======================================= 2023/07/08 19:03:11 Sat Jul 8 10:00:59 2023 -- 20230708113941_users.sql 2023/07/08 19:03:11 Sat Jul 8 17:03:06 2023 -- 20230708115539_role_column.sql
14. Zavolání libovolného příkazu nástroje Goose z jazyka Go
Nic nám pochopitelně nebrání ve spuštění libovolného příkazu nabízeného nástrojem Goose. Pouze budeme potřebovat zajistit, aby se zvolenému příkazu předaly i všechny jeho parametry. To se provede například následujícím způsobem. Nejprve se přesvědčíme, že je příkaz zadán na příkazové řádce:
args := os.Args if len(args) <= 1 { log.Fatalf("command is expected") return } command := args[1]
Dále si vytvoříme řez s případnými argumenty (řez může být prázdný):
arguments := []string{} if len(args) > 1 { arguments = append(arguments, args[1:]...) }
A nyní nám již jen zbývá zavolat nám již známou funkci goose.Run a příkaz i parametry jí předat:
err = goose.Run(command, db, migrationScriptsDirectory, arguments...)
Úplný kód prakticky plnohodnotné náhrady příkazu goose, pouze s předvyplněnou konfigurací databáze, může vypadat například následovně:
package main import ( "log" "os" "github.com/pressly/goose/v3" _ "modernc.org/sqlite" ) const databaseType = "sqlite" const databaseFile = "./test.db" const migrationScriptsDirectory = "./" func main() { args := os.Args if len(args) <= 1 { log.Fatalf("command is expected") return } command := args[1] db, err := goose.OpenDBWithDriver(databaseType, databaseFile) if err != nil { log.Fatalf("goose: failed to open DB: %v\n", err) } defer func() { if err := db.Close(); err != nil { log.Fatalf("goose: failed to close DB: %v\n", err) } }() arguments := []string{} if len(args) > 1 { arguments = append(arguments, args[1:]...) } err = goose.Run(command, db, migrationScriptsDirectory, arguments...) if err != nil { log.Fatalf("goose %v: %v", command, err) } }
15. Zápis migračních skriptů přímo v jazyce Go
Migrační skripty nemusí být zapisovány jen formou výše zmíněného SQL skriptu s bloky „up“ a „down“. Jednotlivé migrace je totiž možné v případě potřeby i zapsat přímo v jazyce Go. Většinou se pro každý krok migrace vytváří nový zdrojový soubor, který obsahuje sadu funkcí nazvaných UpXXX a DownXXX (jména ovšem mohou být odlišná), kde XXX obsahuje číslo kroku (popř. sémantickou verzi atd.). Tyto funkce jsou volány nástrojem Goose a předává se jim již zahájená databázová transakce, tj. konkrétně struktura splňující rozhraní sql.Tx. A toto rozhraní nabízí všechny potřebné příkazy související s řízením relační databáze. Pokud dojde k chybě, měla by být z takové funkce vrácena (jedná se o jedinou návratovou hodnotu). O uzavření transakce se postará nástroj Goose.
Příkladem může být funkce Up00001, která v rámci transakce vytvoří novou tabulku:
func Up00001(tx *sql.Tx) error { _, err := tx.Exec(` CREATE TABLE users ( id INTEGER NOT NULL, name VARCHAR NOT NULL, surname VARCHAR NOT NULL, PRIMARY KEY (id) ); `) if err != nil { // zde by bylo vhodné přidat logování atd. atd. return err } return nil }
Navíc je ještě nutné funkce provádějící migraci zaregistrovat, protože názvy funkcí provádějících kroky migrace mohou být zvoleny uživatelem. Registraci můžeme provést v rámci speciální funkce init, která je volána automaticky runtime systémem jazyka Go:
func init() { goose.AddMigration(Up00001, Down00001) }
Podívejme se nyní na to, jak by mohl vypadat celý zdrojový kód s definicí jedné migrace (směrem vzhůru k vyšší verzi i směrem dolů k verzi nižší):
package main import ( "database/sql" "github.com/pressly/goose/v3" ) func init() { goose.AddMigration(Up00001, Down00001) } func Up00001(tx *sql.Tx) error { _, err := tx.Exec(` CREATE TABLE users ( id INTEGER NOT NULL, name VARCHAR NOT NULL, surname VARCHAR NOT NULL, PRIMARY KEY (id) ); `) if err != nil { // zde by bylo vhodné přidat logování atd. atd. return err } return nil } func Down00001(tx *sql.Tx) error { _, err := tx.Exec("DROP TABLE users;") if err != nil { // zde by bylo vhodné přidat logování atd. atd. return err } return nil }
Do projektu přidáme i druhý soubor s druhou migrací – přidáním/ubráním sloupce do existující tabulky:
package main import ( "database/sql" "github.com/pressly/goose/v3" ) func init() { goose.AddMigration(Up00002, Down00002) } func Up00002(tx *sql.Tx) error { _, err := tx.Exec("ALTER TABLE users ADD COLUMN role VARCHAR;") if err != nil { // zde by bylo vhodné přidat logování atd. atd. return err } return nil } func Down00002(tx *sql.Tx) error { _, err := tx.Exec("ALTER TABLE users DROP COLUMN role;") if err != nil { // zde by bylo vhodné přidat logování atd. atd. return err } return nil }
16. Rozšíření příkazu goose o realizaci migrací zapsaných v jazyce Go
Celý projekt, který obsahuje migrace psané v jazyku Go, má následující plochou strukturu:
01_table_users.go 02_role_column.go migrations.go go.mod go.sum
První dva soubory jsme si ukázali v rámci předchozí kapitoly. Zajímavé je, že soubor migrations.go se vlastně nemusel nijak změnit a vypadá stejně jako zdrojový kód ze čtrnácté kapitoly. Migrace jsou totiž zaregistrovány přes funkce init a o zbytek se postará nám již dobře známá funkce goose.Run:
package main import ( "log" "os" "github.com/pressly/goose/v3" _ "modernc.org/sqlite" ) const databaseType = "sqlite" const databaseFile = "./test.db" const migrationScriptsDirectory = "./" func main() { args := os.Args if len(args) <= 1 { log.Fatalf("command is expected") return } command := args[1] db, err := goose.OpenDBWithDriver(databaseType, databaseFile) if err != nil { log.Fatalf("goose: failed to open DB: %v\n", err) } defer func() { if err := db.Close(); err != nil { log.Fatalf("goose: failed to close DB: %v\n", err) } }() arguments := []string{} if len(args) > 1 { arguments = append(arguments, args[1:]...) } err = goose.Run(command, db, migrationScriptsDirectory, arguments...) if err != nil { log.Fatalf("goose %v: %v", command, err) } }
17. Provedení migrace z nativní aplikace napsané v jazyce Go
Jak jsme se již dozvěděli v předchozím textu, lze migraci provést z námi vytvořené nativní aplikace naprogramované v jazyce Go. Tuto aplikaci si nejdříve přeložíme ze zdrojových kódů:
$ go build
Díky tomu, že tato aplikace akceptuje všechny příkazy a parametry nástroje Goose, můžeme se dotázat na stav databáze:
$ ./migrations status 2023/07/09 10:10:04 Applied At Migration 2023/07/09 10:10:04 ======================================= 2023/07/09 10:10:04 Pending -- 01_table_users.go 2023/07/09 10:10:04 Pending -- 02_role_columnt.go
Následně můžeme provést vlastní migraci – zapsanou v migračních skriptech v Go:
$ ./migrations up 2023/07/09 10:10:07 OK 01_table_users.go (323.1µs) 2023/07/09 10:10:07 OK 02_role_columnt.go (368.08µs) 2023/07/09 10:10:07 goose: no migrations to run. current version: 2
A opět se můžeme pro jistotu přesvědčit, zda migrace proběhla či nikoli:
$ ./migrations status 2023/07/09 10:10:11 Applied At Migration 2023/07/09 10:10:11 ======================================= 2023/07/09 10:10:11 Sun Jul 9 08:10:07 2023 -- 01_table_users.go 2023/07/09 10:10:11 Sun Jul 9 08:10:07 2023 -- 02_role_columnt.go
18. Krátké doplnění: migrace nad databází uloženou v PostgreSQL
Podívejme se nyní, jak se provádí migrace v případě, že se namísto databáze SQLite používá PostgreSQL. V mém konkrétním případě se jedná o databázi, která běží na počítači se jménem/IP adresou 192.168.1.34, a to konkrétně na standardním portu 5432. Jméno databáze je testdb a uživatel mající přístup do této databáze se jmenuje dbuser. Stav migrací se zjistí následovně (oproti SQLite se vlastně změnil jen connection string):
$ goose postgres "user=dbuser password=topsecret dbname=testdb sslmode=disable host=192.168.1.34 port=5432" status 2023/07/10 17:23:29 Applied At Migration 2023/07/10 17:23:29 ======================================= 2023/07/10 17:23:29 Pending -- 20230708113941_users.sql 2023/07/10 17:23:29 Pending -- 20230708115539_role_column.sql
Ani provedení samotné migrace se nijak zásadně neliší od SQLite:
$ goose postgres "user=dbuser password=topsecret dbname=testdb sslmode=disable host=192.168.1.34 port=5432" up 2023/07/10 17:24:25 OK 20230708113941_users.sql (28.71ms) 2023/07/10 17:24:25 OK 20230708115539_role_column.sql (10.68ms) 2023/07/10 17:24:25 goose: no migrations to run. current version: 20230708115539
Po přihlášení do konzole psql se můžeme přesvědčit, jak databáze vypadá. Nejdříve si vypíšeme všechny dostupné tabulky:
postgres=# \dt List of relations Schema | Name | Type | Owner --------+------------------+-------+---------- public | goose_db_version | table | dbuser public | users | table | dbuser (2 rows)
Podívejme se ještě na strukturu tabulek, které byly vytvořeny nástrojem Goose. První z těchto tabulek bude obsahovat informace o verzi:
postgres=# \d goose_db_version Table "public.goose_db_version" Column | Type | Modifiers ------------+-----------------------------+--------------------------------------------------------------- id | integer | not null default nextval('goose_db_version_id_seq'::regclass) version_id | bigint | not null is_applied | boolean | not null tstamp | timestamp without time zone | default now() Indexes: "goose_db_version_pkey" PRIMARY KEY, btree (id)
A tabulka users byla korektně vytvořena oběma migracemi – první migrace vytvořila tabulku, druhá migrace do tabulky přidala další sloupec role:
postgres=# \d users Table "public.users" Column | Type | Modifiers ---------+-------------------+----------- id | integer | not null name | character varying | not null surname | character varying | not null role | character varying | Indexes: "users_pkey" PRIMARY KEY, btree (id)
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů naprogramovaných v jazyku Go byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root. V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
# | Projekt | Stručný popis | Cesta |
---|---|---|---|
1 | status | zjištění aktuálního stavu migrací | https://github.com/tisnik/go-root/blob/master/article_A2/status/ |
2 | up | migrace databáze na nejvyšší verzi | https://github.com/tisnik/go-root/blob/master/article_A2/up/ |
3 | command | spuštění libovolného příkazu nástroje Go | https://github.com/tisnik/go-root/blob/master/article_A2/command/ |
4 | migrations | migrace prováděné přímo z jazyka Go | https://github.com/tisnik/go-root/blob/master/article_A2/migrations/ |
20. Odkazy na Internetu
- goose: a database migration tool
https://github.com/pressly/goose - Alembic na PyPi
https://pypi.org/project/alembic/ - Welcome to Alembic’s documentation!
https://alembic.sqlalchemy.org/en/latest/ - The Python SQL Toolkit and Object Relational Mapper
https://www.sqlalchemy.org/ - Library (documentation for SQLAlchemy)
https://www.sqlalchemy.org/library.html - SQLAlchemy na Wikipedii
https://en.wikipedia.org/wiki/SQLAlchemy - Schema migration
https://en.wikipedia.org/wiki/Schema_migration - Intro to SqlAlchemy
https://overiq.com/sqlalchemy-101/intro-to-sqlalchemy/ - 6 Database Migration Tools For Complete Data Integrity & More
https://hackernoon.com/6-database-migration-tools-for-complete-data-integrity-more - DB Migration In Go Lang
https://medium.com/geekculture/db-migration-in-go-lang-d325effc55de - Golang Database Migration Tutorial
https://golang.ch/golang-database-migration-tutorial/ - Database migrations written in Go
https://github.com/golang-migrate/migrate - How to do Migration using golang-migrate
https://stackoverflow.com/questions/69054061/how-to-do-migration-using-golang-migrate - Accessing relational databases
https://go.dev/doc/database/ - Evolutionary Database Design
https://martinfowler.com/articles/evodb.html