【GORM】CRUD操作する方法まとめ

この記事は DeNA 23 新卒 Advent Calendar 2022 の18日目の記事です!
さまざまなジャンルの技術に関する記事が投稿されていくので, ぜひご覧ください!🎄🌟
https://qiita.com/advent-calendar/2022/dena-23-shinsotsu

## 背景
インターン業務のなかで, Go言語のORMである[GORM](https://gorm.io/ja_JP/docs/index.html)を使う機会がありました. その際に, GORMが高機能であるがゆえに, 個人的に次の2つの課題を感じていました.

- 「レコードを取得したい」, 「レコードを更新したい」などの目的に対し, 方法がいくつかあり, どれを使えばいいのかわからない
- DBに関する知識が乏しいため, ドキュメントにある説明だけでは使用方法がわからないことがある

本記事ではこれらの課題を解消すべく, GORMを使った基本的なCRUD操作の方法についてまとめました.
なお, 基本的に[公式ドキュメント](https://gorm.io/ja_JP/docs/index.html)に沿った内容となっており, 細かい部分は公式ドキュメントの該当箇所を記載していますので, そちらをご参照ください.
個人的に実務を通して学んだ部分や, ドキュメントの説明を補充したような部分だけしっかり目に書いております.

## CRUD操作するテーブルを作成しておく
今回は以下の構造で, `users`, `companies`, `creditCards`, `posts`, `tags`テーブルを作成します.

:::note warn
なお, これらのテーブルは本記事でCRUD操作をおこなうためのサンプルであり, 実際の業務で使用しているものではございません.
:::

```go
type User struct {
	ID         uuid.UUID `gorm:"type:char(36);primary_key"`
	Name       string
	Age        uint
	is_active  bool
	CompanyID  uuid.UUID
	Company    Company
	CreditCard CreditCard
	Posts      []Post
	CreatedAt  time.Time
	UpdatedAt  time.Time
	DeletedAt  gorm.DeletedAt `gorm:"index"`
}
```

```go
type Company struct {
	ID        uuid.UUID `gorm:"type:char(36);primary_key"`
	Name      string
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

```

```go
type CreditCard struct {
	ID        uuid.UUID `gorm:"type:char(36);primary_key"`
	Number    string
	UserID    uuid.UUID `gorm:"type:char(36)"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
}
```

```go
type Post struct {
	ID        uuid.UUID `gorm:"type:char(36);primary_key"`
	Title     string
	Content   string
	UserID    uuid.UUID `gorm:"type:char(36)"`
	Tags      []Tag     `gorm:"many2many:post_tags;"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
}
```

```go
type Tag struct {
	ID        uuid.UUID `gorm:"type:char(36);primary_key"`
	Name      string
	Posts     []Post `gorm:"many2many:post_tags;"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

```

GORMには構造体を定義する際に便利な`gorm.Model`という構造体があります。`gorm.Model`を埋め込むことで, `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`フィールドを含めることができます.
今回は主キーをUUIDにしたかったので`gorm.Model`は使っていませんが, 例えば`User`の構造体を`gorm.Model`を使って以下のように定義することができます.

```go
type User struct {
    gorm.Model
	Name       string
	Age        uint
	is_active  bool
	CompanyID  uuid.UUID
	Company    Company
	CreditCard CreditCard
	Posts      []Post
}
```
上記の構造体は下記のものと同義です.

```go
type User struct {
	ID         uint `gorm:"primaryKey"`
	CreatedAt  time.Time
	UpdatedAt  time.Time
	DeletedAt  gorm.DeletedAt `gorm:"index"`
	Name       string
	Age        uint
	is_active  bool
	CompanyID  uuid.UUID
	Company    Company
	CreditCard CreditCard
	Posts      []Post
}
```
また, 各テーブルは以下のような関係になっています.


- Belong To
  - `User` belongs to `Company`
- Has One
  - `User` has a `CreditCard`
- Has Many
  - `User` has many `Posts`
- Many To Many
  - `Post` has many `Tags`
  - `Tag` has many `Posts`


## DBと接続
ここは公式[ドキュメント](https://gorm.io/ja_JP/docs/connecting_to_the_database.html)通りに接続します.

```go
func InitDB() *gorm.DB {
	dsn := "root:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=true&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	db.AutoMigrate(
		&User{},
		&Company{},
		&CreditCard{},
		&Post{},
        &Tag{},
	)

	return db
}

```
`AutoMigrate`を使うと, その名の通りスキーマ定義のマイグレーションを自動でおこなってくれます. 
ただし, カラムの削除はおこなわないので, カラムを削除する場合は[Migratorインタフェース](https://gorm.io/ja_JP/docs/migration.html#Migrator-Interface)に用意されている`DropColumn`で削除することができます.

また, `Many To Many`の関係が存在する場合, 自動的に中間テーブル（今回の例では`Posts`と`Tags`を結合する`post_tags`テーブル）を作成してくれます.

## CRUD操作をしてみる
### お試ししたいときに便利な機能
実際にDBの操作をおこなう前に, どんな実行がされるかをお試しする際に便利な機能をご紹介します. (私もこの記事を書く際にお世話になりました)
- DryRun
  - [公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/gorm_config.html#DryRun)
  - 実行はせずにSQLの生成のみおこないます. 
  ```go
  db.Session(&gorm.Session{DryRun: isDryRun})
  ```
- Debug
  - [公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/logger.html#%E3%83%87%E3%83%90%E3%83%83%E3%82%B0) 
  - 操作をデバッグし, 操作のログレベルをlogger.Infoに出力します.
  ```go
  db.Debug()
  ```

### Select系
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/query.html)
レコードの取得は`First`, `Take`, `Last`, `Find`メソッドを使っておこないます.
また, リレーションを跨ったレコードの取得には`Preload`メソッドを使います.

#### 単一レコードの取得
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/query.html)


単一レコードの取得は`First`, `Take`, `Last`, `Find`メソッドのどれを使っても可能ですが, `Find`メソッドとそれ以外のメソッドで若干振る舞いが異なります.

##### `First`, `Take`, `Last`メソッドを使う場合
使い方の詳細は公式ドキュメントにある通りなので重要な部分しか説明しませんが, この3つのメソッドは`LIMIT 1`の条件をクエリに追加します. また, レコードが見つからなった場合に`ErrRecordNotFound`を返します.
`ErrRecordNotFound`をハンドリングする場合は, [errors package](https://pkg.go.dev/errors)の`errors.Is`メソッドを使って`ErrRecordNotFound`であるかをチェックできます.

:::note info
レコードがないときに`ErrRecordNotFound`が発生することを避けたい場合は, 後述する`Find`メソッドと`LIMIT 1`を組み合わせることで, 単一レコードを取得しつつ, エラーを避けることができます.
:::

以下は`First`メソッドを用いて単一のユーザーを取得するシンプルな例です.

```go
user := User{}
// 最初のユーザーを取得
err := db.First(&user).Error

if err != nil {
    // ErrRecordNotFoundであるかをチェック
    if errors.Is(err, gorm.ErrRecordNotFound) {
      fmt.Print("Select first user error: user not found")
      return
    }

    fmt.Printf("Select first user error: %s", err.Error())
    return
}

fmt.Printf("user name: %s", user.Name)
```
実行すると, 以下のようなSQLが生成されます.
主キーが定義されていれば, それを使ってソートされ, 定義されていない場合は一番最初に定義されたフィールドを使ってソートします.

また, 取得されたレコードは`First`メソッドに渡したモデルに代入されます. この例では「admin」という名前のユーザーが取得できていることがわかります.
```sql
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1

-- user name: admin
```

上記の例では主キー等の条件を指定していませんが, 次の3つの例のように主キーを指定することができます.

###### 例1
```go
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
user := User{ID: userID}
db.First(&user)
```
実行されるSQL
```sql
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64' ORDER BY `users`.`id` LIMIT 1
```

###### 例2
```go
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
user := User{}
db.First(&user, "id = ?", userID)
```

実行されるSQL
```sql
SELECT * FROM `users` WHERE id = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64' AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1
```

###### 例3(主キーが数値の場合)
主キーが数値の場合のみ, 下記のように主キーを指定できます.
（と, ドキュメントにはありますが, UUID等の文字列で試してもうまくいきそうでした...）

```go
user := User{}
db.First(&user, 10)
```

##### `Find`メソッド
`Find`メソッドを用いた単一レコードの取得では, 主キーを指定するか, `limit=1`の条件を付与します.

主キーを指定する方法は`First`, `Take`, `Last`メソッドで説明した方法と全く同じなので割愛します.
`limit=1`を指定する場合は以下のように`Limit`メソッドを用います.
```go
user := User{}
db.Limit(1).Find(&user)
```

:::note warn
文字列を扱う場合の注意
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/security.html)
文字列を扱う場合は, SQLインジェクションを避けるために注意が必要です.
GORMは`database/sql`のプレースホルダ引数を使用してSQL文を構築し, 引数を自動的にエスケープしてくれています.
なので, ユーザーからの入力は<b>引数としてのみ</b>使用する必要があります.
以下は公式ドキュメントにある例です.


```go
userInput := "jinzhu;drop table users;"
// エスケープされる
db.First(&user, "name = ?", userInput)

// SQLインジェクションが起こりうる
db.First(&user, fmt.Sprintf("name = %v", userInput))
```
:::

##### 単一レコードの取得まとめ
個人的には単一レコードを取得する場合は`Fisrt`メソッドを使い, `ErrRecordNotFound`を避けたい場合だけ`Find`を使うと, ひと目で単一レコードを取得しようとしていることがわかるので, いいかと思いました.

#### 複数レコードの取得
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/query.html#%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B)
複数レコードの取得には`Find`メソッドを用います. (単一レコードの例では必ず実行結果が単一になるような条件を`Find`メソッドに付与していました)

以下は`users`テーブルを全件取得する例です.

```go
users := []User{}
db.Find(&users)
```

#### 条件を指定する
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/query.html#%E5%8F%96%E5%BE%97%E6%9D%A1%E4%BB%B6)
`Where`メソッドを使うか, これまで紹介したメソッドにインラインで条件を付与することで, 取得条件を指定することができます.
いろいろな条件の指定方法があるので, 詳しくは[公式ドキュメント](https://gorm.io/ja_JP/docs/query.html#%E6%96%87%E5%AD%97%E5%88%97%E6%9D%A1%E4%BB%B6)を参照してください.

##### 基本的な条件の指定方法
以下は`name = user1`かつ`is_active = true`に一致する`User`のレコードを取得する例です.

###### `Where`メソッドを使う場合
- 構造体で条件指定
```go
users := []User{}
db.Where(&User{Name: "user1", IsActive: true}).Find(&users)
```

- マップで条件指定
```go
users := []User{}
db.Where(map[string]interface{}{"name": "user1", "is_active": true}).Find(&users)
```

###### `Find`メソッドを使う場合
- 構造体で条件指定
```go
users := []User{}
db.Find(&users, User{Name: "user1", IsActive: true})
```

- マップで条件指定
```go
users := []User{}
db.Find(&users, map[string]interface{}{"name": "user1", "is_active": true})
```

##### `Scopes`を使って共通のロジックを切り出す
[公式ドキュメント](https://gorm.io/ja_JP/docs/scopes.html)
`Scopes`を利用することで, 共通のロジックを切り出すことができます. 共有ロジックは `func(*gorm.DB) *gorm.DB`という型を満たすように定義します.

 例えば非アクティブなユーザーを取得するロジックがいたるところで使用されている場合, 以下のようにロジックを切り出すことができます.

```go
func InActiveUser(db *gorm.DB) *gorm.DB {
	return db.Where("is_active = ?", false)
}
```
このように定義した`Scope`は次のように使用できます.

```go
users := []User{}
db.Scopes(InActiveUser).Find(&users)
```

上記を実行すると, 以下のSQLが生成され, `InActiveUser`が利用できていることがわかります.

```sql
SELECT * FROM `users` WHERE is_active = false AND `users`.`deleted_at` IS NULL
```

##### 条件を指定する際の注意
:::note warn
注意1
`Find`や`First`メソッドに渡すオブジェクトに主キーがセットされている時に, `Where`メソッドにも主キーに関する条件を持たせてしまうと, AND条件でクエリが発行されてしまいます.
公式ドキュメントの例では, 下記が実行されたとき, 
```go
var user = User{ID: 10}
db.Where("id = ?", 20}.First(&user)
```
下記のようなSQLが生成されてしまいます.
```sql
SELECT * FROM users WHERE id = 10 and id = 20 ORDER BY id ASC LIMIT 1
```

`Where`メソッドは主キー以外の条件を指定する際に使い, 主キーに関する条件を指定する場合は`Find`メソッドに付与したり, 構造体に持たせるようにするとよさそうです.
:::

:::note warn
注意2
構造体使って条件を指定する場合, ゼロ値が指定されたフィールドは条件に含まれせん. (goにおけるゼロ値については[こちら](https://go.dev/tour/basics/12))
具体的には, `is_active = false`の`User`を取得したい場合, 以下のように構造体を用いて条件を指定しても, 生成されるSQLに`is_active = false`が反映されません.

```go
users := []User{}
db.Where(&User{IsActive: false}).Find(&users)
```
上記を実行すると, 以下のSQLが生成され, `is_active = false`が反映が反映されていないことがわかります...
```sql
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL
```

これを回避するためには, ゼロ値を指定する際には構造体を使わないようにします.
以下はマップを用いた例です.

```go
users := []User{}
db.Where(map[string]interface{}{"is_active": false}).Find(&users)
```

上記を実行すると以下のSQLが実行され, `is_active = false`が無事反映されていることがわかります.

```sql
SELECT * FROM `users` WHERE `is_active` = false AND `users`.`deleted_at` IS NULL
```

以上の例では`Where`メソッドを例に説明していますが, `Find`メソッドを使った条件指定でも同様のことが起きます.

:::

##### 条件を指定する方法まとめ
条件を指定する際には上述した2点に注意が必要です.
また, 複雑な条件が発生するアプリケーションでは, 主キー以外の条件は`Where`メソッドで指定し, 必要に応じて`Scopes`を使ってロジックの再利用をすると可読性が上がるのではないかと思いました.

#### 関連レコードの取得
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/preload.html#Preload)
「`User`が複数の`Post`を持っている」というようなリレーションがあるレコードを取得する場合, `Preload`メソッドを使うことで`eager loading`ができます.

以下は特定の`User`と, そのユーザーが持つ複数の`Post`を取得する例です.

```go
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
user := User{ID: userID}
db.Preload("Posts").First(&user)
```

上記を実行すると, 以下のSQLが実行されます.
先に`Post`が取得され, その後に`User`が取得されていることがわかります.

```sql
SELECT * FROM `posts` WHERE `posts`.`user_id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64' AND `posts`.`deleted_at` IS NULL

SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64' ORDER BY `users`.`id` LIMIT 1
```

### Create系
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/create.html)
#### 単一レコードの作成
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/create.html#%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)
レコードの作成には基本的に`Create`メソッドを用います.
名前が`company1`の`Company`を作成する場合は, 以下のように作成することができます.

```go
company := Company{Name: "company1"}
err := db.Create(&company).Error
if err != nil {
	fmt.Printf("Create company error: %s", err.Error())
	return
}
fmt.Print("company id: %s", company.ID)
```

ただ, 今回のテーブル定義では主キーが`uuid.UUID`型でデフォルト値を持たないため, このままでは以下のエラーが出力されてしまいます.

```
Create Company Error: Error 1364: Field 'id' doesn't have a default value
```

そこで`Hook`を使用して, 作成時にUUIDを割り当てるようにします.
`Hook`は作成／取得／更新／削除の前後に呼び出される関数です.
オブジェクトの作成前には`BeforeCreate`が実行されます. 他の`Hook`については[公式ドキュメント](https://gorm.io/ja_JP/docs/create.html#%E4%BD%9C%E6%88%90%E6%99%82%E3%81%AEHook)を参照してください.
`Hook`は`func(*gorm.DB) error`という型を満たす必要があります.
以下のような`BeforeCreate`を作成することで, データベースに保存する前にUUIDを割り当てることができます.

```go
func (company *Company) BeforeCreate(db *gorm.DB) error {
	if company.ID == uuid.Nil {
		company.ID = uuid.New()
	}

	return nil
}
```
`BeforeCreate`メソッドを作成したあとに先ほどのサンプルコードを実行すると, 以下のようなSQLが生成されます.

また, レコードの取得のときと同様に, 取得されたレコードは`Create`メソッドに渡したモデルに代入されます. レコードを追加した際に生成されたIDも代入されます.

```sql
INSERT INTO `companies` (`name`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES ('company1','2022-12-06 17:27:28.123','2022-12-06 17:27:28.123',NULL,'2893bac3-e2f0-4f66-b61e-45553aec6398')

-- company id: fbbe1f9f-2957-468e-bd60-ee914634bf46
```

同様の方法で関連データを関連づけて作成することができます. 値が`false`や`0`等のゼロ値でない場合は関連データもupsertされ, 関連データのHookメソッドも実行されます.
```go
user := User{
	Name:       "user1",
	Company:    Company{Name: "company2"}, // 関連するCompanyを作成
	CreditCard: CreditCard{Number: "1111"}, // 関連するCreditCardを作成
}
err := db.Create(&user).Error

if err != nil {
	fmt.Printf("Create user with related data error: %s", err.Error())
	return
}
```

関連付けの作成及び更新を避けたい場合は以下のように関連を`Omit`で指定することでスキップできます.
```go
db.Omit("CreditCard").Create(&user)

// skip all associations
db.Omit(clause.Associations).Create(&user)
```

#### レコード一括作成
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/create.html#%E4%B8%80%E6%8B%AC%E4%BD%9C%E6%88%90)
レコードを一括作成する場合, `create`メソッドまたは `CreateInBatches`メソッドを使います.

##### createメソッドを使う場合
以下は3件の`Post`を追加する例です
```go
// 既にDBにあるPostのID
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
posts := []Post{
	{
		Title:   "post1",
		Content: "post1 content",
		UserID:  userID,
	},
	{
		Title:   "post2",
		Content: "post2 content",
		UserID:  userID,
	},
	{
		Title:   "post3",
		Content: "post3 content",
		UserID:  userID,
	},
}

db.Create(&posts)
```
上記を実行すると, 以下のようなSQLが実行されます.
```sql
INSERT INTO `posts` (`title`,`content`,`view`,`user_id`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES ('post1','post1 content',0,'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64','2022-12-10 21:46:21.805','2022-12-10 21:46:21.805',NULL,'374f16cf-0996-45e0-9c71-e903a6ef72c8'),('post2','post2 content',0,'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64','2022-12-10 21:46:21.805','2022-12-10 21:46:21.805',NULL,'e6759971-37a6-4b6f-8331-61f3cf7da957'),('post3','post3 content',0,'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64','2022-12-10 21:46:21.805','2022-12-10 21:46:21.805',NULL,'c36dc846-43c2-4e30-a4b6-0a904c2665a8')
```

##### `CreateInBatches`メソッドを使う場合
`CreateInBatches`メソッドを使うと, バッチサイズを指定してレコードを作成することができます.

```go
// 既にDBにあるUserのID
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
posts := []Post{}
// Postを100件生成
for i := 0; i < 100; i++ {
	posts = append(posts, Post{
		Title:   fmt.Sprintf("post %d", i),
		Content: fmt.Sprintf("post %d content", i),
		UserID:  userID,
	})
}
// バッチサイズを50件に指定
db.CreateInBatches(&posts, 50)
```
上記を実行すると以下のように, 50件ずつデータがインサートされます. (一部を省略して表示しています)

```sql
INSERT INTO `posts` (`title`,`content`,`view`,`user_id`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES ('post 0','post 0 content',0,'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64','2022-12-10 21:54:11.439','2022-12-10 21:54:11.439',NULL,'36621d66-e85c-400a-86ea-3e7bf9ddc03e'),...,('post 49','post 49 content',0,'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64','2022-12-10 21:54:11.439','2022-12-10 21:54:11.439',NULL,'451e331a-664e-4777-9275-ede688d8585c')

INSERT INTO `posts` (`title`,`content`,`view`,`user_id`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES ('post 50','post 50 content',0,'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64','2022-12-10 21:54:11.459','2022-12-10 21:54:11.459',NULL,'9f56c919-905e-4e87-b690-404371b698d0'),...,('post 99','post 99 content',0,'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64','2022-12-10 21:54:11.459','2022-12-10 21:54:11.459',NULL,'34e805c9-ab6c-46ad-87c1-46ab8f0fdfeb')
```

##### 関連があるデータを追加する
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/associations.html#%E9%96%A2%E9%80%A3%E3%81%AE%E8%BF%BD%E5%8A%A0)
関連があるデータを追加する場合, `Association Mode`が便利です.
`Association Mode`は, ソースモデルに対して`Association`メソッドで関連を指定することで開始されます. `Association Mode`では関連に対してさまざまな操作をすることができます.

例えば`Tag`を`Post`に追加する場合, 
- ソースモデルとして`Post`を指定
- `Association`メソッドで`Post`に関連している`Tags`を指定
- `Append`メソッドで関連データを追加する

ということができます.

```go
// 既にDBにあるPostのID
postID := uuid.MustParse("0155dffb-c98b-4e58-a169-04cb3f8634d2")
post := Post{ID: postID}
tags := []Tag{
	{Name: "tag1"},
	{Name: "tag2"},
}
// Tagを追加
db.Model(&post).Association("Tags").Append(&tags)
```
上記を実行すると, 以下のSQLが実行され, 中間テーブルの更新までおこなってくれていることがわかります.

```sql
INSERT INTO `tags` (`name`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES ('tag1','2022-12-10 22:07:40.596','2022-12-10 22:07:40.596',NULL,'9f63212f-1f35-4f66-a31a-7aba352ea949'),('tag2','2022-12-10 22:07:40.596','2022-12-10 22:07:40.596',NULL,'8e377a09-1d89-4e61-8515-b29769a7d7e6') ON DUPLICATE KEY UPDATE `id`=`id`

INSERT INTO `post_tags` (`post_id`,`tag_id`) VALUES ('0155dffb-c98b-4e58-a169-04cb3f8634d2','9f63212f-1f35-4f66-a31a-7aba352ea949'),('0155dffb-c98b-4e58-a169-04cb3f8634d2','8e377a09-1d89-4e61-8515-b29769a7d7e6') ON DUPLICATE KEY UPDATE `post_id`=`post_id`

UPDATE `posts` SET `updated_at`='2022-12-10 22:07:40.594' WHERE `posts`.`deleted_at` IS NULL AND `id` = '0155dffb-c98b-4e58-a169-04cb3f8634d2'
{9f63212f-1f35-4f66-a31a-7aba352ea949 tag1 [] 2022-12-10 22:07:40.596 +0900 JST 2022-12-10 22:07:40.596 +0900 JST {0001-01-01 00:00:00 +0000 UTC false}}
```

#### Upsert
Upsertについては[公式ドキュメント](https://gorm.io/ja_JP/docs/create.html#%E3%82%B3%E3%83%B3%E3%83%95%E3%83%AA%E3%82%AF%E3%83%88%E7%99%BA%E7%94%9F%E6%99%82%E3%81%AEUpsert)の通りの説明となってしまいますが, 以下のようにすることで, 主キー以外のカラムはUpsertすることができます.

例えば`User`モデルで`name`と`age`のUpsertを許可したい場合は, 以下のようにします.

```go
// 既にDBにあるUserのID
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
user := User{
	ID:   userID,
	Name: "new name",
	Age:  30,
}
db.Clauses(clause.OnConflict{
	Columns:   []clause.Column{{Name: "id"}},
	// upsertを許可するカラムを指定する
	DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&user)
```

上記の例では`Create`メソッドを使っていますが, `CreateInBatches`メソッドでも同様にUpsertすることができます.

### Update系
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/update.html)
Updateは`Update`および`Updates`メソッドを使うか, レコード取得関連のメソッドと`Save`メソッドを合わせて使うことで実現できます.

#### 単一カラムの更新
[公式ドキュメントの該当箇所](https://gorm.io/ja_JP/docs/update.html#%E5%8D%98%E4%B8%80%E3%81%AE%E3%82%AB%E3%83%A9%E3%83%A0%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B)
`Update`メソッドに更新したいカラムの名前と, 更新後の値を渡します.
条件を指定せずに`Update`メソッドを使用すると`ErrMissingWhereClause`というエラーが起きてしまうので, 基本的には`Where`メソッドを使ったり, 構造体に主キーをセットするなどして何かしらの条件を付与する必要があります.

あまりなさそうですが, もし`ErrMissingWhereClause`を無視して指定したカラムのデータを更新したい場合は
- `db.Session(&gorm.Session{AllowGlobalUpdate: true})`を指定して`AllowGlobalUpdate`モードを有効にする
- `1=1`などの無意味な条件を`Where`メソッドで指定する
- `Exec`メソッドで生のSQLを実行する
などをすることで実現できます.

以下は, 構造体に`ID`をセットしておくことで, IDを指定して`name`を更新している例です.

```go
// 既にDBにあるUserのID
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
user := User{ID: userID}
db.Model(&user).Update("name", "new name")
```
上記を実行すると, 次のSQLが実行されます.

```sql
UPDATE `users` SET `name`='new name',`updated_at`='2022-12-10 23:37:12.008' WHERE `users`.`deleted_at` IS NULL AND `id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64'
```

:::note warn
注意したいのが, `Update`メソッドは条件に一致するレコードがない場合でもエラーになりません. 例えば上記の例で, DBに存在しないユーザーのIDを指定したとしても, エラーになりません.
アップデートする対象が存在していないことがそもそも問題になる場合は, `Update`メソッドを実行する前に`Find`や`First`メソッドでレコードの存在を確認する必要があります。
:::

#### レコード取得関連のメソッドと`Save`メソッドを組み合わせる
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/update.html#%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%83%95%E3%82%A3%E3%83%BC%E3%83%AB%E3%83%89%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B)
前述したように, レコードの存在確認には`Find`や`First`メソッドを使う必要があります. その際に`Save`メソッドを合わせて使うことで, データの更新ができます.
以下は年齢をインクリメントする例です.

```go
// 既にDBにあるUserのID
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
user := User{ID: userID}
err := db.First(&user).Error
if err != nil {
	// ErrRecordNotFoundであるかをチェック
	if errors.Is(err, gorm.ErrRecordNotFound) {
		fmt.Print("Select first user error: user not found")
		return
	}
	fmt.Printf("Select first user error: %s", err.Error())
	return
}

fmt.Printf("user age before update: %d", user.Age)
user.Age += 1
db.Save(user)
```

上記を実行すると, 次のようなSQLが実行されます.
`Save`メソッドは更新するカラムを指定しないので, 全てのカラムがアップデートされます.
```sql
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64' ORDER BY `users`.`id` LIMIT 1

-- user age before update: 20

UPDATE `users` SET `id`='b89d13f3-bd28-46d1-b1ad-4f48d7b11d64',`name`='new name',`age`=21,`is_active`=false,`company_id`='84aa8c00-b179-4842-8d94-c360a1354cfe',`created_at`='2022-12-03 02:22:46.488',`updated_at`='2022-12-10 23:54:49.341',`deleted_at`=NULL WHERE `users`.`deleted_at` IS NULL AND `id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64'
```

これだけでも一見問題なさそうですが, `First`メソッド実行後に, 他のユーザーによって取得したレコードの`age`が更新されてしまったり, レコード自体が削除されてしまうと, データの整合性が取れなくなってしまいます.
この問題を回避するためには, `Select`する際に`Update`が完了するまでレコードをロックするように指定します.
GORMは複数種類のロックをサポートしており, 詳細は[公式ドキュメント](https://gorm.io/ja_JP/docs/advanced_query.html#%E3%83%AD%E3%83%83%E3%82%AF-FOR-UPDATE)に譲りますが, 今回の例では以下のようにして`Select`する際に`FOR UPDATE`のオプションを追加します.
```go
db.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user)
```

また`FOR UPDATE`では, トランザクションがコミットまたはロールバックされるとロックが解除されるため, `Select`~`Update`をひとつのトランザクションで実行する必要があります. (ロック読み取りについての詳細は[MySQLのドキュメント](https://dev.mysql.com/doc/refman/5.6/ja/innodb-locking-reads.html)に譲ります)
トランザクション内で一連の操作を実行する場合は`Transaction`メソッドを使います.
トランザクションは`nil`を返すことでコミットされます.

トランザクションについての詳細は, [公式ドキュメント](https://gorm.io/ja_JP/docs/transactions.html)を参照してください.

`FOR UPDATE`と`Transaction`メソッドを取り入れると, 先ほどの年齢をインクリメントする例は, 以下のように書き換えることができます.

```go
// 既にDBにあるUserのID
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
user := User{ID: userID}
err := db.Transaction(func(tx *gorm.DB) error {
	err := db.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user).Error
	if err != nil {
		return err
	}

	fmt.Printf("user age before update: %d", user.Age)
	user.Age += 1
	db.Save(user)

	// nilが返却されるとトランザクション内の全処理がコミットされる
	return nil
})

if err != nil {
	if errors.Is(err, gorm.ErrRecordNotFound) {
		fmt.Print("Update user error: user not found")
		return
	}
	fmt.Printf("Update user error: %s", err.Error())
	return
}
```
上記が実行されると, 以下のSQLが実行されます.

```sql
SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64' ORDER BY `users`.`id` LIMIT 1 FOR UPDATE

-- user age before update: 20

UPDATE `users` SET `id`='b89d13f3-bd28-46d1-b1ad-4f48d7b11d64',`name`='new name',`age`=21,`is_active`=false,`company_id`='84aa8c00-b179-4842-8d94-c360a1354cfe',`created_at`='2022-12-03 02:22:46.488',`updated_at`='2022-12-11 00:21:04.217',`deleted_at`=NULL WHERE `users`.`deleted_at` IS NULL AND `id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64'
```

#### 複数カラムのアップデート
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/update.html#%E8%A4%87%E6%95%B0%E3%81%AE%E3%82%AB%E3%83%A9%E3%83%A0%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B)
複数のカラムを更新する場合, `Updates`メソッドに構造体もしくはマップを渡します.

以下は構造体を用いて複数カラムを更新する例です.
```go
// 既にDBにあるUserのID
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
user := User{
	ID:       userID,
	Name:     "updated name",
	IsActive: true,
}
db.Model(&user).Updates(user)
```
上記を実行すると, 以下のSQLが生成されます.

```sql
UPDATE `users` SET `id`='b89d13f3-bd28-46d1-b1ad-4f48d7b11d64',`name`='updated name',`is_active`=true,`updated_at`='2022-12-11 00:33:39.769' WHERE `users`.`deleted_at` IS NULL AND `id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64'
```

:::note warn
`Update`および`Updates`の注意点

`Where`メソッドと同様に, 構造体にゼロ値をセットしても, そのフィールドは更新されません. これを回避するためには, `Where`メソッドと同様にマップ使うか, 次のように`Select`メソッドを使って更新するフィールドを指定します.

```go
// 既にDBにあるUserのID
userID := uuid.MustParse("b89d13f3-bd28-46d1-b1ad-4f48d7b11d64")
user := User{
	ID:       userID,
	IsActive: false,
}
db.Model(&user).Select("is_active").Updates(user)
```
上記を実行すると以下のSQLが実行され, ゼロ値でアップデートができていることがわかります.

```sql
UPDATE `users` SET `is_active`=false,`updated_at`='2022-12-11 00:48:21.101' WHERE `users`.`deleted_at` IS NULL AND `id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64'
```
:::

#### `Update`系その他
`Update`にも`Create`と同様に`Hook`があったり, SQL式やサブクエリを使うことで柔軟にデータをアップデートできます. 詳しくは[公式ドキュメント](https://gorm.io/ja_JP/docs/update.htm)をご覧ください.

### Delete系
[公式ドキュメント](https://gorm.io/ja_JP/docs/delete.html)
レコードの削除には`Delete`メソッドを使用します.

#### 単一レコードの削除
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/delete.html#%E4%B8%BB%E3%82%AD%E3%83%BC%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E5%89%8A%E9%99%A4)
単一レコードを削除する場合, 単一レコードを取得する場合のように主キーをインライン条件で指定します.

以下は`ID`を指定して`Post`を削除する例です.
```go
postID := uuid.MustParse("0204e10c-20f2-40cf-886d-cd38f1789c0f")
post := Post{ID: postID}
db.Delete(&post)
```
実行されるSQLは後述する論理削除か物理削除かで異なります.

:::note warn
`Update`と同様に, 対象レコードが存在しない場合でもエラーにならないので, 対象レコードが存在しないことが問題になるような場合には`Find`や`First`メソッドでレコードの存在を確認する必要があります.
:::

#### 一括削除
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/delete.html#%E4%B8%80%E6%8B%AC%E5%89%8A%E9%99%A4)
主キーが指定されない場合は条件に一致したレコードを一括削除します.

以下の例では　非アクティブなユーザーを一括削除しています.
```go
users := []User{}
db.Where("is_active = ?", false).Delete(&users)
```
こちらも実行されるSQLは後述する論理削除か物理削除かで異なります.

#### 論理削除
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/delete.html#%E8%AB%96%E7%90%86%E5%89%8A%E9%99%A4)
`gorm.DeletedAt`というフィールをモデルに含めると, そのモデルは論理削除されるようになります.
`gorm.DeletedAt`は`gorm.Model`に含まれています.
今回のように`gorm.Model`を使わない場合は, 以下の例のように`gorm.DeletedAt`型の`Deleted`をモデルに含めることで論理削除されるようになります.
```go
type User struct {
  ID      int
  Deleted gorm.DeletedAt
  Name    string
}
```

論理削除機能をオンにすると, レコードを取得する際のSQLに`deleted_at IS NULL`という条件が自動で付与され, `Deleted`が`NULL`でないレコードはデフォルトで取得されなくなります.
論理削除されたレコードを含めてレコードを取得する場合は, `Unscoped`メソッドを使います.

```go
users := []User{}
db.Unscoped().Find(&users)
```
上記を実行すると, 以下のSQLが実行されます
```sql
SELECT * FROM users;
```

物理削除する場合も, 以下のように`Unscoped`メソッドを使います.
```go
// Postを持っていないUserのID
userID := uuid.MustParse("117963d3-16f0-494f-8da2-7b6fa7fbcbd3")
user := User{ID: userID}
db.Unscoped().Delete(&user)
```
上記を実行すると以下のSQLが実行され, レコードが物理削除されたことがわかります.

```sql
DELETE FROM `users` WHERE `users`.`id` = '117963d3-16f0-494f-8da2-7b6fa7fbcbd3'
```
#### 関連の削除
[公式ドキュメント該当箇所](https://gorm.io/ja_JP/docs/associations.html#%E9%96%A2%E9%80%A3%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%A6%E5%89%8A%E9%99%A4%E3%81%99%E3%82%8B)
`Post`と`CreditCard`を持つ`User`のような関連を持つレコードを削除する場合は, `Select`メソッドで関連を指定することで, 関連も同時に削除することができます.

```go
// 削除するユーザーのID
userID := uuid.MustParse("103a58cf-44e6-4e31-8a33-802fdcce58ae")
user := User{ID: userID}
db.Select("Posts", "CreditCard").Delete(&user)
```
生成されるSQL
```sql
UPDATE `posts` SET `deleted_at`='2022-12-11 21:53:13.293' WHERE `posts`.`user_id` = '103a58cf-44e6-4e31-8a33-802fdcce58ae' AND `posts`.`deleted_at` IS NULL

UPDATE `credit_cards` SET `deleted_at`='2022-12-11 21:53:13.309' WHERE `credit_cards`.`user_id` = '103a58cf-44e6-4e31-8a33-802fdcce58ae' AND `credit_cards`.`deleted_at` IS NULL

DELETE FROM `posts` WHERE `posts`.`user_id` = 'b89d13f3-bd28-46d1-b1ad-4f48d7b11d64'

UPDATE `users` SET `deleted_at`='2022-12-11 21:53:13.311' WHERE `users`.`id` = '103a58cf-44e6-4e31-8a33-802fdcce58ae' AND `users`.`deleted_at` IS NULL
```

また, 関連の参照のみを削除する場合には, 関連データの追加のときのように`Association Mode`を使います.
例えば, ある`Post`から特定の`Tag`を外したい(参照を削除したい)場合は, 以下のようになります.
```go
// あるPostのID
postID := uuid.MustParse("003ad45c-538e-4568-b2c2-a93d8be6a791")
post := Post{ID: postID}
// 外したいTagのID
tagID := uuid.MustParse("8ddf4cf9-9ac2-428a-a5c2-8dcd15cfd44c")
tag := Tag{ID: tagID}
db.Model(&post).Association("Tags").Delete(&tag)
```
上記を実行すると以下のようなSQLが生成され, 参照が削除されたことがわかります.

```sql
DELETE FROM `post_tags` WHERE `post_tags`.`post_id` = '003ad45c-538e-4568-b2c2-a93d8be6a791' AND `post_tags`.`tag_id` = '8ddf4cf9-9ac2-428a-a5c2-8dcd15cfd44c'
```

#### `Delete`系その他
`Delete`メソッドにも`Update`, `Create`メソッドと同様に`Hook`があり, 削除前と削除後におこなう処理を指定することができます. 定義方法等は`Create`メソッドと同様であるため, 詳細は[公式ドキュメント](https://gorm.io/ja_JP/docs/delete.html#%E5%89%8A%E9%99%A4%E3%81%AE%E3%83%95%E3%83%83%E3%82%AF%E5%87%A6%E7%90%86)に譲ります.

## 最後に
かなり長くなってしまいましたが, GORMを用いたCRUD操作をまとめてみました.
最後まで読んでくださり, ありがとうございました!!
GORMを使う方のお役に立てるととてもうれしいです.

明日以降の記事も, ご期待ください🌟
https://qiita.com/advent-calendar/2022/dena-23-shinsotsu

## 参考
- https://gorm.io/ja_JP/
- https://pkg.go.dev/errors
- https://go.dev/tour/basics/12
- https://dev.mysql.com/doc/refman/5.6/ja/innodb-locking-reads.html