じゃあ、おうちで学べる

思考を呼び覚ます このコードに、君は抗えるか。

SQLBoilerによるPostgreSQLの操作についての話

はじめに

データベースは、現代のアプリケーション開発において不可欠な要素です。特にリレーショナルデータベースは、その整合性と信頼性から幅広い用途で使用されています。しかし、リレーショナルデータベースを効率的に操作するためには、複雑なSQLクエリを記述し、アプリケーションのコードとデータベースのスキーマを適切に統合する必要があります。この作業は開発者にとって時間と労力を要するものです。

この背景から、ORM(Object-Relational Mapping)ライブラリの利用が一般的になりました。ORMライブラリは、プログラミング言語のオブジェクトとデータベースレコードをマッピングし、SQLクエリの生成を抽象化して開発者がデータベース操作を容易に行えるようにサポートします。この記事では、Go言語でのデータベース操作を効率化するためのORMライブラリ「SQLBoiler」の活用方法について解説します。

SQLBoilerは、Go言語に特化した強力なORMライブラリで、データベーススキーマに基づいてGoのコードを自動生成します。この自動生成機能により、開発者は煩雑なボイラープレートコードの記述を削減し、ビジネスロジックに集中できるようになります。本記事では、SQLBoilerの基本的な使用方法から生成されたコードの実際の利用方法までを段階的に紹介します。それでは、SQLBoilerを活用してGo言語でのデータベース操作を効率化する方法を見ていきましょう。

ORM(Object-Relational Mapping)について

ORMは、リレーショナルデータベースとプログラミング言語の間の橋渡しをする技術です。通常のデータベース操作に使用されるSQLとは異なり、ORMはプログラムのオブジェクト(今回のGoでは構造体)とデータベースレコードを自動で関連付け、SQL文の組み立てを可能にします。

SQLBoilerの利点

SQLBoilerはGo言語に特化した強力なORMライブラリで、データベーススキーマから直接Goのコードを生成します。この自動生成機能により、ボイラープレートコードの削減と開発効率の向上が図れます。ボイラープレートコードとはいくつかの異なるコンテキストでほとんどまたはまったく変更せずに再利用できるコンピュータ言語のテキストのことを指します。

SQLBoilerの使用方法

SQLBoilerを利用する際は、まずデータベーススキーマを定義します。以下は、著者、出版社、利用者、書籍、貸出記録を管理するスキーマの例です。

-- 著者テーブル
create table authors (
  author_id serial primary key,
  name varchar(100) not null
);

-- 出版社テーブル
create table publishers (
  publisher_id serial primary key,
  name varchar(100) not null
);

-- 利用者テーブル
create table users (
  user_id serial primary key,
  family_name varchar(100) not null,
  given_name varchar(100) not null,
  email_address varchar(254) not null,
  registration_date date not null
);

-- メールアドレスに対するユニークキー制約(ユニークインデックス)
create unique index idx_users_email_address on users(email_address);

-- 書籍テーブル
create table books (
  book_id serial primary key,
  title varchar(255) not null,
  author_id integer not null,
  publisher_id integer not null,
  isbn varchar(20),
  year_published integer
);

-- 貸出記録テーブル
create table loans (
  loan_id serial primary key,
  book_id integer not null,
  user_id integer not null,
  loan_date date not null,
  return_date date
);

-- 外部キー制約の追加
alter table books add constraint fk_books_author_id foreign key (author_id) references authors(author_id);
alter table books add constraint fk_books_publisher_id foreign key (publisher_id) references publishers(publisher_id);
alter table loans add constraint fk_loans_book_id foreign key (book_id) references books(book_id);
alter table loans add constraint fk_loans_user_id foreign key (user_id) references users(user_id);

このスキーマをもとに、SQLBoilerはGoのモデル、クエリビルダー、CRUD操作を自動生成します。この自動生成により、開発者は細かなデータベース操作を手作業で行う必要がなく、ビジネスロジックに集中できます。

環境構築

Dockerを使用して、PostgreSQLのバージョン16を動作させる環境を構築します。以下のdocker-compose.yamlファイルを使用してPostgreSQLサーバーを立ち上げます。

version: '3'
services:
  postgres:
    container_name: postgres
    image: postgres:16
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: "postgres"
      POSTGRES_PASSWORD: "postgres"

サンプルデータの投げ込み

v01_insert.sql を作成します

-- 著者テーブルにサンプルデータを挿入
INSERT INTO authors (name) VALUES ('Sample Author 1');
INSERT INTO authors (name) VALUES ('Sample Author 2');
INSERT INTO authors (name) VALUES ('Sample Author 3');

-- 出版社テーブルにサンプルデータを挿入
INSERT INTO publishers (name) VALUES ('Sample Publisher 1');
INSERT INTO publishers (name) VALUES ('Sample Publisher 2');
INSERT INTO publishers (name) VALUES ('Sample Publisher 3');

-- 利用者テーブルにサンプルデータを挿入
INSERT INTO users (family_name, given_name, email_address, registration_date) VALUES ('Yamada', 'Taro', 'taro@example.com', '2021-01-01');
INSERT INTO users (family_name, given_name, email_address, registration_date) VALUES ('Suzuki', 'Hanako', 'hanako@example.com', '2021-02-01');
INSERT INTO users (family_name, given_name, email_address, registration_date) VALUES ('Tanaka', 'Ichiro', 'ichiro@example.com', '2021-03-01');

-- 書籍テーブルにサンプルデータを挿入
INSERT INTO books (title, author_id, publisher_id, isbn, year_published) VALUES ('Sample Book 1', 1, 1, '1234567890', 2021);
INSERT INTO books (title, author_id, publisher_id, isbn, year_published) VALUES ('Sample Book 2', 2, 2, '0987654321', 2020);
INSERT INTO books (title, author_id, publisher_id, isbn, year_published) VALUES ('Sample Book 3', 3, 3, '1122334455', 2022);

-- 貸出記録テーブルにサンプルデータを挿入
INSERT INTO loans (book_id, user_id, loan_date, return_date) VALUES (1, 1, '2022-01-01', '2022-01-15');
INSERT INTO loans (book_id, user_id, loan_date, return_date) VALUES (2, 2, '2022-01-05', '2022-01-20');
INSERT INTO loans (book_id, user_id, loan_date) VALUES (3, 3, '2022-01-10');

投げ込み投げ込み

psql -h localhost -U postgres -d postgres -f v01_insert.sql

SQLBoilerのインストール

SQLBoilerのインストール手順は以下の通りです。

go install github.com/volatiletech/sqlboiler/v4@latest
go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-psql@latest

PostgreSQL接続情報の設定(YAML形式)

psql:
  dbname: "postgres"
  host: "127.0.0.1"
  port: 5432
  user: "postgres"
  pass: "postgres"
  sslmode: "disable"
  whitelist:
    - "authors"
    - "publishers"
    - "users"
    - "books"
    - "loans"

whitelistにはコード生成の対象となるテーブルを明示的に指定します。この例では、著者、出版社、利用者、書籍、貸出記録の各テーブルを指定しています。

SQLBoilerによるコード生成

以下のコマンドを使うことで、SQLBoilerは指定されたデータベーススキーマに基づいてGoのモデルを生成します。

sqlboiler psql -c config/database.yaml -o models --no-tests

このコマンドは、config/database.yamlに指定された設定を使用して、modelsディレクトリ内にモデルファイルを生成します。--no-testsオプションにより、テストファイルの生成をスキップします。

生成されたファイル構成は以下の通りです。

models/
├── authors.go
├── boil_queries.go
├── boil_table_names.go
├── boil_types.go
├── boil_view_names.go
├── books.go
├── loans.go
├── psql_upsert.go
├── publishers.go
└── users.go

生成されたファイルの解説

例として、books.goファイルの一部を見てみましょう。このファイルはデータベースのbooksテーブルに対応するGoの構造体と、それに関連する関数を定義しています。

Book構造体

type Book struct {
    BookID        int         `boil:"book_id" json:"book_id" toml:"book_id" yaml:"book_id"`
    Title         string      `boil:"title" json:"title" toml:"title" yaml:"title"`
    AuthorID      int         `boil:"author_id" json:"author_id" toml:"author_id" yaml:"author_id"`
    PublisherID   int         `boil:"publisher_id" json:"publisher_id" toml:"publisher_id" yaml:"publisher_id"`
    Isbn          null.String `boil:"isbn" json:"isbn,omitempty" toml:"isbn" yaml:"isbn,omitempty"`
    YearPublished null.Int    `boil:"year_published" json:"year_published,omitempty" toml:"year_published" yaml:"year_published,omitempty"`
}

Book構造体はbooksテーブルの各列をフィールドとして持ち、タグを用いてデータベースの列名とのマッピングを定義しています。

SQLBoilerによるコード生成の詳細解説

SQLBoilerによって生成されたモデルファイルは、リレーショナルデータベースと連携するための多様な機能を提供します。ここでは、具体的なコード例を使って、関連関数やリレーションシップ、フックについて解説します。生成されたファイルには、CRUD操作(作成、読み取り、更新、削除)を行うための関数も含まれています。例えば、Insert関数はBookオブジェクトをデータベースに挿入し、Update関数は既存のレコードを更新します。また、Delete関数はレコードを削除し、Reload関数はデータベースから最新の情報を再読み込みします。

関連関数の例

// Insert a single record using an executor.
func (o *Book) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error {
    // ...関数の本体...
}

// Update uses an executor to update the Book.
func (o *Book) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) {
    // ...関数の本体...
}

// Delete deletes a single Book record with an executor.
func (o *Book) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) {
    // ...関数の本体...
}

// Reload refetches the object from the database.
func (o *Book) Reload(ctx context.Context, exec boil.ContextExecutor) error {
    // ...関数の本体...
}

これらの関数は、Bookオブジェクトを使用して、データベースに対する挿入、更新、削除、再読み込みの操作を行います。

リレーションシップの例

// Author pointed to by the foreign key.
func (o *Book) Author(mods ...qm.QueryMod) authorQuery {
    // ...関数の本体...
}

// Publisher pointed to by the foreign key.
func (o *Book) Publisher(mods ...qm.QueryMod) publisherQuery {
    // ...関数の本体...
}

これらの関数は、Bookオブジェクトが参照する外部キー(Author, Publisher)に基づいて、関連するデータを取得するためのクエリを作成します。SQLBoilerはテーブル間のリレーションシップを認識し、それに対応する関数も生成します。例えば、BookAuthorPublisherに関連している場合、それぞれのリレーションシップに対応するLoadAuthorLoadPublisherなどの関数が生成されます。

フックの例

// AddBookHook registers your hook function for all future operations.
func AddBookHook(hookPoint boil.HookPoint, bookHook BookHook) {
    // ...関数の本体...
}

フックを使用すると、データベース操作の前後に特定の処理を実行できます。例えば、AddBookHook関数は、特定のタイミングで実行されるカスタムフックを登録します。SQLBoilerは各CRUD操作の前後に実行されるフック(Hook)もサポートしています。これにより、データベース操作の前後にカスタムロジックを実行することが可能です。

SQLBoilerの応用と実践的な利用方法

Go言語とSQLBoilerを使用して、リレーショナルデータベースでのデータ操作を行う方法を解説します。この記事では、実際のコードを使用して、SQLBoilerで生成されたモデルを利用してデータベースの書籍テーブルを操作する一連のプロセスを紹介します。主にSelect、Insert、Update、Upsert、Delete、Reloadといった基本的なデータベース操作をカバーし、Eager Loadingやデバッグ出力などの高度な機能についても触れます。

以下のサンプルコードは、PostgreSQLデータベースに接続し、複数の異なる操作を実行するGoプログラムです。このプログラムでは、SQLBoilerで生成されたモデルを使用して、書籍の情報を取得、挿入、更新、アップサート、削除し、データベースの状態を再読み込みする操作を行います。

package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq"
    "github.com/nwiizo/workspace_2024/sqlboiler/models" // 生成されたモデルのインポート
    "github.com/volatiletech/null/v8"
    "github.com/volatiletech/sqlboiler/v4/boil"
    "github.com/volatiletech/sqlboiler/v4/queries/qm"
)

func main() {
    // データベース接続
    db, err := sql.Open(
        "postgres",
        "postgres://postgres:postgres@localhost/postgres?sslmode=disable",
    )
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    ctx := context.Background()

    // SELECT: 全ての書籍を取得
    allBooks, err := models.Books().All(ctx, db)
    if err != nil {
        log.Fatal(err)
    }
    for _, book := range allBooks {
        log.Printf("Book: %+v\n", book)
    }

    fmt.Println("Select: 高度なクエリでの書籍の取得")
    books, err := models.Books(
        models.BookWhere.Title.EQ("Specific Title"),
        models.BookWhere.AuthorID.EQ(1),
        qm.Limit(10),
    ).All(ctx, db)
    if err != nil {
        log.Fatal(err)
    }
    for _, book := range books {
        fmt.Println("Book:", book.Title)
    }

    fmt.Println("Count: 書籍の数を数える")
    count, err := models.Books(models.BookWhere.Title.EQ("Specific Title")).Count(ctx, db)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Count:", count)

    fmt.Println("Exists: 特定の条件に一致する書籍が存在するかを確認")
    exists, err := models.Books(models.BookWhere.Title.EQ("Specific Title")).Exists(ctx, db)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Exists:", exists)

    fmt.Println("Insert: 書籍の挿入")
    newBook := &models.Book{
        Title:         "New Book",
        AuthorID:      1,
        PublisherID:   1,
        Isbn:          null.StringFrom("1234567890"),
        YearPublished: null.IntFrom(2023),
    }
    err = newBook.Insert(ctx, db, boil.Infer())
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Update: 書籍の更新")
    newBook.Title = "Updated Title"
    _, err = newBook.Update(ctx, db, boil.Infer())
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Upsert: 書籍のアップサート")
    upsertBook := &models.Book{
        BookID:        newBook.BookID,
        Title:         "Upserted Title",
        AuthorID:      2,
        PublisherID:   2,
        Isbn:          null.StringFrom("0987654321"),
        YearPublished: null.IntFrom(2024),
    }
    err = upsertBook.Upsert(ctx, db, true, []string{"book_id"}, boil.Infer(), boil.Infer())
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Delete: 書籍の削除")
    _, err = newBook.Delete(ctx, db)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Reload: 書籍の再読み込み")
    err = newBook.Reload(ctx, db)
    if err != nil {
        if err == sql.ErrNoRows {
            fmt.Println("Reload: 書籍が見つかりませんでした")
        } else {
            log.Fatal(err)
        }
    }
    // Eager Loading の例
    // ユーザーと関連する書籍を取得
    // user, err := models.FindUser(ctx, db, 1, qm.Load("Books"))
    // if err != nil {
    //     log.Fatal(err)
    // }
    // for _, book := range user.R.Books {
    //     fmt.Println("Book:", book.Title)
    // }

    // デバッグ出力の例
    // boil.DebugMode = true
    // books, _ = models.Books().All(ctx, db)
    // boil.DebugMode = false

    // Raw Query の例
    // _, err = queries.Raw("SELECT * FROM books WHERE title = 'New Book'").QueryAll(ctx, db)
    // if err != nil {
    //     log.Fatal(err)
    // }

    // Hook の例
    // func myBookHook(ctx context.Context, exec boil.ContextExecutor, book *models.Book) error {
    //     fmt.Println("Book Hook Triggered")
    //     return nil
    // }
    // models.AddBookHook(boil.BeforeInsertHook, myBookHook)

    // null パッケージの使用例
    // newBook.Isbn = null.StringFromPtr(nil) // ISBN を null に設定
}

このプログラムでは、まずデータベースに接続し、全ての書籍を取得する操作から始まります。その後、特定の条件に一致する書籍の数を数えたり、特定の条件に一致する書籍が存在するかを確認したりする操作を行います。その後、新しい書籍をデータベースに挿入し、その書籍の情報を更新します。次に、アップサート操作を行い、特定の書籍を削除し、最終的には削除された書籍の情報を再読み込みします。このプロセスは、SQLBoilerを使ってGo言語でデータベース操作を行う際の典型的なフローを示しています。また、コメントアウトしていたコードに関しても一部は解説させてください。

高度なクエリ構築

SQLBoilerでは、qmパッケージを利用して複雑なクエリを組み立てることができます。例えば、特定の条件を満たす書籍を取得するために、以下のようなクエリを構築することが可能です。

books, err := models.Books(
    models.BookWhere.Title.EQ("Specific Title"),
    models.BookWhere.AuthorID.EQ(1),
    qm.Limit(10),
).All(ctx, db)
if err != nil {
    log.Fatal(err)
}
for _, book := range books {
    fmt.Println("Book:", book.Title)
}

このコードは、タイトルが"Specific Title"であり、かつ著者IDが1の書籍を最大10件まで取得します。

Eager Loading

SQLBoilerを使うと、関連するレコードを事前にロードするEager Loadingも可能です。たとえば、あるユーザーに関連するすべての書籍を取得するには、以下のようにします。

user, err := models.FindUser(ctx, db, 1, qm.Load("Books"))
if err != nil {
    log.Fatal(err)
}
for _, book := range user.R.Books {
    fmt.Println("Book:", book.Title)
}

この例では、IDが1のユーザーに関連するすべての書籍を取得しています。

デバッグ出力

SQLBoilerを使用する際、boil.DebugModeを有効にすることで、実行されたSQLクエリを確認することができます。これはデバッグ時に非常に便利です。

boil.DebugMode = true
books, _ := models.Books().All(ctx, db)
boil.DebugMode = false

Raw Queryの使用

SQLBoilerでは、生のSQLクエリを直接実行することも可能です。これは特定のシナリオで必要となる複雑なクエリを実行する際に役立ちます。

_, err = queries.Raw("SELECT * FROM books WHERE title = 'New Book'").QueryAll(ctx, db)
if err != nil {
    log.Fatal(err)
}

このコードは、タイトルが'New Book'のすべての書籍を取得します。

Hookの設定

SQLBoilerでは、データベース操作の前後に実行されるHookを設定することができます。これはデータの整合性を保つための追加のロジックを実行する際に便利です。

func myBookHook(ctx context.Context, exec boil.ContextExecutor, book *models.Book) error {
    fmt.Println("Book Hook Triggered")
    return nil
}
models.AddBookHook(boil.BeforeInsertHook, myBookHook)

この例では、書籍がデータベースに挿入される前に特定の処理を行うHookを設定しています。

nullパッケージの活用

SQLBoilerでは、nullパッケージを利用して、データベースのNULL値を扱うことができます。これにより、NULL許容のフィールドを安全に操作することが可能になります。

newBook.Isbn = null.StringFromPtr(nil) // ISBNをNULLに設定
newBook.YearPublished = null.IntFrom(2023) // 発行年を設定

このコードでは、ISBNをNULLに設定し、発行年を2023に設定しています。

まとめ

SQLBoilerは、Go言語でリレーショナルデータベースを効率的に操作するための強力なORM(Object-Relational Mapping)ライブラリです。データベーススキーマから直接Goのコードを生成し、開発者が細かなデータベース操作を手作業で行う負担を軽減します。SQLBoilerは、CRUD操作、リレーションシップの管理、フックの実装など、多様な機能を提供し、開発者がビジネスロジックに集中できる環境を整えます。

この記事では、SQLBoilerの基本的な使用方法から、生成されたコードの実際の利用方法までを解説しました。まず、データベーススキーマの定義とSQLBoilerの環境設定を行い、サンプルデータの挿入を通じて実際のデータベース操作を準備しました。次に、SQLBoilerによって生成されたGoのモデルを利用して、Select、Insert、Update、Upsert、Delete、Reloadといった一連のデータベース操作を行うプロセスを紹介しました。これらの操作は、リレーショナルデータベースとGoプログラムの間のインタラクションを容易にし、開発プロセスを加速します。

SQLBoilerは、Go言語でのデータベース操作を簡素化し、開発速度を向上させることが可能です。手動でのデータベース操作コードの記述が多い開発者にとって、SQLBoilerは大きな助けとなります。SQLBoilerの詳細や使い方については、SQLBoiler GitHubページを参照してください。

参考資料