gormのDB.Set関数利用時のエラーについて

これは[Hubble Advent Calendar 2023](https://qiita.com/advent-calendar/2023/hubble)の3日目[^1]の記事です。
[^1]: 平日のみの投稿なので、投稿日は5日ですが3日目の記事としています。 

# はじめに

契約書の作成から管理までを行える契約書管理クラウドの[Hubble](https://hubble-docs.com/)では、一部のサービスをGo言語で実装しています。
ORMに[gorm](https://gorm.io/)を使っているサービスがあり、その利用シーンにおいて問題が発生し、解決に時間を要したため、この記事にて紹介させていただきます。


# gormのDB.Set関数の振る舞いについて
## サンプル

```go:sample.go
const key = "test_key"

type (
	Form struct {
		ID                  uint                 `json:"id" gorm:"primaryKey"`
		Name                string               `json:"name" gorm:"not null"`
		IsAllUserAuthorized *bool                `json:"isAllUserAuthorized" gorm:"default:false"`
		FormAuthorizedTeams []FormAuthorizedTeam `json:"formAuthorizedTeams" gorm:"foreignKey:FormID"`
	}
	FormAuthorizedTeam struct {
		ID     uint   `json:"id" gorm:"primaryKey"`
		FormID uint   `json:"formId" gorm:"not null"`
		TeamID string `json:"teamId" gorm:"not null"`
	}
)

func (f *Form) AfterFind(tx *gorm.DB) error {
	v, ok := tx.Get(key)
	if ok {
		fmt.Printf("Form:%d\n", v)
	} else {
		fmt.Println("xxxx")
	}
	return nil
}

func (f *FormAuthorizedTeam) AfterFind(tx *gorm.DB) error {
	v, ok := tx.Get(key)
	if ok {
		fmt.Printf("FormAuthorizedTeam:%s\n", v)
	} else {
		fmt.Println("xxxx")
	}
	return nil
}

func IsAuthorized(tx *gorm.DB, formID uint, teamIDs []string) error {
	tx = tx.Set(key, fmt.Sprintf("form_id:%d", formID))

	subQuery := tx.Model(&FormAuthorizedTeam{}).
		Where("team_id IN (?)", teamIDs).Select("form_id")

	return tx.Table("forms").Where("id", formID).
		Where(
			tx.Or("is_all_user_authorized", 1).
				Or("id IN (?)", subQuery),
		).First(&Form{}).Error
}
```

上記を実行したところ、下記のように`SQL`のシンタックスエラーが発生してしまいました。

```shell:エラー内容
 Error 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')) ORDER BY `forms`.`id` LIMIT 1' at line 1
```

余談ですが、このサンプルにおいて、メインクエリで`Debug()`を指定すると、`Stack Overflow`となり、`Access Violation`が発生してしまいました。

```go
	return tx.Debug().Set(key, fmt.Sprintf("form_id:%d", formID)).Table("forms").Where("id", formID).
		Where(
			tx.Or("is_all_user_authorized", 1).
				Or("id IN (?)", subQuery),
		).First(&Form{}).Error
```

## SQL文について

上記のサンプルにおいて、下記のクエリが実行されることを想定していました。

```sql:期待したSQL
SELECT * FROM `forms` WHERE `id` = 864322 AND (`is_all_user_authorized` = 1 OR id IN (SELECT `form_id` FROM `form_authorized_teams` WHERE team_id IN ('IsAuthorizedWithTx_team_id'))) ORDER BY `forms`.`id` LIMIT 1
```

しかしながら、実際のクエリは下記となり、`subQuery`で指定したサブクエリにメインクエリがいりじ混じったクエリとなってしまいました。

```sql:実際のSQL
SELECT `form_id` FROM `forms` WHERE (team_id IN ('IsAuthorizedWithTx_team_id') AND `id` = 546285 OR `is_all_user_authorized` = 1 OR id IN (SELECT `form_id` FROM `forms` WHERE (team_id IN ('IsAuthorizedWithTx_team_id') AND `id` = 546285 OR `is_all_user_authorized` = 1 OR id IN ()) ORDER BY `forms`.`id` LIMIT 1
```
## 解決方法

結論から、`IsAuthorized`関数を下記のように、メインクエリに`Set`関数を指定するように修正することで、期待通りの動作となりました。

```go:IsAuthorized
func IsAuthorized(tx *gorm.DB, formID uint, teamIDs []string) error {
	subQuery := tx.Model(&FormAuthorizedTeam{}).
		Where("team_id IN (?)", teamIDs).Select("form_id")

	return tx.Set(key, fmt.Sprintf("form_id:%d", formID)).Table("forms").Where("id", formID).
		Where(
			tx.Or("is_all_user_authorized", 1).
				Or("id IN (?)", subQuery),
		).First(&Form{}).Error
}
```

`*gorm.DB`のオブジェクトを引数で受け渡す前に、`Set`関数が指定されるケースがあり、今回のエラーが発生しておりました。
なお、下記のように`tx.Session(&gorm.Session{})`とセッションを生成することでも回避できましたので、`*gorm.DB`のオブジェクトを引数で受け渡す前に、`Set`関数が必要であれば、この方法が良いかもしれません。

```go:IsAuthorized
func IsAuthorized(tx *gorm.DB, formID uint, teamIDs []string) error {
	tx = tx.Set(key, fmt.Sprintf("form_id:%d", formID))

	subQuery := tx.Session(&gorm.Session{}).Model(&FormAuthorizedTeam{}).
		Where("team_id IN (?)", teamIDs).Select("form_id")

	return tx.Table("forms").Where("id", formID).
		Where(
			tx.Or("is_all_user_authorized", 1).
				Or("id IN (?)", subQuery),
		).First(&Form{}).Error
}
```

# 備考
## `gorm`の`Hooks`について
`gorm`には、[Hooks](https://gorm.io/ja_JP/docs/hooks.html)という機能があります。
上記のサンプルでは、`form`モデルに対し、`SELECT`が施されると、`func (f *Form) AfterFind(tx *gorm.DB) error`が呼び出され、下記のように出力されます。

```shell:出力結果
Form:form_id:704764
```
ただし、このケースでは、サブクエリも施されているため、`func (f *FormAuthorizedTeam) AfterFind(tx *gorm.DB) error`が呼ばれる想定しておりました。サブクエリで`hooks`が機能するかどうか、引き続き調査中です。


明日は@taamoo2さんの投稿を予定しております。