GORM まとめ


環境

# mysql --version
mysql  Ver 8.0.23 for Linux on x86_64 (MySQL Community Server - GPL)
$ go version
go version go1.15.7 linux/amd64
$ go mod graph
mymodule gorm.io/driver/[email protected]
mymodule gorm.io/[email protected]

GORM の動作

外部キーの作り方

次のような構造体をAutoMigrateに渡すことで、外部キーが自動生成されます。

type Parent struct {
    gorm.Model
}

type Child struct {
    gorm.Model
    ParentID uint
    Parent   Parent
}

先程のコードは Child から Parent への参照の方向でした。Parent から Child への参照を行いたい場合は、次のように定義します。

type Parent struct {
    gorm.Model
    Child Child
}

type Child struct {
    gorm.Model
    ParentID uint
}

上記コードは「Parent がひとつの Child を持つ」場合の定義です。Parent が複数の Child を持つ場合は、Child ChildChild []Childと書き換えます。

type Parent struct {
    gorm.Model
    Child []Child
}

type Child struct {
    gorm.Model
    ParentID uint
}

外部キーの作り方を3つ紹介しました。すべて「ChildParentIDを持つ」という点で共通しており、異なるのは「参照の方向」と「単数形か複数形か」の2つだけであることがわかります。

多対多

次のように定義することで、多対多のテーブルを自動生成できます。

type User struct {
    gorm.Model
    Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
    gorm.Model
}

次の図が、自動生成されたテーブルとそれらの関係を表します。多対多の情報を持つ user_languages テーブルが作られていることがわかります。

自動生成されたインデックスを見ると、Languageの ID から素早く情報が取得できるようになっていることがわかります。

上記の定義とは対照にLanguage側でUsersを定義すると、自動生成されるインデックスがLanguageの ID からUserの ID に変わります。

type User struct {
    gorm.Model
}

type Language struct {
    gorm.Model
    Users []User `gorm:"many2many:user_languages;"`
}

多対多のテーブルに直接対応する構造体は存在しないため、User(またはLanguage)構造体を使って間接的に操作する必要があります。

languages := []Language{{}, {}}
db.Create(&languages)
user := User{Languages: languages}
db.Create(&user)

user_languages テーブルは次のようになります。

外部キーのフィールド情報の取得

次のコードを実行すると、Parentの情報を含んだChildが得られます。

var children []Child
db.Preload(clause.Associations).Find(&children)
log.Print(children)

clause.Associationsがない場合は、Parentの情報を持たないChildが得られます。

たとえclause.Associationsを使ったとしても、2つ深い情報は取得できません。

type Parent struct {
    gorm.Model
}

type Child struct {
    gorm.Model
    ParentID uint
    Parent   Parent
}

type Grandson struct {
    gorm.Model
    ChildID uint
    Child   Child
}
var grandson Grandson
db.Preload(clause.Associations).Find(&grandson)
log.Print(grandson.Child.Parent)    // 初期値が出力される

2つ以上深い情報を取得するためには、次のように記述します。

var grandson Grandson
db.Preload("Child.Parent").Find(&grandson)
log.Print(grandson.Child.Parent) // 期待した値が出力される

.Preload()に渡す文字列はテーブル名ではなく Go 言語での名前です。

外部キー制約の設定方法

次の場所に、外部キー制約を記述します。

type Parent struct {
    gorm.Model
}

type Child struct {
    gorm.Model
    ParentID uint
    Parent   Parent `gorm:"constraint:OnUpdate:RESTRICT,OnDelete:RESTRICT;"`
}

次の場所に外部キー制約を記述しても反映されないため、注意が必要です。

type Child struct {
    gorm.Model
    ParentID uint `gorm:"constraint:OnUpdate:RESTRICT,OnDelete:RESTRICT;"`
    Parent   Parent
}

外部キー制約のデフォルトの値はRESTRICTであるため、上記のような制約の記述は省略できます。

外部キーと削除

GORM の削除は論理削除であり、外部キー制約に違反するような削除を行っても正常に処理が終了してしまいます。これにより、テーブル結合時に空のフィールドになる恐れがあります。

論理削除ではなく物理削除を行う場合は、次のように.Unscoped()を使います。

var p Parent
db.Find(&p)
db.Unscoped().Delete(&p)

物理削除であるため、外部キー制約に違反した場合は次のようなエラーログが出力されます。

Error 1451: Cannot delete or update a parent row: a foreign key constraint fails (`root`.`children`, CONSTRAINT `fk_children_parent` FOREIGN KEY (`parent_id`) REFERENCES `parents` (`id`))

ネストしたトランザクション

GORM では、特別な設定なしにネストしたトランザクションを使うことができます。

次のコードを実行すると、1と3だけが反映されます。

db.Transaction(func(tx *gorm.DB) error {
    tx.Transaction(func(tx *gorm.DB) error {
        p := Parent{}
        tx.Create(&p)   // 1
        return nil
    })
    tx.Transaction(func(tx *gorm.DB) error {
        p := Parent{}
        tx.Create(&p)   // 2
        return errors.New("")
    })
    tx.Transaction(func(tx *gorm.DB) error {
        p := Parent{}
        tx.Create(&p)   // 3
        return nil
    })
    return nil  // 4
})

4の行をreturn errors.New("")などに書き換えると、すべての変更は破棄されます。

NULL

GORM はデフォルトで NULL 許容です。しかし、次のように nil を許容しない構造体のフィールドを定義すると、そのフィールドの値は 0 になります。

type Child struct {
    gorm.Model
    ParentID uint
}

NULL のニュアンスを正確にフィールドに反映させるためには、上記コードのuint*uintに変更します。これにより、値が NULL のフィールドには nil が入ります。

type Child struct {
    gorm.Model
    ParentID *uint
}