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. Rows
・Row
・Tx
の詳細解説
ここが少し取っつきづらいので、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()
で確定 orRollback()
で取り消し
イメージ
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. 実装ポイント解説
それぞれの構造体とメソッドがどのように役割を果たしているか、
実際のコードを引用しながら順番に解説します。
🔵 postgresHandler
(SQL
インターフェース実装)
この構造体は、通常の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
として返します。
🔵 postgresRow
(Row
インターフェース実装)
1行だけのSELECT結果を扱うためのラッパーです。
📄 Scan:取得した1行を構造体などにマッピング
type postgresRow struct {
row *sql.Row
}
func (pr postgresRow) Scan(dest ...interface{}) error {
return pr.row.Scan(dest...)
}
👉 Scan()
を呼ぶだけで、結果を変数に取り込めます。
🔵 postgresRows
(Rows
インターフェース実装)
複数行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()
します。
🔵 postgresTx
(Tx
インターフェース実装)
トランザクション内の一連の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)に移行しても最小限の変更
- ✅ クリーンなコード構成を自然に維持できる