起きたこと
GORM(v1.9.16)を使っていて、アソシエーションの削除がうまくいかなかったときにハマったのでその時の調査メモです。
Productモデルと、そこに紐づくShopモデルを並行して更新するようなプログラムを作成していました。
package main
import (
"github.com/my-best/products.my-best.com/go/internal/models"
"golang.org/x/sync/errgroup"
)
func main() error {
var product models.Product
eg := errgroup.Group{}
eg.Go(func() error { return updateProduct(&product) })
eg.Go(func() error { return updateProductShop(&product})
// 略
}
updateProductShop
の内部では不要になったShops
を削除するような処理が書かれていました。
err := db.GetDB().Transaction(func(tx *gorm.DB) error {
db.GetDB().Model(&product).Related(&product.Shops)
for _, shop := range product.Shops {
if err := tx.Delete(&shop).Error; err != nil {
return fmt.Errorf("既存のShopの削除中にエラーが発生しました: %v", err)
}
}
}
ところが、実際にmain.go
を回し終わっても、削除されているはずのShop
が残ってしまっていました。
Save(&product)にて関連が上書きされてしまっていた
ログを確認してみると、下記のようになっていました。(一部抜粋)
// updateProductShopの処理
[2022-07-12 10:51:16] [2.35ms] SELECT * FROM `shops` WHERE (`product_id` = 17821591)
[1 rows affected or returned ]
[2022-07-12 10:51:16] [1.40ms] DELETE FROM `shops` WHERE `shops`.`id` = 155648
[1 rows affected or returned ]
// updateProductの処理
[2022-07-12 10:51:17] [1.85ms] SELECT * FROM `products` WHERE (product_id = 17821591)
[0 rows affected or returned ]
[2022-07-12 10:51:17] [1.59ms] UPDATE `products` SET (略) WHERE `products`.`id` = 17821591
[1 rows affected or returned ]
[2022-07-12 10:51:17] [1.51ms] UPDATE `shops` SET `product_id` = 17821591, (略) WHERE `yahoo_shops`.`id` = 155648
[0 rows affected or returned ]
これを確認してみると、updateProductShop
ではたしかに削除ができているのですが、updateProduct
でなぜか削除したはずのshopのUPDATEが行われていることがわかります。
こちらを実行元を見てみると、
db.GetDB().Save(&product)
が呼ばれていました。
AssociationのDeleteも合わせて行う
どうやらGORMでは単にShopを削除するだけではなくて、関連も削除してあげないといけなかったようです。
GORMはレコードの作成・更新時にUpsertを使用して自動的に関連データとその参照を保存します
err := db.GetDB().Transaction(func(tx *gorm.DB) error {
db.GetDB().Model(&product).Related(&product.Shops)
for _, shop := range product.Shops {
if err := tx.Delete(&shop).Error; err != nil {
return fmt.Errorf("既存のShopの削除中にエラーが発生しました: %v", err)
}
}
}
+if err := db.GetDB().Model(&product).Association("Shops").Delete(shop).Error; err != nil {
+ return fmt.Errorf("関連の削除に失敗しました: %v", err)
+}
このように書くことで暫定対応しました。
Omitオプション
恒久対応としては、他箇所のdb.GetDB().Save(&product)の箇所にOmitオプションを付けるのが良さそうです
作成/更新時のアソシエーションレコードの自動保存をスキップするには、 Select または Omit を使用します。