Search code examples
gogo-gormgo-gin

use same struct for both create and update operations in go lang using gin and gorm


Disclaimer: go lang noob here

I've my user struct as

type User struct {
    ID        uint32    `json:"id"`
    FirstName string    `json:"firstName" binding:"required"`
    LastName  string    `json:"lastName"`
    Email     string    `json:"email" binding:"required,email,uniqueModelValue=users email"`
    Active    bool      `json:"active"`
    Password  string    `json:"password,omitempty" binding:"required,gte=8"`
    UserType  string    `json:"userType" binding:"oneof=admin backoffice principal staff parent student"`
    CreatedAt time.Time `json:"createdAt"`
    UpdatedAt time.Time `json:"updatedAt"`
}

/POST /users handler

func Create(ctx *gin.Context) {
    user := models.User{}
    //validate
    if err := ctx.ShouldBind(&user); err != nil {
        response.Error(ctx, err)
        return

    }
    db, _ := database.GetDB()
    db.Create(&user)
    // removing password from the response
    user.Password = ""
    response.Success(ctx, user)
}

I want to create an update handler using the same struct, is there any way I can perform using the same struct ?

Mind you, struct has required bindings on many fields firstName,email etc. while updating, I might not pass these fields

I came up with something like

/PUT /users/ handler

func Update(ctx *gin.Context) {
    userID := ctx.Param("userId")
    user := models.User{}
    db, _ := database.GetDB()
    if err := db.First(&user, userID).Error; err != nil {
        response.Error(ctx, err)
        return
    }
    updateUser := models.User{}
    if err := ctx.BindJSON(&updateUser); err != nil {
        response.Error(ctx, err)
    }
    //fmt.Printf("%v", updateUser)
    db.Model(&user).Updates(updateUser)
    response.Success(ctx, user)
}

this is failing obviously due to required validations missing, if I try to update, let's say, just the lastName


Solution

  • You could try for this case to bind to the User struct you just pulled out of the database, like so:

    func Update(ctx *gin.Context) {
        userID := ctx.Param("userId")
        user := models.User{}
        db, _ := database.GetDB()
        if err := db.First(&user, userID).Error; err != nil {
            response.Error(ctx, err)
            return
        }
        if err := ctx.BindJSON(&user); err != nil {
            response.Error(ctx, err)
        }
        db.Model(&user).Updates(user)
        response.Success(ctx, user)
    }
    

    That might work because the old values + the changes (written into the struct by BindJSON) might be able to pass validation.

    Generally speaking, this pattern isn't going to help you for long in Go.

    Using the same struct for two different purposes: representation of the entity model and representation of the messages in your API that pertain to the model, will end up giving you trouble sooner or later. These tend to be slightly different, as for example you might eventually have fields that are only exposed internally or, as you have encountered, you have validation that doesn't make sense for all use cases.

    For this problem what would help you is to create a new struct to represent the update user message:

    package messages
    
    type UpdateUser struct {
        FirstName string    `json:"firstName"`
        LastName  string    `json:"lastName"`
        ... fields that are updatable with appropriate validation tags
    }
    
    func (u *UpdateUser) ToModel() *model.User {
        return &model.User{
           FirstName: u.FirstName,
           LastName: u.LastName,
           ...
        }
    }
    

    Then use that as to validate your request model, then turn it into a model.User for the update:

    func Update(ctx *gin.Context) {
        userID := ctx.Param("userId")
        user := models.User{}
        db, _ := database.GetDB()
        if err := db.First(&user, userID).Error; err != nil {
            response.Error(ctx, err)
            return
        }
        updateUser := messages.UpdateUser{}
        if err := ctx.BindJSON(&updateUser); err != nil {
            response.Error(ctx, err)
        }
        //fmt.Printf("%v", updateUser)
        db.Model(&user).Updates(updateUser.ToModel())
        response.Success(ctx, user)
    }