Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
163 views
in Technique[技术] by (71.8m points)

go - Updates() doesn't seem to update the associations

I have the following structs.

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"`
}

And the following handler

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))
    }
}

I read

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

and

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

but it does not seem to work.

I use a rest client to call the endpoint with the following JSON

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

And I parse it into a struct. When I print that struct update := new(model.Company) it has the correct data. But when I run the update I get the following output?

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

So it does inserts for it's relations. But I want it to update the records.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

The Insert statements you see are in fact Upsert statements, you'll notice that after the insert part, they have an ON CONFLICT clause. What that does is update the record if the primary key is already in the table (i.e. there is a key conflict, hence the name). So Updates is working as expected.

BUT!

There is still a problem. As it stands those statements will still result in a new Relation and a new Address being inserted into the database because there will be no primary key conflict. This stems from the fact that you've not entered a primary key for the Relation or the Address row (you'll see that there is no id column in the insert).

Rule of thumb:

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.

You might think that you have given a primary key in the Where call, but that only applies to the top level struct. When gorm processes the relations, it sees no primary keys and assumes you're wanting to add new relations, not update the existing ones.

If you think about it for a moment, this makes sense: how could gorm know which Relation is currently related to the Company if you don't provide a Relation.ID. The way to know that would be to do a SELECT with the OwnerID and OwnerType first to find out, but this isn't something gorm will do for you (gorm in general tries to be minimalistic and doesn't attempt to find out more info on it's own than what you've given).

One trivial way to tackle this would be to have your API user provide the Relation.ID and Address.IDs in the input, but that's highly inconvenient in this case.

A good pattern I have found to tackle this is to first load the current state of your target root object together with the relevant relations that you want to update, then apply the changes coming from the API user to the struct and relations, then use Save. This would save all fields regardless of whether they were changed.

Another option would be to put in the existing IDs of the relations into update.Relation.ID and update.Relation.Addresses[i].ID from the version you've pulled out of the DB and then calling Updates as you've done here.

Bonus: HasMany has more update challenges

Special care would need to be taken in the case of Addresses because it's a HasMany relation. When calling Save/Updates gorm will never delete relations, so when for example you had 3 addresses before and now want to have just two, it will just update two of them and leave the third one hanging about; this is to prevent removal of data by mistake. Instead you have to be explicit about your intention.

What you want to do then is to use Association mode to replace the association, but in the same transaction as where you call 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
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
...