Hasham Ali

How to Use UUID Key Type with Gorm

I recently picked up Go after years of Python, Java, and most recently, Kotlin. So far, I’ve been quite impressed.

One of the best features is the incredibly deep set of packages that is part of the standard library. On the rare occasion I’ve needed to use an external framework, I’ve found the community support to be just as thorough.

Among the more useful packages I’ve come across is GORM, a fantastic ORM for the Go language. It has covered all of my ORM related needs. And while the documentation is also quite good, I couldn’t find a good example for a specific use case I ran into: using UUIDs as keys. Luckily, it’s not too complex at all!

In this article, I’ll run through a quick example using UUID as the key type for some tables.

Installation

First, you’ll need to install the appropriate packages:

  1. GORM
  2. go.uuid
    go get github.com/jinzhu/gorm
    go get github.com/satori/go.uuid

Base Model

Next, we will create a Base model struct that our subsequent models will embed to contain common columns, mainly: id, created_at, updated_at, and deleted_at. This is the same structure as GORM’s Model base class, but with the ID type swapped to UUID.

Feel free to skip this if you don’t care about the time fields, and embed the ID field directly.

    // Base contains common columns for all tables.
    type Base struct {
     ID        uuid.UUID `gorm:"type:uuid;primary_key;"`
     CreatedAt time.Time `json:"created_at"`
     UpdatedAt time.Time `json:"update_at"`
     DeletedAt *time.Time `sql:"index" json:"deleted_at"`
    }

    // BeforeCreate will set a UUID rather than numeric ID.
    func (base *Base) BeforeCreate(scope *gorm.Scope) error {
     uuid, err := uuid.NewV4()
     if err != nil {
      return err
     }
     return scope.SetColumn("ID", uuid)
    }

Notice the BeforeCreate function after the model. As the name implies, this is run before every create call via the ORM. We can use this to generate a new UUID and auto-populate it.

Data Models

Now, we can use the above Base model to create our subsequent data models to reflect tables. We’ll create a User model and a Profile model that has a one-to-one relationship with User.

    // User is the model for the user table.
    type User struct {
     Base
     SomeFlag bool    `gorm:"column:some_flag;not null;default:true" json:"some_flag"`
     Profile  Profile `json:"profile"`
    }

    // Profile is the model for the profile table.
    type Profile struct {
     Base
     Name   string    `gorm:"column:name;size:128;not null;" json:"name"`
     UserID uuid.UUID `gorm:"type:uuid;column:user_foreign_key;not null;" json:"-"`
    }

CRUD

Finally, we can apply some basic CRUD (create, read, update, delete) operations, at least the create and read parts.

    func main() {
     db, err := gorm.Open("sqlite3", "test.db")
     if err != nil {
      panic(err)
     }

    db.LogMode(true)
     db.AutoMigrate(&User{}, &Profile{})

    user := &User{SomeFlag: false}
     if db.Create(&user).Error != nil {
      log.Panic("Unable to create user.")
     }

    profile := &Profile{Name: "New User", UserID: user.Base.ID}
     if db.Create(&profile).Error != nil {
      log.Panic("Unable to create profile.")
     }

    fetchedUser := &User{}
     if db.Where("id = ?", profile.UserID).Preload("Profile").First(&fetchedUser).RecordNotFound() {
      log.Panic("Unable to find created user.")
     }

    fmt.Printf("User: %+v\n", fetchedUser)
    }

In this function, we create a User. We then create a Profile that references the created User. Lastly, we confirm that the fetched User matches the initially created one.

Full Example

And that’s it! Putting it all together, we have the following:

    package main

    import (
     "fmt"
     "log"
     "time"

    "github.com/jinzhu/gorm"
     _ "github.com/jinzhu/gorm/dialects/sqlite"
     "github.com/satori/go.uuid"
    )

    // Base contains common columns for all tables.
    type Base struct {
     ID        uuid.UUID  `gorm:"type:uuid;primary_key;"`
     CreatedAt time.Time  `json:"created_at"`
     UpdatedAt time.Time  `json:"update_at"`
     DeletedAt *time.Time `sql:"index" json:"deleted_at"`
    }

    // BeforeCreate will set a UUID rather than numeric ID.
    func (base *Base) BeforeCreate(scope *gorm.Scope) error {
     uuid, err := uuid.NewV4()
     if err != nil {
      return err
     }
     return scope.SetColumn("ID", uuid)
    }

    // User is the model for the user table.
    type User struct {
     Base
     SomeFlag bool    `gorm:"column:some_flag;not null;default:true" json:"some_flag"`
     Profile  Profile `json:"profile"`
    }

    // Profile is the model for the profile table.
    type Profile struct {
     Base
     Name   string    `gorm:"column:name;size:128;not null;" json:"name"`
     UserID uuid.UUID `gorm:"type:uuid;column:user_foreign_key;not null;" json:"-"`
    }

    func main() {
     db, err := gorm.Open("sqlite3", "test.db")
     if err != nil {
      panic(err)
     }

    db.LogMode(true)
     db.AutoMigrate(&User{}, &Profile{})

    user := &User{SomeFlag: false}
     if db.Create(&user).Error != nil {
      log.Panic("Unable to create user.")
     }

    profile := &Profile{Name: "New User", UserID: user.Base.ID}
     if db.Create(&profile).Error != nil {
      log.Panic("Unable to create profile.")
     }

    fetchedUser := &User{}
     if db.Where("id = ?", profile.UserID).Preload("Profile").First(&fetchedUser).RecordNotFound() {
      log.Panic("Unable to find created user.")
     }

    fmt.Printf("User: %+v\n", fetchedUser)
    }

The gist for this code can be found here.