🐘

PostgreSQL が採番した値を Go で扱おう

2023/10/01に公開

はじめに

エンジニア人生の本格的なアプリケーション開発の第一歩で永続化層を DynamoDB でスタートしたということがあったからなのかこれまでアプリケーション側でIDや時間を採番する手法をとってきました。そしてそれが楽だと思っていました。

しかし「データ指向アプリケーションデザイン」8章 分散システムの問題 を読んでそれはあまりよくないことなのかもしれないと気づきました。

https://www.oreilly.co.jp/books/9784873118703/

じゃあ実際どうすればいいのか、をちょろっと手を動かして確認した記録です。
実装は Go です。

環境準備

uname -a
Darwin MacBook-Pro-7.local 22.6.0 Darwin Kernel Version 22.6.0: Wed Jul  5 22:22:52 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T8103 arm64
go version
go version go1.21.1 darwin/arm64
docker version
Client:
 Cloud integration: v1.0.35-desktop+001
 Version:           24.0.5
 API version:       1.43
 Go version:        go1.20.6
 Git commit:        ced0996
 Built:             Fri Jul 21 20:32:30 2023
 OS/Arch:           darwin/arm64
 Context:           desktop-linux

Server: Docker Desktop 4.22.1 (118664)
 Engine:
  Version:          24.0.5
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.6
  Git commit:       a61e2b4
  Built:            Fri Jul 21 20:35:38 2023
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.6.21
  GitCommit:        3dce8eb055cbb6872793272b4f20ed16117344f8
 runc:
  Version:          1.1.7
  GitCommit:        v1.1.7-0-g860f061
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

動作確認のための PostgreSQL は docker compose で用意します。

comopse.yaml

services:
  postgres:
    container_name: postgres
    image: postgres:15.4-alpine
    ports:
      - 5432:5432
    environment:
      TZ: UTC
      LANG: ja_JP.UTF-8
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
      POSTGRES_HOST_AUTH_METHOD: trust
    restart: always

以下のコマンドにて起動します。

docker compose --project-name postgres --file compose.yaml up -d 

またテーブルは下記の SQL にて用意します。

ddl.sql

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE FUNCTION set_updated_at() RETURNS TRIGGER AS $$
BEGIN
    IF (TG_OP = 'UPDATE') THEN
        NEW.updated_at := now();
        return NEW;
    END IF;
END;
$$ LANGUAGE plpgsql;

CREATE TABLE tests (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    created_at TIMESTAMP (3) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP (3) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
    deleted BOOLEAN DEFAULT FALSE
);

CREATE OR REPLACE TRIGGER trg_tests_updated_at BEFORE UPDATE ON tests FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
docker exec -i postgres psql -U postgres postgres < ddl.sql

これにて準備完了です。

実装

以下の main.go にて実装しました。

package main

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
)

type Test struct {
	ID        uuid.UUID
	CreatedAt time.Time
	UpdatedAt time.Time
	Deleted   bool
}

func main() {
	conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
	if err != nil {
		panic(err)
	}

	defer func() {
		if err := conn.Close(context.Background()); err != nil {
			slog.Warn(err.Error())
		}
	}()

	var res Test

	ins := "INSERT INTO tests DEFAULT VALUES RETURNING id, created_at, updated_at, deleted"

	if err := conn.QueryRow(context.Background(), ins).Scan(&res.ID, &res.CreatedAt, &res.UpdatedAt, &res.Deleted); err != nil {
		panic(err)
	}

	slog.Info(fmt.Sprintf("INSERT: %+v", res))

	upd := "UPDATE tests SET deleted = TRUE WHERE id = $1 RETURNING id, created_at, updated_at, deleted"

	if err := conn.QueryRow(context.Background(), upd, res.ID).Scan(&res.ID, &res.CreatedAt, &res.UpdatedAt, &res.Deleted); err != nil {
		panic(err)
	}

	slog.Info(fmt.Sprintf("UPDATE: %+v", res))
}

接続情報は環境変数にて実行時に渡します。

INSERT 実行時に DEFAULT VALUES を指定してあげるとすべてのカラムをデフォルト値で保存( PostgreSQL での採番)ができるのですね。

https://www.postgresql.org/docs/16/sql-insert.html

INSERT および UPDATE 実行時に RETURNING にて実行結果を取得することができるのですね。

https://www.postgresql.org/docs/current/dml-returning.html

おわりに

PostgreSQL が採番した値はこんなにも簡単に扱えるのですね。
普段 PostgreSQL を使って開発をしていますがこんな単純なことも知らなかったのだと恥ずかしくなりました。一回ドキュメントを一通り読まないとダメですね。
( PostgreSQL の更新日時の自動更新ってトリガー関数を仕込むしか方法ないのですかね ... )

そして pgx で実装してみましたが悪くないですね。
Go の ORM どうすればいいのだろう問題も pgx でいいのかもしれません。

Discussion