Ismét a Go fejlesztői eszközkészletet bővítjük. PostgreSQL adatbázist szeretnénk használni és sémamigrációkat kezelni a Go-projektünkben, amit docker-compose és make segítségével viszünk véghez.

Laravel-alkalmazásfejlesztés során épp elégszer használtam már migrációkat, tehát az elvüket és a hasznukat jól ismerem. Az itt bemutatott módszer tulajdonképp nyelv- és környezetfüggetlen.

Először is vegyük elő a PostgreSQL és pgAdmin futtatására összeállított docker-compose.yml konfigfájlunkat, melyet most bővítünk.

version: "3"

services:

    postgres:
        image: postgres:12
        restart: always
        environment:
            POSTGRES_DB: ${DB_DATABASE}
            POSTGRES_USER: ${DB_USER}
            POSTGRES_PASSWORD: ${DB_PASSWORD}
            PGDATA: /var/lib/postgresql/data
        ports:
            - "${DB_PORT}:5432"
        volumes:
            - postgres-data:/var/lib/postgresql/data

    pgadmin:
        image: dpage/pgadmin4
        restart: always
        environment:
            PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL}
            PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
        ports:
            - "${PGADMIN_PORT}:80"
        volumes:
            - pgadmin-data:/var/lib/pgadmin
        depends_on:
            - postgres

    migrate:
        image: migrate/migrate
        restart: "no"
        profiles: ["one-off"]
        volumes:
            - ./migrations:/migrations
        depends_on:
            - postgres

volumes:
    postgres-data:
    pgadmin-data:

Mi változott:

  • A fontos paraméterek helyére változók kerültek, ezek a .env-ből jönnek.
  • Megjelent a migrate konténer.

A következő lépésben elkészítjük a Makefile fájlt, szintén bővítve az egyszerű kezdetit. Itt már jobban kidomborodik a make és a .env értelme.

include .env
DB_URI_PASS = $(shell perl -MURI::Escape -e 'print uri_escape($$ARGV[0])' "${DB_PASSWORD}")
DB_URI = "postgres://${DB_USER}:${DB_URI_PASS}@postgres:${DB_PORT}/${DB_DATABASE}?sslmode=disable"

.PHONY: build run clean pg-up pg-down pg-shell pg-migrate-up pg-migrate-rollback help

build: ## Build the binary. (Default target.)
    GOARCH=amd64 GOOS=linux go build -o ${MAKE_BINARY_NAME} ${MAKE_MAIN_NAME}

run: ## Run the code.
    @go run ${MAKE_MAIN_NAME}

clean: ## Clean up.
    go clean
    rm -f ${MAKE_BINARY_NAME}

pg-up: ## Start the required docker containers.
    docker-compose --file docker-compose.yml --env-file .env \
        up --detach

pg-down: ## Stop the docker containers.
    docker-compose --file docker-compose.yml \
        down

pg-shell: ## Open the psql shell.
    docker-compose --file docker-compose.yml \
        exec postgres \
        psql -U ${DB_USER} -w ${DB_DATABASE}

pg-migrate-up: ## Run the db migrations.
    @docker-compose --file docker-compose.yml \
        run --rm migrate \
        -path /migrations/ -database ${DB_URI} \
        up

pg-migrate-rollback: ## Roll back the last db migration.
    @docker-compose --file docker-compose.yml \
        run --rm migrate \
        -path /migrations/ -database ${DB_URI} \
        down 1

help:
    @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf "\033[36m%-38s\033[0m %s\n", $$1, $$2}' Makefile

Vegyük sorra az újdonságokat:

  • DB_URI_PASS: URL-kódoljuk (percent-encoding) a jelszót a Postgres URI-hoz, amelyet a migrate használ majd. Perl meghívásához szükség van a shell parancsra, valamint az $ARGV[0] escape-elendő, hogy azt a make ne tekintse változónak, ezért szerepel dupla $.
  • pg-up és pg-down: Explicit módon adtuk meg a docker-compose.yml és .env inputokat: noha ezek az alapértelmezettek, itt lehetünk bőbeszédűek. Csak a postgres és a pgadmin konténereket akarjuk indítani, ezek szolgáltatások, melyek a háttérben futnak. Újabb verziójú, 3.9+ compose fájlban a profiles segítségével könnyen szabályozható, mit indítson az up. Ehhez 1.27+ docker-compose kell. Előtte meg kellett adni az up alparancsnak az indítandó szolgáltatások nevét.
  • pg-shell: Van ugyan pgAdmin GUI-nk, de legyen shell elérésünk is. A jelszót környezeti változóból veszi a psql.
  • pg-migrate-up és pg-migrate-rollback: A migrate konténert parancsként használjuk, egyelőre kettőt definiáltunk. Az up a nevükben up-ot tartalmazó sql fájlokat végrehajtva felépíti a sémát, a down a nevükben down-t tartalmazó sql fájlokat végrehajtva lebontja a sémát. Mivel visszavonni szinte mindig csak az utolsó migrációt akarjuk, ezért down 1 lett a rollback.
  • help: Ez egy jó trükk a kommentelt targetek listázására.

A migrate további lehetőségeit illetően lásd a golang-migrate/migrate GitHub-repót, illetve azon belül a PostgreSQL tutoriált.

A migrációkat érdemes lehet idempotens módon elkészíteni1 (if [not] exists), amellett, hogy inkrementálisak és reverzibilisek. Mivel szövegfájlokról van szó, így természetesen verziózhatók is.

A migrációs sql fájlok helyének a ./migrations/ mappát adtuk meg (bind mount). Nézzünk néhány példát:

000001_create_users_table.up.sql:

create table if not exists users (
   id serial primary key,
   email varchar (255) unique not null,
   password varchar(255) not null,
   created_at timestamp default current_timestamp not null
);

000001_create_users_table.down.sql:

drop table if exists users;

000002_add_username_column.up.sql:

alter table users
add column if not exists username varchar(50) unique not null;

000002_add_username_column.down.sql:

alter table users
drop column if exists username;

Végezetül a .env fájlunk tartalma:

MAKE_MAIN_NAME=hello-world.go
MAKE_BINARY_NAME=bin/hello-world

DB_PORT=5432
DB_DATABASE=mydb
DB_USER=root
DB_PASSWORD=root

PGADMIN_PORT=8080
PGADMIN_EMAIL=myemail@address.com
PGADMIN_PASSWORD=root

A .env soha ne kerüljön git repóba,2 vegyük fel a .gitignore-ba. Hozzunk létre egy .env.example fájlt olyan (kamu)adatokkal, amelyek fejlesztéshez megfelelhetnek. Ez alapján könnyen létrehozható a .env.

Tehát mit értünk el? Így néz ki a projektmappánk:

$ tree -a --dirsfirst
.
├── bin
│   └── hello-world
├── migrations
│   ├── 000001_create_users_table.down.sql
│   ├── 000001_create_users_table.up.sql
│   ├── 000002_add_username_column.down.sql
│   └── 000002_add_username_column.up.sql
├── docker-compose.yml
├── .env
├── .env.example
├── hello-world.go
└── Makefile

És például így futtathatjuk a frissen klónozott forrást (telepített go és docker-compose előfeltételekkel):

$ cp .env.example .env && chmod go= .env && nano .env
$ make pg-up
$ make pg-migrate-up
$ make run

Természetesen a Makefile receptjei tetszés szerint módosíthatók, de az ennyiből is látszik, hogy sokat nyertünk.


  1. Eric Hosick: Start Treating Your SQL as a First-Class Language with Idempotent SQL DDL. medium.com, 2022-05-19. ↩︎

  2. Basti Ortiz: Please don’t commit .env. dev.to, 2019-02-03. ↩︎