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.
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.
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.
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With