これはHubble Advent Calendar 2023の3日目1の記事です。
はじめに
契約書の作成から管理までを行える契約書管理クラウドのHubbleでは、一部のサービスをGo言語で実装しています。
ORMにgormを使っているサービスがあり、その利用シーンにおいて問題が発生し、解決に時間を要したため、この記事にて紹介させていただきます。
gormのDB.Set関数の振る舞いについて
サンプル
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
のシンタックスエラーが発生してしまいました。
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
が発生してしまいました。
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文について
上記のサンプルにおいて、下記のクエリが実行されることを想定していました。
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
で指定したサブクエリにメインクエリがいりじ混じったクエリとなってしまいました。
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
関数を指定するように修正することで、期待通りの動作となりました。
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
関数が必要であれば、この方法が良いかもしれません。
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という機能があります。
上記のサンプルでは、form
モデルに対し、SELECT
が施されると、func (f *Form) AfterFind(tx *gorm.DB) error
が呼び出され、下記のように出力されます。
Form:form_id:704764
ただし、このケースでは、サブクエリも施されているため、func (f *FormAuthorizedTeam) AfterFind(tx *gorm.DB) error
が呼ばれる想定しておりました。サブクエリでhooks
が機能するかどうか、引き続き調査中です。
明日は@taamoo2さんの投稿を予定しております。
-
平日のみの投稿なので、投稿日は5日ですが3日目の記事としています。 ↩