Closed4

Go、クリーンアーキテクチャ、トランザクション

シロシロ
  • 依存例 service -> repository
  • repository層は、単一リソースに対する処理を行う
  • service層はrepositoryを使ってリソースに対して処理を行う

対応内容

トランザクション用のRepositoryを作成することでスッキリしたのでメモとして残しておく。

// internal/repository/tx.go
package repository

import (
	"context"
	"database/sql"

	"github.com/uptrace/bun"

	// メモ用で ./ としています。通常は、"github.com/username/repo/internal/***" などになります。
	"./internal/ctxkey"
)

type TxRepository struct {
	db *bun.DB
}

func NewTxRepository(db *bun.DB) *TxRepository {
	return &TxRepository{db: db}
}

func (r *TxRepository) DoInTx(ctx context.Context, opts *sql.TxOptions, fn func(ctx context.Context, tx bun.Tx) error) error {
	tx, err := r.db.BeginTx(ctx, opts)
	if err != nil {
		return err
	}

	// bunの `RunInTx`をベースに途中でcontextにトランザクションオブジェクトを入れる処理を追加
	c := context.WithValue(ctx, ctxkey.TxCtxKey, tx)

	var done bool

	defer func() {
		if !done {
			_ = tx.Rollback()
		}
	}()

	if err := fn(c, tx); err != nil {
		return err
	}

	done = true
	return tx.Commit()
}
  • contextのkeyは別で管理(ここはおのおのよしなに)
// internal/ctxkey/ctxkey.go
package ctxkey

type key int

const (
	TxCtxKey key = iota
)

使い方の例

構造体の詳細は省略します。

  • repository層
// internal/repository/event.go
package repository

import (
	"context"

	"github.com/uptrace/bun"

	// メモ用で ./ としています。通常は、"github.com/username/repo/internal/***" などになります。
	"./internal/ctxkey"
	"./internal/entity"
)

type EventRepository struct {
	db *bun.DB
}

func NewEventRepository(db *bun.DB) *EventRepository {
	return &EventRepository{db: db}
}

func (r *EventRepository) Create(ctx context.Context, event *entity.Event) error {
	var inserter *bun.InsertQuery

	// context からトランザクションオブジェクトを取得する
	if tx, ok := ctx.Value(ctxkey.TxCtxKey).(*bun.Tx); ok {
		// トランザクションオブジェクトがある場合は、トランザクションで処理を行う
		inserter = tx.NewInsert()
	} else {
		// トランザクションオブジェクトがない場合は通常の処理を行う
		inserter = r.db.NewInsert()
	}

	// 実行処理は共通化
	if _, err := inserter.Model(event).Exec(ctx); err != nil {
		return err
	}

	return nil
}
  • service層
// internal/service/event.go
package service

import (
	"context"
	"database/sql"

	"github.com/uptrace/bun"

	// メモ用で ./ としています。通常は、"github.com/username/repo/internal/***" などになります。
	"./graph/model"
	"./internal/domain"
	"./internal/entity"
)

type EventService struct {
	// domainには interface を記載してます。
	txRepo domain.TxRepository
	eRepo  domain.EventRepository
}

func NewEventService(txRepo domain.TxRepository, eRepo domain.EventRepository) *EventService {
	return &EventService{txRepo: txRepo, eRepo: eRepo}
}

func (s *EventService) New(ctx context.Context, input model.NewEvent) (*entity.Event, error) {
	event := &entity.Event{
		Name: input.Name,
	}

	// トランザクション処理
	err := s.txRepo.DoInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
		// トランザクションで行う処理を記載する
		if err := s.eRepo.Create(ctx, event); err != nil {
			return err
		}
		// 他のトランザクションで行う処理を記載

		return nil
	})
	if err != nil {
		return nil, err
	}

	return event, nil
}
シロシロ
  • 各Queryを共通化
// internal/repository/common.go
package repository

import (
	"context"

	"github.com/uptrace/bun"

	"github.com/your/repo/internal/ctxkey"
)

func GetInsertQuery(ctx context.Context, db *bun.DB) *bun.InsertQuery {
	if tx, ok := ctx.Value(ctxkey.TxCtxKey).(*bun.Tx); ok {
		return tx.NewInsert()
	}
	return db.NewInsert()
}

func GetUpdateQuery(ctx context.Context, db *bun.DB) *bun.UpdateQuery {
	if tx, ok := ctx.Value(ctxkey.TxCtxKey).(*bun.Tx); ok {
		return tx.NewUpdate()
	}
	return db.NewUpdate()
}

func GetDeleteQuery(ctx context.Context, db *bun.DB) *bun.DeleteQuery {
	if tx, ok := ctx.Value(ctxkey.TxCtxKey).(*bun.Tx); ok {
		return tx.NewDelete()
	}
	return db.NewDelete()
}

func GetSelectQuery(ctx context.Context, db *bun.DB) *bun.SelectQuery {
	if tx, ok := ctx.Value(ctxkey.TxCtxKey).(*bun.Tx); ok {
		return tx.NewSelect()
	}
	return db.NewSelect()
}
  • 使うときは GetXxxxxxQueryを呼び出すだけ!お手軽!
// internal/repository/event.go
package repository

...

func (r *EventRepository) Create(ctx context.Context, event *entity.Event) error {
	inserter := GetInsertQuery(ctx, r.db)

	// 実行処理は共通化
	if _, err := inserter.Model(event).Exec(ctx); err != nil {
		return err
	}

	return nil
}
このスクラップは2023/01/20にクローズされました