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/mysql@v1.0.4
mymodule gorm.io/gorm@v1.21.3
```

## GORM の動作

### 外部キーの作り方

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

```golang
type Parent struct {
	gorm.Model
}

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

![スクリーンショット 2021-03-08 21.29.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59884/d7c1fe9e-9239-fc63-4793-e4d8bc02d40b.png)

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

```golang
type Parent struct {
	gorm.Model
	Child Child
}

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

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

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

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

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

### 多対多

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

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

type Language struct {
	gorm.Model
}
```

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

![スクリーンショット 2021-03-08 23.21.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59884/4ae9cf67-f9f7-1deb-dff0-1b933f0a5345.png)

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

![スクリーンショット 2021-03-08 23.23.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59884/4e074b0a-be2b-d7d4-dc66-5c39fde7ec0e.png)

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

```golang
type User struct {
	gorm.Model
}

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

![スクリーンショット 2021-03-08 23.31.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59884/82f2f568-8fd9-db19-76c5-d9fea21b4810.png)

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

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

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

![スクリーンショット 2021-03-08 23.53.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59884/2bccfec7-693d-0c25-dea6-cc22e1fbb935.png)

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

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

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

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

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

```golang
type Parent struct {
	gorm.Model
}

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

type Grandson struct {
	gorm.Model
	ChildID uint
	Child   Child
}
```

```golang
var grandson Grandson
db.Preload(clause.Associations).Find(&grandson)
log.Print(grandson.Child.Parent)	// 初期値が出力される
```

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

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

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

### 外部キー制約の設定方法

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

```golang
type Parent struct {
	gorm.Model
}

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

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

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

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

### 外部キーと削除

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

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

```golang
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だけが反映されます。

```golang
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` になります。

```golang
type Child struct {
	gorm.Model
	ParentID uint
}
```

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

```golang
type Child struct {
	gorm.Model
	ParentID *uint
}
```