Updates() 似乎没有更新关联

Updates() doesn't seem to update the associations

我有以下结构。

type Relation struct {
    gorm.Model
    OwnerID      uint          `json:"ownerID"`
    OwnerType    string        `json:"ownerType"`
    Addresses    []Address     `gorm:"polymorphic:Owner;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"addresses"`
}

type Company struct {
    gorm.Model
    Name     string   `json:"name"`
    Relation Relation `gorm:"polymorphic:Owner;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"relation"`
}

type Address struct {
    gorm.Model
    OwnerID   uint   `json:"ownerID"`
    OwnerType string `json:"ownerType"`
    Country   string `json:"country"`
    Zip       string `json:"zip"`
    Number    uint   `json:"number,string"`
    Addition  string `json:"addition"`
    Street    string `json:"street"`
    State     string `json:"state"`
    City      string `json:"city"`
}

以及以下处理程序

func UpdateCompany(db *database.Database) fiber.Handler {
    return func(c *fiber.Ctx) error {
        id, err := IDFromParams(c)
        if err != nil {
            return c.JSON(responseKit.ParameterMissing())
        }

        update := new(model.Company)
        if err = json.Unmarshal(c.Body(), update); err != nil {
            return c.JSON(responseKit.ParsingError())
        }

        if result := db.Session(&gorm.Session{FullSaveAssociations: true}).Where("id = ?", id).Debug().Updates(update); result.Error != nil {
            return c.JSON(responseKit.RecordUpdateError())
        }

        updated := new(model.Company)
        result := db.Preload("Relation.Addresses").
            Find(updated, id)

        if result.Error != nil {
            return c.JSON(responseKit.RecordReadError())
        }

        return c.JSON(responseKit.RecordUpdatedSuccess(updated))
    }
}

我读了

https://gorm.io/docs/associations.html

https://github.com/go-gorm/gorm/issues/3487#issuecomment-698303344

但是好像不行。

我使用休息客户端通过以下方式调用端点 JSON

{
    "name": "yep",
    "relation": {
        "adresses": [
            {
                "number": "5"
            }
        ]
    }
}

然后我将它解析成一个结构。当我打印那个 struct update := new(model.Company) 它有正确的数据。但是当我 运行 更新时,我得到以下输出?

INSERT INTO "addresses" ("created_at","updated_at","deleted_at","owner_id","owner_type","country","zip","number","addition","street","state","city") VALUES ('2021-01-14 10:48:56.399','2021-01-14 10:48:56.399',NULL,11,'relations','','',5,'','','','') ON CONFLICT ("id") DO UPDATE SET "created_at"="excluded"."created_at","updated_at"="excluded"."updated_at","deleted_at"="excluded"."deleted_at","owner_id"="excluded"."owner_id","owner_type"="excluded"."owner_type","country"="excluded"."country","zip"="excluded"."zip","number"="excluded"."number","addition"="excluded"."addition","street"="excluded"."street","state"="excluded"."state","city"="excluded"."city" RETURNING "id"
INSERT INTO "relations" ("created_at","updated_at","deleted_at","owner_id","owner_type") VALUES ('2021-01-14 10:48:56.399','2021-01-14 10:48:56.399',NULL,0,'companies') ON CONFLICT ("id") DO UPDATE SET "created_at"="excluded"."created_at","updated_at"="excluded"."updated_at","deleted_at"="excluded"."deleted_at","owner_id"="excluded"."owner_id","owner_type"="excluded"."owner_type" RETURNING "id"
UPDATE "companies" SET "updated_at"='2021-01-14 10:48:56.399',"name"='yep' WHERE id = 3

因此它会为其关系插入。但是我想让它更新记录。

你看到的Insert语句实际上是Upsert语句,你会注意到在insert部分之后,它们有一个ON CONFLICT子句。如果主键已经在 table 中(即存在键冲突,因此得名),则更新记录。所以 Updates 正在按预期工作。

但是!

还有问题。就目前而言,这些语句仍将导致新的关系和新的地址被插入到数据库中,因为不会有主键冲突。这是因为您没有为 Relation 或 Address 行输入主键(您会看到插入中没有 id 列)。

经验法则:

Gorm treats structs with a zero primary key as new rows, and inserts them on save. Corollary: you can only update structs that have a nonzero primary key.

您可能认为您在 Where 调用中给出了主键,但这仅适用于顶级结构。当 gorm 处理关系时,它看不到主键并假定您想要添加新关系,而不是更新现有关系。

如果您仔细考虑一下,这是有道理的:如果您不提供 Relation.ID,gorm 如何知道当前与公司相关的关系。知道的方法是先用 OwnerID 和 OwnerType 做一个 SELECT 来找出答案,但这不是 gorm 会为你做的事情(gorm 通常会尝试简约并且不会尝试自行查找比您提供的信息更多的信息。

解决这个问题的一个简单方法是让您的 API 用户在输入中提供 Relation.ID 和 Address.IDs,但在这种情况下这非常不方便。

我发现解决这个问题的一个好模式是首先加载目标根对象的当前状态以及要更新的相关关系,然后应用来自 API 用户的更改到结构和关系,然后使用 Save。这将保存所有字段,无论它们是否被更改。

另一种选择是将关系的现有 ID 从您从数据库中拉出的版本中放入 update.Relation.IDupdate.Relation.Addresses[i].ID,然后调用 Updates 作为你已经完成了这里。

奖励:HasMany 有更多更新挑战

地址的情况需要特别小心,因为它是一个 HasMany 关系。当调用 Save/Updates 时,gorm 永远不会删除关系,所以当例如你之前有 3 个地址而现在只想有两个时,它只会更新其中两个并留下第三个悬而未决;这是为了防止错误地删除数据。相反,您必须明确说明您的意图。

然后你要做的是用Association模式来代替association,但是在你调用Updates的同一个事务中:

tx := db.Begin()
// do your thing
err := tx.
    Session(&gorm.Session{FullSaveAssociations: true}).
    Where("id = ?", id).
    Omit("Relation.Addresses")
    Updates(update).
    Error
if err != nil {
    tx.Rollback()
    // handle error
}
// assuming update.Relation.ID is set
err = tx.
    Model(&update.Relation).
    Association("Addresses").
    Replace(&update.Relation.Addresses)
if err != nil {
    tx.Rollback()
    // handle error
}
if err := tx.Commit().Error; err !=  nil {
    tx.Rollback()
    // handle error
}