Goで理解するDDD – リポジトリ


ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」書籍がDDDを実践しはじめるのに分かりやすくとてもよいのですが、忘れてしまい何度か読み直しているのでまとめておきます。今回はリポジトリです。

リポジトリとは

リポジトリはデータの保管庫です。直接データの永続化と再構築を行うのではなく、リポジトリを経由して行います。

リポジトリの責務

リポジトリの責務はドメインオブジェクトの永続化や再構築を行うことです。永続化するデータストアが RDB か NoSQL かファイルなのかドメインにとっては重要ではありません。

リポジトリのインターフェース

リポジトリはドメイン層にインターフェースで定義します。リポジトリの責務はあくまでもオブジェクトを永続化することです。

type Repository interface {
    SaveUser(user *model.User) error
    FindUserByName(name string) (*model.User, error)
}

SQL を利用したリポジトリを作成する

インターフェースをうまく活用することで、クラス上には具体的な永続化にまつわる処理を記述せずにデータストアにインスタンスを永続化可能になります。

リポジトリに定義されるふるまい

永続化のふるまいは永続化を行うオブジェクトを引数にとります。

良い例

type Repository interface {
    SaveUser(user *model.User) error
    DeleteUser(user *model.User) error
}

悪い例

type Repository interface {
    UpdateUserByName(id, name string) error
    UpdateUserByEmail(id, email string) error
    UpdateUserByAddress(id, address string) error
}

テスト用のリポジトリを作成する

最後にテスト用のリポジトリを作成してDBユニットテストを実行してみます。

docker-compose.yml

  test_mysql:
    image: mysql:5.7.30
    restart: always
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_USER: user
      MYSQL_ROOT_PASSWORD: root
      MYSQL_PASSWORD: password
      MYSQL_DATABASE: go-ddd-test
    ports:
      - "13307:3306

infrastructure/repository/main_test.go

package repository

import (
    "fmt"
    "log"
    "os"
    "testing"
    "time"

    "github.com/joho/godotenv"
    "github.com/taisa831/go-ddd/domain/model"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

var rdb *gorm.DB

func TestMain(m *testing.M) {
    err := godotenv.Load()
    if err != nil {
        log.Fatal(".env ファイルの読み込みに失敗しました。")
    }

    newLogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags),
        logger.Config{
            SlowThreshold:             time.Second,
            LogLevel:                  logger.Info,
            IgnoreRecordNotFoundError: true,
            Colorful:                  false,
        },
    )

    dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        os.Getenv("USERNAME"), os.Getenv("PASSWORD"), os.Getenv("DBHOST"), os.Getenv("DBPORT"), os.Getenv("SCHEMA"))
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: newLogger,
    })
    if err != nil {
        panic(err)
    }

    sqlDB, err := db.DB()
    if err != nil {
        panic(err)
    }
    defer sqlDB.Close()

    sqlDB.SetMaxOpenConns(151)
    sqlDB.SetMaxIdleConns(100)
    sqlDB.SetConnMaxLifetime(10 * time.Minute)

    err = db.AutoMigrate(&model.User{})
    if err != nil {
        panic(err)
    }
    rdb = db
    code := m.Run()
    os.Exit(code)
}

func truncate(db *gorm.DB) {
    db.Exec("truncate users")
}

infrastructure/repository/user_repository_test.go

ackage repository

import (
    "reflect"
    "testing"

    "github.com/taisa831/go-ddd/domain/model"
    "gorm.io/gorm"
)

func Test_dbRepository_FindUsers(t *testing.T) {
    type fields struct {
        db *gorm.DB
    }
    tests := []struct {
        name          string
        fields        fields
        want          []*model.User
        wantErr       bool
        insertFixture func(db *gorm.DB)
    }{
        {
            name: "FindUsers",
            fields: fields{
                db: rdb,
            },
            want: []*model.User{
                {
                    ID:   "u-1",
                    Name: "name-1",
                },
            },
            wantErr: false,
            insertFixture: func(db *gorm.DB) {
                u := model.User{
                    ID:   "u-1",
                    Name: "name-1",
                }
                if err := db.Create(&u).Error; err != nil {
                    t.Fatal(err)
                }
            },
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            defer truncate(tt.fields.db)
            tt.insertFixture(tt.fields.db)
            r := &dbRepository{
                db: tt.fields.db,
            }
            got, err := r.FindUsers()
            if (err != nil) != tt.wantErr {
                t.Errorf("dbRepository.FindUsers() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("dbRepository.FindUsers() = %v, want %v", got, tt.want)
            }
        })
    }
}

まとめ

  • リポジトリを利用するとデータの永続化にまつわる処理が抽象化できる
  • ドメインのルールに比べると、データストアが何であるかは些末な問題である

 

リポジトリについて簡単にまとめましたが、詳しくは下記書籍にてご確認ください。

 

その他参考書籍


関連記事