I am trying to build a REST API unsing golang and GORM specifically. I have four entities: Promo, which contains many PromoUnique entities, one Target entity which also contains many Category entities. Code:
type Promo struct {
PromoID string `json:"promo_id" gorm:"primaryKey;not null;type:uuid;default:gen_random_uuid()"`
CompanyID string `json:"company_id" gorm:"not null;foreignKey:ID"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
Target Target `json:"target" gorm:"foreignKey:TargetID;not null"`
Active bool `json:"-" gorm:"default:true"`
ActiveFrom time.Time `json:"active_from"`
ActiveUntil time.Time `json:"active_until"`
Description string `json:"description" gorm:"not null"`
ImageURL string `json:"image_url"`
MaxCount int `json:"max_count" gorm:"not null"`
Mode string `json:"mode" gorm:"not null"`
LikeCount int `json:"like_count" gorm:"default:0"`
UsedCount int `json:"used_count" gorm:"default:0"`
PromoCommon string `json:"promo_common"`
PromoUnique []PromoUnique `json:"promo_unique" gorm:"foreignKey:PromoUniqueID;"`
}
type PromoUnique struct {
PromoUniqueID string `json:"-" gorm:"primaryKey;not null;type:uuid;default:gen_random_uuid()"`
PromoID string `json:"-" gorm:"not null;foreignKey:PromoID"`
Body string `json:"-" gorm:"not null"`
Activated bool `json:"-" gorm:"default:false"`
}
type Target struct {
TargetID string `json:"-" gorm:"primaryKey;not null;type:uuid;default:gen_random_uuid()"`
PromoID string `json:"-" gorm:"not null;foreignKey:PromoID"`
AgeFrom int `json:"age_from"`
AgeUntil int `json:"age_until"`
Country countries.CountryCode `json:"country"`
Categories []Category `json:"categories" gorm:"foreignKey:CategoryID;"`
}
type Category struct {
CategoryID string `json:"id" gorm:"primaryKey;not null;type:uuid;default:gen_random_uuid()"`
TargetID string `json:"-" gorm:"not null"`
Name string `json:"name" gorm:"not null"`
}
But when I try to create a promo record in the database, it would have only one PromoUnique and Target would have only one Category, despite me creating it with multiple PromoUniques and categories. Service’s and storage’s code:
func (s *promoStorage) Create(ctx context.Context, promo entity.Promo) (*entity.Promo, error) {
err := s.db.Model(&promo).WithContext(ctx).Create(&promo).Error
return &promo, err
}
func (s *promoService) Create(ctx context.Context, fiberCTX fiber.Ctx, promoDTO dto.PromoCreate) (*entity.Promo, error) {
activeFrom, timeError := time.Parse("2006-01-02", promoDTO.ActiveFrom)
if timeError != nil {
return nil, timeError
}
activeUntil, timeError := time.Parse("2006-01-02", promoDTO.ActiveUntil)
if timeError != nil {
return nil, timeError
}
var categories []entity.Category
var promoUniques []entity.PromoUnique
for _, category := range promoDTO.Target.Categories {
categories = append(categories, entity.Category{
Name: category,
})
}
for _, promoUnique := range promoDTO.PromoUnique {
promoUniques = append(promoUniques, entity.PromoUnique{
Body: promoUnique,
})
}
company := fiberCTX.Locals("business").(*entity.Business)
promo := entity.Promo{
Target: entity.Target{
AgeFrom: promoDTO.Target.AgeFrom,
AgeUntil: promoDTO.Target.AgeUntil,
Country: countries.ByName(strings.ToUpper(promoDTO.Target.Country)),
Categories: categories,
},
CompanyID: company.ID,
Active: true,
ActiveFrom: activeFrom,
ActiveUntil: activeUntil,
Description: promoDTO.Description,
ImageURL: promoDTO.ImageURL,
MaxCount: promoDTO.MaxCount,
Mode: promoDTO.Mode,
PromoCommon: promoDTO.PromoCommon,
PromoUnique: promoUniques,
}
company.Promos = append(company.Promos, promo)
_, err := s.businessStorage.Update(ctx, company)
if err != nil {
return nil, err
}
return s.promoStorage.Create(ctx, promo)
}
My request body:
{
"description": "Повышенный кэшбек 10% для новых клиентов бака!",
"image_url": "https://cdn2.thecatapi.com/images/3lo.jpg",
"target": {
"age_from": 15,
"age_until": 20,
"country": "ru",
"categories": [
"коты",
"футбол",
"учитель"
]
},
"max_count": 100000000,
"active_from": "2025-01-18",
"active_until": "2025-01-20",
"mode": "COMMON",
"promo_common": "sale-10",
"promo_unique": [
"winter-sale-30-abc28f99qa",
"winter-sale-30-299faab2c",
"sale-100-winner"
]
}
What should I do? I did research on this topic, but I didn’t find any problems with my code. Appreciate any help!
EDIT: queries, that gorm runs on creation:
backend-1 | 2025/01/19 12:18:10 /opt/internal/adapters/database/postgres/promo.go:25
backend-1 | [0.798ms] [rows:1] INSERT INTO "categories" ("target_id","name","category_id") VALUES ('','коты','8e450f1e-ceb4-4a15-918b-63c91f8d2dbb') ON CONFLICT ("category_id") DO UPDATE SET "category_id"="excluded"."category_id" RETURNING "category_id"
backend-1 |
backend-1 | 2025/01/19 12:18:10 /opt/internal/adapters/database/postgres/promo.go:25
backend-1 | [1.889ms] [rows:1] INSERT INTO "targets" ("promo_id","age_from","age_until","country","target_id") VALUES ('',15,20,643,'8e450f1e-ceb4-4a15-918b-63c91f8d2dbb') ON CONFLICT ("target_id") DO UPDATE SET "target_id"="excluded"."target_id" RETURNING "target_id"
backend-1 |
backend-1 | 2025/01/19 12:18:10 /opt/internal/adapters/database/postgres/promo.go:25
backend-1 | [0.871ms] [rows:1] INSERT INTO "promo_uniques" ("promo_id","body","activated","promo_unique_id") VALUES ('','winter-sale-30-abc28f99qa',false,'8e450f1e-ceb4-4a15-918b-63c91f8d2dbb') ON CONFLICT ("promo_unique_id") DO UPDATE SET "promo_unique_id"="excluded"."promo_unique_id" RETURNING "promo_unique_id"
backend-1 |
backend-1 | 2025/01/19 12:18:10 /opt/internal/adapters/database/postgres/promo.go:25
backend-1 | [5.933ms] [rows:1] INSERT INTO "promos" ("company_id","created_at","updated_at","active","active_from","active_until","description","image_url","max_count","mode","like_count","used_count","promo_common") VALUES ('abbe7343-c3a1-4ff2-b18b-1135632bdbf7','2025-01-19 12:18:10.395','2025-01-19 12:18:10.395',true,'2025-01-18 00:00:00','2025-01-20 00:00:00','Повышенный кэшбек 10% для новых клиентов бака!','https://cdn2.thecatapi.com/images/3lo.jpg',100000000,'COMMON',0,0,'sale-10') RETURNING "promo_id"
2
Answers
Solved via using raw SQL and changing my entities:
Shouldn’t your foreignKey be "PromoID" as well?
When inserting to PromoUnique, promo_id is empty and only promoUniqueId is being filled up. I’m assuming that the value being set on promoUniqueId is Promo.PromoId. And since promoUniqueId is the primary key, only one would be saved.