Closed4
Go、クリーンアーキテクチャ、トランザクション
Go と クリーンアーキテクチャ と トランザクション
悩むやつ
そして関連の記事は多い
gormの記事多すぎ...
- 依存例
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にクローズされました