Search code examples
gogo-gormgo-gin

How to return nested entities after creating a new object?


Model Account contains nested structures - Currency and User

When I create a new instance of Account in DB, and then return it in my response, nested entities are empty:


type Account struct {
    BaseModel
    Name       string          `gorm:"size:64;not null" json:"name"`
    Balance    decimal.Decimal `gorm:"type:decimal(16, 2);default:0;not null;" json:"balance"`
    UserID     int             `gorm:"not null" json:"-"`
    User       User            `gorm:"foreignKey:UserID" json:"user"`
    CurrencyID int             `gorm:"not null" json:"-"`
    Currency   Currency        `gorm:"foreignKey:CurrencyID" json:"currency"`
}

type CreateAccountBody struct {
    Name       string          `json:"name" binding:"required"`
    Balance    decimal.Decimal `json:"balance"`
    CurrencyID int             `json:"currency_id" binding:"required"`
}

func CreateAccount(ctx *gin.Context) {
    body := CreateAccountBody{}

    if err := ctx.Bind(&body); err != nil {
        log.Println("Error while binding body:", err)
        ctx.JSON(
            http.StatusBadRequest,
            gin.H{"error": "Wrong request parameters"},
        )
        return
    }

    account := Account {
        Name:       body.Name,
        Balance:    body.Balance,
        CurrencyID: body.CurrencyID,
        UserID:     1,
    }
    
    if result := db.DB.Create(&account); result.Error != nil {
        log.Println("Unable to create an account:", result.Error)
    }    

    ctx.JSON(http.StatusCreated, gin.H{"data": account})
}


To avoid this problem, I refresh account variable with separate query:

db.DB.Create(&account)
db.DB.Preload("User").Preload("Currency").Find(&account, account.ID)
ctx.JSON(http.StatusCreated, gin.H{"data": account})

Is this the most effective and correct way to achieve the desired result?


Solution

  • I'm gonna share you how usually I managed this scenario. First, let me share the code.

    main.go file

    package main
    
    import (
        "context"
    
        "gogindemo/handlers"
    
        "github.com/gin-gonic/gin"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
    )
    
    var (
        db  *gorm.DB
        ctx *gin.Context
    )
    
    func init() {
        dsn := "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable"
        var err error
        db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
            panic(err)
        }
    
        db.AutoMigrate(&handlers.Currency{})
        db.AutoMigrate(&handlers.User{})
        db.AutoMigrate(&handlers.Account{})
    }
    
    func AddDb() gin.HandlerFunc {
        return func(ctx *gin.Context) {
            ctx.Request = ctx.Request.WithContext(context.WithValue(ctx.Request.Context(), "DB", db))
            ctx.Next()
        }
    }
    
    func main() {
        db.Create(&handlers.User{Id: 1, Name: "john doe"})
        db.Create(&handlers.User{Id: 2, Name: "mary hut"})
        db.Create(&handlers.Currency{Id: 1, Name: "EUR"})
        db.Create(&handlers.Currency{Id: 2, Name: "USD"})
    
        r := gin.Default()
        r.POST("/account", AddDb(), handlers.CreateAccount)
    
        r.Run()
    }
    

    Here, I've just added the code for bootstrapping the database objects and add some dummy data to it.

    handlers/handlers.go file

    package handlers
    
    import (
        "net/http"
    
        "github.com/gin-gonic/gin"
        "github.com/shopspring/decimal"
        "gorm.io/gorm"
    )
    
    type User struct {
        Id   int
        Name string
    }
    
    type Currency struct {
        Id   int
        Name string
    }
    
    type Account struct {
        Id         int
        Name       string          `gorm:"size:64;not null" json:"name"`
        Balance    decimal.Decimal `gorm:"type:decimal(16, 2);default:0;not null;" json:"balance"`
        UserID     int             `gorm:"not null" json:"-"`
        User       User            `gorm:"foreignKey:UserID" json:"user"`
        CurrencyID int             `gorm:"not null" json:"-"`
        Currency   Currency        `gorm:"foreignKey:CurrencyID" json:"currency"`
    }
    
    type CreateAccountBody struct {
        Name       string          `json:"name" binding:"required"`
        Balance    decimal.Decimal `json:"balance"`
        CurrencyID int             `json:"currency_id" binding:"required"`
    }
    
    func CreateAccount(c *gin.Context) {
        db, ok := c.Request.Context().Value("DB").(*gorm.DB)
        if !ok {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
            return
        }
        var accountReq CreateAccountBody
        if err := c.BindJSON(&accountReq); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": "wrong request body payload"})
            return
        }
    
        // create Account & update the "account" variable
        account := Account{Name: accountReq.Name, Balance: accountReq.Balance, CurrencyID: accountReq.CurrencyID, UserID: 1}
        db.Create(&account).Preload("Currency").Preload("User").Find(&account, account.Id)
    
        c.IndentedJSON(http.StatusCreated, account)
    }
    

    Within this file, I actually talk with the database through the DB passed in the context. Now, back to your question.
    If the relationship between the Currency/Account and User/Account is of type 1:1, then, you should rely on the Preload clause. This will load the related entity in a separate query instead of adding it in an INNER JOIN clause.

    Let me know if this solves your issue, thanks!