Migrace databázového schématu v ekosystému programovacího jazyka Go

18. 7. 2023
Doba čtení: 21 minut

Sdílet

 Autor: Depositphotos
V prakticky každé aplikaci, komplexní službě nebo mikroslužbě, jež ukládá data do relační databáze, je nutné řešit problematiku migrací databáze na nové schéma nebo naopak migrací zpět na starší schéma.

Obsah

1. Migrace databázového schématu v ekosystému programovacího jazyka Go

2. Balíček github.com/pressly/goose

3. Instalace balíčku Goose

4. Kontrola instalace nástroje Goose

5. Vytvoření migračního skriptu s příkazem pro vytvoření tabulky

6. Výchozí stav databáze

7. Provedení migrace nástrojem Goose

8. Návrat zpět

9. Vícekroková migrace

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

20. Odkazy na Internetu

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)
);
Poznámka: z výše uvedeného výsledku je patrné, že jsme skutečně migrovali zpět na druhou dostupnou verzi.

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:

bitcoin_skoleni

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/mi­grations/

20. Odkazy na Internetu

  1. goose: a database migration tool
    https://github.com/pressly/goose
  2. Alembic na PyPi
    https://pypi.org/project/alembic/
  3. Welcome to Alembic’s documentation!
    https://alembic.sqlalchemy­.org/en/latest/
  4. The Python SQL Toolkit and Object Relational Mapper
    https://www.sqlalchemy.org/
  5. Library (documentation for SQLAlchemy)
    https://www.sqlalchemy.or­g/library.html
  6. SQLAlchemy na Wikipedii
    https://en.wikipedia.org/wi­ki/SQLAlchemy
  7. Schema migration
    https://en.wikipedia.org/wi­ki/Schema_migration
  8. Intro to SqlAlchemy
    https://overiq.com/sqlalchemy-101/intro-to-sqlalchemy/
  9. 6 Database Migration Tools For Complete Data Integrity & More
    https://hackernoon.com/6-database-migration-tools-for-complete-data-integrity-more
  10. DB Migration In Go Lang
    https://medium.com/geekculture/db-migration-in-go-lang-d325effc55de
  11. Golang Database Migration Tutorial
    https://golang.ch/golang-database-migration-tutorial/
  12. Database migrations written in Go
    https://github.com/golang-migrate/migrate
  13. How to do Migration using golang-migrate
    https://stackoverflow.com/qu­estions/69054061/how-to-do-migration-using-golang-migrate
  14. Accessing relational databases
    https://go.dev/doc/database/
  15. Evolutionary Database Design
    https://martinfowler.com/ar­ticles/evodb.html

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.