What is GORM? It is an ORM built to be used in golang. Previously I was using bun but came across a few issues which proved more complex than they should be so I transitioned to GORM to test out if those would help solve the issues I came across. The resulting code was far smaller but there were some things that proved challenging in GORM.
What is this post about? I have started using GORM recently and came across a few problems with it. This post is a summary of those problems and how I solved them.
This is the model I will be basing my examples on. It is part of a billing system where a Person has a credit card. Since they can only have one credit card it will be a has-one
relationship. We are just storing the billing token associated with the card.
type Person struct {
ID int
Name string
Email string
CreditCard CreditCard
CreditCardID int
CreatedAt time.Time
DeletedAt time.Time
UpdatedAt time.Time
}
type CreditCard struct {
ID int
Token int64
CreatedAt time.Time
DeletedAt time.Time
UpdatedAt time.Time
}
Get
How I thought it would work: Normally I would have thought that within the ORM if you selected a Person it would automatically return the associated credit card. The line below should return the first person with the id of 10.
db.First(&person, "10")
However this does not return the associated credit card. Instead there’s nothing attached.
How it actually works: Instead, we must preload the credit card so GORM knows it must fetch that model too. Now we can get credit card data from like the token by using person.CreditCard.Token
e.db.Model(&Person{}).Preload("CreditCard").First(&person, id)
Soft Delete
https://gorm.io/docs/delete.html#Soft-Delete
In the examples above we have a DeletedAt
field on each of the models. In this example if we call the following line to delete the record, GORM will automatically detect it should be soft deleted and set the DeletedAt
field to the current time. It translates to the SQL below
db.Delete(&Person{}, 10)
// UPDATE persons SET deleted_at="2023-05-16 08:23" WHERE id = 10;
How I thought it would work: In the example I have, the person has a credit card. So when I call delete upon a person, I would expect the associated credit card to be deleted also. However this does not happen and the credit card appears as active even though the person it is associated to is deleted.
How it actually works: In this StackOverflow answer they give a compelling reason to rearchitect the database schema to move deleted records to a separate table instead of soft deleting them. This might be a good answer for some but in my case I did not want to change everything to fit the ORM. Instead I used a transaction to delete both
err := db.Transaction(func(tx *gorm.DB) error {
result := tx.Delete(&Person{}, id)
if result.Error != nil {
return result.Error
}
result = tx.Where("credit_card_id = ?", id).Delete(&CreditCard{})
if result.Error != nil {
return result.Error
}
return nil
})