環境
# 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/mysql@v1.0.4
mymodule gorm.io/gorm@v1.21.3
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 Child
をChild []Child
と書き換えます。
type Parent struct {
gorm.Model
Child []Child
}
type Child struct {
gorm.Model
ParentID uint
}
外部キーの作り方を3つ紹介しました。すべて「Child
がParentID
を持つ」という点で共通しており、異なるのは「参照の方向」と「単数形か複数形か」の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
}