Using GORM library to access postgreSQL with JSONB field with GoLang

This tutorial demonstrates how to ‘auto migrate’ the DB and how to store and retrieve data  from JSONB field in postgreSQL database using GoLang.

Auto migrating the DB

In the context of Go programming language and GORM (Go Object Relational Mapping) library, automigration is a feature that automatically creates or updates database tables based on the Go struct definitions.

For the purpose of this example, we will create a table with id, username, email and meta data field. The meta data will be a JSONB field. We could use JSON as well but JSONB is stored in binary format, and although insert operations are slower searching is faster.

In general it is recommended to always use JSONB unless we have a real good reason to use JSON. For example JSON preserves formatting and allows for duplicate keys.

But before we could use JSONB with GORM we have to install the package

go get github.com/jackc/pgtype

Create a connection to PostgreSQL database.

Gorm supports different databases but here will do this exercise with PostgreSQL only.

func connectToPostgreSQL() (*gorm.DB, error) {
    dsn := "user=toninichev dbname=tests host=localhost port=5432 sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }

    return db, nil
}

Define the structure that will be used to create the the Users table

type User struct {
    ID       uint   `gorm:"primaryKey"`
    Username string `gorm:"unique"`
    Email    string
    Age      int
    MetaData pgtype.JSONB `gorm:"type:jsonb" json:"fieldnameofjsonb"`
}

 

Now let’s create two helper functions that will create a new user:

func createuserWithMetaData(db *gorm.DB, username string, email string, metaData string) User {
    jsonData := pgtype.JSONB{}
    err := jsonData.Set([]byte(metaData))
    if err != nil {
        log.Fatal(err)
    }
    // Create a user
    newUser := User{Username: username, Email: email, Age: 36, MetaData: jsonData}
    err = createUser(db, &newUser)
    if err != nil {
        log.Fatal(err)
    }
    return newUser
}

func createUser(db *gorm.DB, user *User) error {
    result := db.Create(user)
    if result.Error != nil {
        return result.Error
    }
    return nil
}

And let’s put it all together:

The entire code

package main

import (
    "log"

    "github.com/jackc/pgtype"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type User struct {
    ID       uint   `gorm:"primaryKey"`
    Username string `gorm:"unique"`
    Email    string
    Age      int
    MetaData pgtype.JSONB `gorm:"type:jsonb" json:"fieldnameofjsonb"`
}

func connectToPostgreSQL() (*gorm.DB, error) {
    dsn := "user=toninichev dbname=tests host=localhost port=5432 sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }

    return db, nil
}

func createuserWithMetaData(db *gorm.DB, username string, email string, metaData string) User {
    jsonData := pgtype.JSONB{}
    err := jsonData.Set([]byte(metaData))
    if err != nil {
        log.Fatal(err)
    }
    // Create a user
    newUser := User{Username: username, Email: email, Age: 36, MetaData: jsonData}
    err = createUser(db, &newUser)
    if err != nil {
        log.Fatal(err)
    }
    return newUser
}

func createUser(db *gorm.DB, user *User) error {
    result := db.Create(user)
    if result.Error != nil {
        return result.Error
    }
    return nil
}

func getUserByID(db *gorm.DB, userID uint) (*User, error) {
    var user User
    result := db.First(&user, userID)
    if result.Error != nil {
        return nil, result.Error
    }
    return &user, nil
}

func updateUser(db *gorm.DB, user *User) error {
    result := db.Save(user)
    if result.Error != nil {
        return result.Error
    }
    return nil
}

func deleteUser(db *gorm.DB, user *User) error {
    result := db.Delete(user)
    if result.Error != nil {
        return result.Error
    }
    return nil
}

func autoMigrateDB(db *gorm.DB) {
    // Perform database migration
    err := db.AutoMigrate(&User{})
    if err != nil {
        log.Fatal(err)
    }
}

func main() {

    db := func() *gorm.DB {
        db, err := connectToPostgreSQL()
        if err != nil {
            log.Fatal(err)
        }
        return db
    }()

    autoMigrateDB(db)

    //CRUD operations

    func() {
        newUser := createuserWithMetaData(db, "Toni", "toni@gmail.com", `{"key": "value", "days":[{"dayOne": "1"}], "user-id": "1"}`)
        log.Println("Created user:", newUser)
    }()

    func() {
        newUser := createuserWithMetaData(db, "John", "john@gmail.com", `{"key": "value two", "days":[{"dayOne": "2"}], "user-id": "2"}`)
        log.Println("Created user:", newUser)
    }()

    func() {
        newUser := createuserWithMetaData(db, "Sam", "sam@gmail.com", `{"key": "value three", "days":[{"dayOne": "3"}], "user-id": "3"}`)
        log.Println("Created user:", newUser)
    }()

    // Query user by ID
    user, err := getUserByID(db, 2)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("User by ID:", user)

    var result User
    db.Where("meta_data->>'user-id' = ?", "2").First(&result)
    log.Println(result)
}

 

Updates

We can use byte type instead of pgtype libray which simplifies the code a bit.

type Users struct {
    ID       uint   `gorm:"primaryKey"`
    Username string `gorm:"unique"`
    MetaData []byte `gorm:"type:jsonb" json:"meta-data"`
}

 

func createuserWithMetaData(db *gorm.DB) bool {
    metaData := "{\"one\":\"1\"}"
    // Create a user
    newUser := Users{
        Username: "TEST 123",
        MetaData: []byte(metaData),
    }

    db.Create(&newUser)

    return true
}

 

Leave a Reply