The Wizard's Guide to GORM and PostgreSQL: Upsert with Ease

kochurovro

Roman

Posted on June 5, 2024

The Wizard's Guide to GORM and PostgreSQL: Upsert with Ease

Interesting story. You have two PostgreSQL tables related to each other in a one-to-many relationship.

type Availability struct {
    ID        int64    `gorm:"column:id;primaryKey"`
    ProductID int64    `gorm:"column:product_id"`
    Enabled   bool     `gorm:"column:enabled;not null"`
    Dates     []*Dates `gorm:"foreignKey:AvailabilityID"`
}

type Dates struct {
    ID             int64  `gorm:"column:id;primaryKey"`
    AvailabilityID int64  `gorm:"column:availability_id;foreignKey:Availability" json:"availability_id"`
    Date           string `gorm:"column:date"`
}
Enter fullscreen mode Exit fullscreen mode

And such an index product_availability_dates_uindex(AvailabilityID, date) in the Dates table. You need to update the Enabled field in the Availability table using GORM. If such a record does not exist, create it.

func (that *AvailabilityRepo) Upsert(ctx context.Context, pa []*model.ProductAvailability) error {
    for _, a := range pa {
        tx := that.db.WithContext(ctx).Model(&model.ProductAvailability{}).
            Where("product_id = ?", a.ProductID).
            Update("enabled = ?", a.Enabled)
        if tx.Error != nil {
            return fmt.Errorf("%w: failed to update product availability", tx.Error)
        }

        if tx.RowsAffected == 0 {
            if err := tx.Create(&a).Error; err != nil {
                return fmt.Errorf("%w: failed to create product availability", err)
            }
        }

        return nil
    }
}
Enter fullscreen mode Exit fullscreen mode

Problem 1. We need to return the ID if the field is updated. Currently, the ID is not being returned. We try to pass the instance of the object directly in the Model. This method seems to work, but not for me.

func (that *AvailabilityRepo) Upsert(ctx context.Context, pa []*model.ProductAvailability) error {
    for _, a := range pa {
        tx := that.db.WithContext(ctx).Model(&a).
            Where("product_id = ?", a.ProductID).
            Update("enabled = ?", a.Enabled)
        if tx.Error != nil {
            return fmt.Errorf("%w: failed to update product availability", tx.Error)
        }

        if tx.RowsAffected == 0 {
            if err := tx.Create(&a).Error; err != nil {
                return fmt.Errorf("%w: failed to create product availability", err)
            }
        }

        return nil
    }
}
Enter fullscreen mode Exit fullscreen mode

Why the above update did not work for me. The first reason, it makes me want to wash my hands after such an approach. The second reason is that the tables are connected through a unique index. And GORM, by some internal magic, returns an error because the uniqueness index is triggered. You didn't touch the dates field, but the error will be related to this index.

How I solved the problem for myself:

func (that *AvailabilityRepo) Upsert(ctx context.Context, pa []*model.ProductAvailability) error {
    for _, a := range pa {
        idContainer := model.ProductAvailability{}
        tx := updatesBuilder(that.db.WithContext(ctx).Model(&idContainer).
            Clauses(clause.Returning{Columns: []clause.Column{
                {Name: "id"},
            }}).
            Where("product_id = ?", a.ProductID), deprecate, a)
        if tx.Error != nil {
            return fmt.Errorf("%w: failed to update product availability", tx.Error)
        }

        if tx.RowsAffected == 0 {
            if err := tx.Create(&a).Error; err != nil {
                return fmt.Errorf("%w: failed to create product availability", err)
            }
        }

        if a.ID == 0 {
            a.ID = idContainer.ID
        }
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
kochurovro
Roman

Posted on June 5, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related