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