1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】repositoryパッケージ完全解説|SQLインターフェース設計と実装ガイド

Last updated at Posted at 2025-04-26

1. 【まずは全体コード】repositoryパッケージの中身

最初に、対象となる完全なコードを提示します。
これが今回解説する対象です。

package repository

import "context"

type SQL interface {
	ExecuteContext(context.Context, string, ...interface{}) error
	QueryContext(context.Context, string, ...interface{}) (Rows, error)
	QueryRowContext(context.Context, string, ...interface{}) Row
	BeginTx(ctx context.Context) (Tx, error)
}

type Rows interface {
	Scan(dest ...interface{}) error
	Next() bool
	Err() error
	Close() error
}

type Row interface {
	Scan(dest ...interface{}) error
}

type Tx interface {
	ExecuteContext(context.Context, string, ...interface{}) error
	QueryContext(context.Context, string, ...interface{}) (Rows, error)
	QueryRowContext(context.Context, string, ...interface{}) Row
	Commit() error
	Rollback() error
}

これから、このコードの各部分について順番に深堀りしていきます。

2. repository.SQLインターフェースの設計意図

まず、SQLインターフェースは、

SQLデータベースを操作するための基本機能を標準化するためのもの

です。

このインターフェースを満たす実装(たとえばPostgreSQL版)を用意すれば、
アプリケーション側は**「PostgreSQLか?MySQLか?SQLiteか?」**を意識せずに使えます。

各メソッドの役割

メソッド名 説明
ExecuteContext INSERT/UPDATE/DELETEを実行する(変更系クエリ)
QueryContext SELECTで複数行の結果を取得する
QueryRowContext SELECTで1行の結果を取得する
BeginTx トランザクションを開始する

→ つまり、データベースを使うために絶対必要な最小限のセットだけをまとめています。

3. RowsRowTxの詳細解説

ここが少し取っつきづらいので、1つずつ順番に見ていきます。

3-1. Rowsインターフェース

type Rows interface {
	Scan(dest ...interface{}) error
	Next() bool
	Err() error
	Close() error
}

🔵 これは「SELECTでたくさんの行を取ったとき」に使うインターフェース。

  • Next()で1行ずつ進む
  • Scan()で今の行のデータを変数に取り込む
  • 読み終わったらClose()
  • エラー確認はErr()

イメージ

rows, _ := db.QueryContext(ctx, "SELECT id, name FROM users")
defer rows.Close()

for rows.Next() {
    rows.Scan(&id, &name)
}

3-2. Rowインターフェース

type Row interface {
	Scan(dest ...interface{}) error
}

🔵 これは「SELECTで1行だけ取ったとき」に使うインターフェース。

  • Scan()だけで完了
  • Next()Close()は不要

イメージ

row := db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", 1)
row.Scan(&id, &name)

3-3. Txインターフェース

type Tx interface {
	ExecuteContext(context.Context, string, ...interface{}) error
	QueryContext(context.Context, string, ...interface{}) (Rows, error)
	QueryRowContext(context.Context, string, ...interface{}) Row
	Commit() error
	Rollback() error
}

🔵 これは「トランザクション中の操作」を定義するインターフェース。

  • ExecuteContextでデータ変更
  • QueryContextで複数行SELECT
  • QueryRowContextで1行SELECT
  • 最後にCommit()で確定 or Rollback()で取り消し

イメージ

tx, _ := db.BeginTx(ctx)
defer tx.Rollback()

tx.ExecuteContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", 100, 1)
tx.ExecuteContext(ctx, "UPDATE accounts SET balance = balance + $1 WHERE id = $2", 100, 2)

tx.Commit()

4. この設計のメリット

この設計には大きなメリットがいくつもあります。

メリット 説明
データベース非依存 PostgreSQL、MySQL、SQLite何でも対応できる
テストが楽 モック実装を作ればDBなしでテストできる
クリーンアーキテクチャに合う ドメイン層とデータ層をしっかり分離できる
保守・拡張しやすい 将来別DBに乗り換えても影響最小限

5. 【実例】PostgreSQL版の具体的な実装

ここからは、実際にPostgreSQL用にこのインターフェースを実装したコードを解説していきます。

5-1. 全体コード

package database

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

	"github.com/gsabadini/go-clean-architecture/adapter/repository"

	_ "github.com/lib/pq"
)

type postgresHandler struct {
	db *sql.DB
}

func NewPostgresHandler(c *config) (*postgresHandler, error) {
	var ds = fmt.Sprintf(
		"host=%s port=%s user=%s dbname=%s sslmode=disable password=%s",
		c.host,
		c.port,
		c.user,
		c.database,
		c.password,
	)

	fmt.Println(ds)
	db, err := sql.Open(c.driver, ds)
	if err != nil {
		return &postgresHandler{}, err
	}

	err = db.Ping()
	if err != nil {
		log.Fatalln(err)
	}

	return &postgresHandler{db: db}, nil
}

func (p postgresHandler) BeginTx(ctx context.Context) (repository.Tx, error) {
	tx, err := p.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return postgresTx{}, err
	}

	return newPostgresTx(tx), nil
}

func (p postgresHandler) ExecuteContext(ctx context.Context, query string, args ...interface{}) error {
	_, err := p.db.ExecContext(ctx, query, args...)
	if err != nil {
		return err
	}

	return nil
}

func (p postgresHandler) QueryContext(ctx context.Context, query string, args ...interface{}) (repository.Rows, error) {
	rows, err := p.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}

	row := newPostgresRows(rows)

	return row, nil
}

func (p postgresHandler) QueryRowContext(ctx context.Context, query string, args ...interface{}) repository.Row {
	row := p.db.QueryRowContext(ctx, query, args...)

	return newPostgresRow(row)
}

type postgresRow struct {
	row *sql.Row
}

func newPostgresRow(row *sql.Row) postgresRow {
	return postgresRow{row: row}
}

func (pr postgresRow) Scan(dest ...interface{}) error {
	if err := pr.row.Scan(dest...); err != nil {
		return err
	}

	return nil
}

type postgresRows struct {
	rows *sql.Rows
}

func newPostgresRows(rows *sql.Rows) postgresRows {
	return postgresRows{rows: rows}
}

func (pr postgresRows) Scan(dest ...interface{}) error {
	if err := pr.rows.Scan(dest...); err != nil {
		return err
	}

	return nil
}

func (pr postgresRows) Next() bool {
	return pr.rows.Next()
}

func (pr postgresRows) Err() error {
	return pr.rows.Err()
}

func (pr postgresRows) Close() error {
	return pr.rows.Close()
}

type postgresTx struct {
	tx *sql.Tx
}

func newPostgresTx(tx *sql.Tx) postgresTx {
	return postgresTx{tx: tx}
}

func (p postgresTx) ExecuteContext(ctx context.Context, query string, args ...interface{}) error {
	_, err := p.tx.ExecContext(ctx, query, args...)
	if err != nil {
		return err
	}

	return nil
}

func (p postgresTx) QueryContext(ctx context.Context, query string, args ...interface{}) (repository.Rows, error) {
	rows, err := p.tx.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}

	row := newPostgresRows(rows)

	return row, nil
}

func (p postgresTx) QueryRowContext(ctx context.Context, query string, args ...interface{}) repository.Row {
	row := p.tx.QueryRowContext(ctx, query, args...)

	return newPostgresRow(row)
}

func (p postgresTx) Commit() error {
	return p.tx.Commit()
}

func (p postgresTx) Rollback() error {
	return p.tx.Rollback()
}

5-2. 実装ポイント解説

それぞれの構造体とメソッドがどのように役割を果たしているか、
実際のコードを引用しながら順番に解説します。

🔵 postgresHandlerSQLインターフェース実装)

この構造体は、通常のDB操作とトランザクション開始を担当します。

📄 ExecuteContext:変更系クエリを実行

func (p postgresHandler) ExecuteContext(ctx context.Context, query string, args ...interface{}) error {
	_, err := p.db.ExecContext(ctx, query, args...)
	return err
}

👉 sql.DB.ExecContextを呼んで、変更系クエリ(INSERT/UPDATE/DELETE)を実行します。

📄 QueryContext:複数行SELECT

func (p postgresHandler) QueryContext(ctx context.Context, query string, args ...interface{}) (repository.Rows, error) {
	rows, err := p.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	return newPostgresRows(rows), nil
}

👉 sql.DB.QueryContextでSELECTを実行し、結果をpostgresRowsにラップして返します。

📄 QueryRowContext:1行SELECT

func (p postgresHandler) QueryRowContext(ctx context.Context, query string, args ...interface{}) repository.Row {
	row := p.db.QueryRowContext(ctx, query, args...)
	return newPostgresRow(row)
}

👉 sql.DB.QueryRowContextを使い、1行だけの結果を取得します。

📄 BeginTx:トランザクション開始

func (p postgresHandler) BeginTx(ctx context.Context) (repository.Tx, error) {
	tx, err := p.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return postgresTx{}, err
	}
	return newPostgresTx(tx), nil
}

👉 新しいトランザクションを開始して、postgresTxとして返します。

🔵 postgresRowRowインターフェース実装)

1行だけのSELECT結果を扱うためのラッパーです。

📄 Scan:取得した1行を構造体などにマッピング

type postgresRow struct {
	row *sql.Row
}

func (pr postgresRow) Scan(dest ...interface{}) error {
	return pr.row.Scan(dest...)
}

👉 Scan()を呼ぶだけで、結果を変数に取り込めます。

🔵 postgresRowsRowsインターフェース実装)

複数行SELECTの結果を順に処理するためのラッパーです。

📄 Next/Scan/Err/Close:行ごとにデータを処理

type postgresRows struct {
	rows *sql.Rows
}

func (pr postgresRows) Scan(dest ...interface{}) error {
	return pr.rows.Scan(dest...)
}

func (pr postgresRows) Next() bool {
	return pr.rows.Next()
}

func (pr postgresRows) Err() error {
	return pr.rows.Err()
}

func (pr postgresRows) Close() error {
	return pr.rows.Close()
}

👉 Next()で次の行へ進み、Scan()でカラム値を取り出し、処理後は必ずClose()します。

🔵 postgresTxTxインターフェース実装)

トランザクション内の一連のDB操作をラップします。

📄 ExecuteContext/QueryContext/QueryRowContext:トランザクション中のクエリ実行

type postgresTx struct {
	tx *sql.Tx
}

func (p postgresTx) ExecuteContext(ctx context.Context, query string, args ...interface{}) error {
	_, err := p.tx.ExecContext(ctx, query, args...)
	return err
}

func (p postgresTx) QueryContext(ctx context.Context, query string, args ...interface{}) (repository.Rows, error) {
	rows, err := p.tx.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	return newPostgresRows(rows), nil
}

func (p postgresTx) QueryRowContext(ctx context.Context, query string, args ...interface{}) repository.Row {
	return newPostgresRow(p.tx.QueryRowContext(ctx, query, args...))
}

👉 通常のSQL操作とほぼ同じ感覚でトランザクション内でも操作できます。

📄 Commit/Rollback:トランザクションの完了

func (p postgresTx) Commit() error {
	return p.tx.Commit()
}

func (p postgresTx) Rollback() error {
	return p.tx.Rollback()
}

👉 成功時はCommit()、失敗時はRollback()で終了処理をします。

🧠【ここまでのまとめ】

実装対象 役割
postgresHandler 通常のSQL操作、トランザクション開始
postgresRow 1行SELECT結果をラップ
postgresRows 複数行SELECT結果をラップ
postgresTx トランザクション中の操作と管理

すべてのラッパーが、
repositoryパッケージのインターフェースに完璧に準拠しています!

6. 総まとめ

このrepositoryパッケージの設計によって、
SQL操作を超シンプル&超柔軟に統一できる世界が実現します。

  • ✅ アプリケーション側はデータベースを意識しなくていい
  • モック実装を作ればDBなしでテスト可能
  • ✅ 将来、別DB(MySQLやSQLite)に移行しても最小限の変更
  • クリーンなコード構成を自然に維持できる
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?