• Home
  • Testimonials
  • Blog
  • Contact Us

At a Glance of a Key

Crafting Dreams into Ventures, Code into Excellence: Your Journey to Success

  • Home
  • Testimonials
  • Blog
  • Contact Us

Using GORM – Part 1: Introduction

2023-03-03 Development Tips & Tricks No Comments 6 minute read

In today’s software development landscape, Object-Relational Mapping (ORM) has become an essential technique for managing relational databases. ORM allows developers to work with databases using an object-oriented approach without having to write SQL queries. ORM tools provide a set of methods that abstract away the complexity of dealing with databases and make it easier to perform CRUD operations. When it comes to the Go programming language, ORM tools have gained popularity due to the language’s simplicity, efficiency, and emphasis on concurrency. While Go’s standard library doesn’t include an ORM tool, third-party libraries such as GORM, XORM, and QBS provide a high level of abstraction that allows developers to write idiomatic Go code without dealing with the low-level details of database interaction. In this blog post, we’ll explore some of the best practices and pitfalls of using GORM.

Note: For simplicity, I will write the examples in this post as unit tests, that can be copy-pasted into your IDE and executed individually.

Let’s start with the following type definitions:

1
2
3
4
5
6
7
8
9
10
11
12
13
type User struct {
    UUID      string `gorm:"primary_key"`
    FirstName string
    LastName  string
}

type Bill struct {
    UUID     string `gorm:"primary_key"`
    UserID   string
    User     *User
    Amount   int64
    DueDate  time.Time
}

Let’s start by creating our tables:

1
2
3
4
5
6
7
8
9
10
11
12
13
// See: https://gorm.io/docs/connecting_to_the_database.html
func connectToDB() (*gorm.DB, error) {
    dsn := "host=localhost user=gorm password=gorm dbname=test_db port=9920 sslmode=disable TimeZone=Etc/UTC"
    return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}

func TestCreateTables(t *testing.T) {
    db, err := connectToDB()
    require.NoError(t, err)

    err = db.Debug().Migrator().CreateTable(User{}, Bill{})
    require.NoError(t, err)
}

Looking at my Postgres database, I can see now two tables with the following definition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
test_db=# \d+ users
                                           Table "public.users"
   Column   | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description
------------+------+-----------+----------+---------+----------+-------------+--------------+-------------
 uuid       | text |           | not null |         | extended |             |              |
 first_name | text |           |          |         | extended |             |              |
 last_name  | text |           |          |         | extended |             |              |
Indexes:
    "users_pkey" PRIMARY KEY, btree (uuid)
Referenced by:
    TABLE "bills" CONSTRAINT "fk_bills_user" FOREIGN KEY (user_id) REFERENCES users(uuid)
Access method: heap

test_db=# \d+ bills
                                                    Table "public.bills"
  Column  |           Type           | Collation | Nullable | Default | Storage  | Compression | Stats target | Description
----------+--------------------------+-----------+----------+---------+----------+-------------+--------------+-------------
 uuid     | text                     |           | not null |         | extended |             |              |
 user_id  | text                     |           |          |         | extended |             |              |
 amount   | bigint                   |           |          |         | plain    |             |              |
 due_date | timestamp with time zone |           |          |         | plain    |             |              |
Indexes:
    "bills_pkey" PRIMARY KEY, btree (uuid)
Foreign-key constraints:
    "fk_bills_user" FOREIGN KEY (user_id) REFERENCES users(uuid)
Access method: heap

As you can see, GORM is smart enough to create a foreign-key constraint and link the user_id from bills to the uuid in the users’ table. You can use different tags to customize the table creation, add indexes and other interesting things, but I won’t touch on them in this blog post.

Another interesting thing to note is that you can always add Debug() (as I had in my example) to your expression, and GORM will dump the actual SQL query that it generated. In our example:

1
2
[12.888ms] [ROWS:0] CREATE TABLE "users" ("uuid" text,"first_name" text,"last_name" text,PRIMARY KEY ("uuid"))
[4.935ms] [ROWS:0] CREATE TABLE "bills" ("uuid" text,"user_id" text,"amount" BIGINT,"due_date" timestamptz,PRIMARY KEY ("uuid"),CONSTRAINT "fk_bills_user" FOREIGN KEY ("user_id") REFERENCES "users"("uuid"))

Now, that I have the tables ready, I can start creating records. Having to set the uuid manually each time I want to create an item is an annoyance, and as it’s not part of my business logic, I don’t really want to deal with it each time πŸ™‚
For that, GORM allows you to use different hooks that can simplify your life. In our example, we can use the BeforeCreate hook to assign a UUID to the records:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.UUID == "" {
        u.UUID = generateUUID()
    }
    return nil
}

func (b *Bill) BeforeCreate(tx *gorm.DB) error {
    if b.UUID == "" {
        b.UUID = generateUUID()
    }
    return nil
}

func TestCreateItems(t *testing.T) {
    db, err := connectToDB()
    require.NoError(t, err)

    user := &User{FirstName: "Charlie", LastName: "Chaplin"}
    err = db.Create(user).Error
    require.NoError(t, err)

    bill := &Bill{UserID: user.UUID, Amount: 30, DueDate: time.Now().Add(3 * 24 * time.Hour)}
    err = db.Create(bill).Error
    require.NoError(t, err)
}

The generated SQL queries look like this:

1
2
[3.469ms] [ROWS:1] INSERT INTO "users" ("uuid","first_name","last_name") VALUES ('89c3d790-2712-4321-ae07-7fa9b5b112d7','Charlie','Chaplin')
[2.868ms] [ROWS:1] INSERT INTO "bills" ("uuid","user_id","amount","due_date") VALUES ('a8d45bd5-fabe-4856-8f94-f95adb8faf21','89c3d790-2712-4321-ae07-7fa9b5b112d7',30,'2023-03-07 01:13:43.243')

Now, we can also read it from the database, using different criteria.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestReadItems(t *testing.T) {
    db, err := connectToDB()
    require.NoError(t, err)

    var user *User
    err = db.Where(User{FirstName: "Charlie"}).First(&user).Error
    require.NoError(t, err)
    require.NotNil(t, user)

    var bills []*Bill
    err = db.Where(Bill{UserID: user.UUID}).Find(&bills).Error
    require.NoError(t, err)
    assert.Len(t, bills, 1)
}

We got the user record and the bills for that user (in this case, just one). The generated SQL queries are:

1
2
[3.645ms] [ROWS:1] SELECT * FROM "users" WHERE "users"."first_name" = 'Charlie' ORDER BY "users"."uuid" LIMIT 1
[1.256ms] [ROWS:1] SELECT * FROM "bills" WHERE "bills"."user_id" = '89c3d790-2712-4321-ae07-7fa9b5b112d7'

Unfortunately, GORM is not smart enough to remember that we have the user object and link it to the bill object, so bills[0].User will be nil. For situations like this, we can use Preload and specify the fields we are interested in:

1
2
3
4
5
6
7
8
9
10
11
12
func TestReadBillWithUserPreloaded(t *testing.T) {
    db, err := connectToDB()
    require.NoError(t, err)

    userID := "1916fc8f-fdb8-4d41-9de4-ec30dc6b9300"
    var bills []*Bill
    err = db.Preload("User").Where("user_id = ?", userID).Find(&bills).Error
    require.NoError(t, err)
    require.Len(t, bills, 1)
    require.NotNil(t, bills[0].User)
    assert.Equal(t, "Charlie", bills[0].User.FirstName)
}

This will generate the following SQL queries:

1
2
[1.462ms] [ROWS:1] SELECT * FROM "users" WHERE "users"."uuid" = '1916fc8f-fdb8-4d41-9de4-ec30dc6b9300'
[3.998ms] [ROWS:1] SELECT * FROM "bills" WHERE user_id = '1916fc8f-fdb8-4d41-9de4-ec30dc6b9300'

There are many types of queries you can run, including Select, Joins, Orders, and many more. You can find a full list in the official GORM documentation.

Now, let’s talk about updates. Let’s say I want to change the bill amount to 0 and the due date to today.

1
2
3
4
5
6
7
8
9
10
11
func TestUpdateBill(t *testing.T) {
    db, err := connectToDB()
    require.NoError(t, err)

    billID := "172e4a83-8fdd-452c-b4f2-7fc53c937e50"
    err = db.
        Model(Bill{UUID: billID}).
        Updates(Bill{Amount: 0, DueDate: time.Now()}).
        Error
    require.NoError(t, err)
}

This will generate the following SQL query:

1
[4.160ms] [ROWS:1] UPDATE "bills" SET "due_date"='2023-03-04 02:27:10.685' WHERE "uuid" = '172e4a83-8fdd-452c-b4f2-7fc53c937e50'

OOPS! What happened? Why I don’t have an update clause for the bill amount? The reason is that when passing a struct to GORM, it doesn’t know if you explicitly specified the “Amount” to be zero, or you haven’t specified it at all (zero is the default value for integers). So, GORM will just ignore it.
We can overcome this by making it more explicit:

1
2
3
4
5
6
7
8
9
10
11
12
func TestUpdateBill(t *testing.T) {
    db, err := connectToDB()
    require.NoError(t, err)

    billID := "172e4a83-8fdd-452c-b4f2-7fc53c937e50"
    err = db.
        Model(Bill{UUID: billID}).
        Updates(Bill{DueDate: time.Now()}).
        UpdateColumn("amount", 0).
        Error
    require.NoError(t, err)
}

Now we (almost) got what we wanted:

1
2
[4.839ms] [ROWS:1] UPDATE "bills" SET "due_date"='2023-03-04 02:29:50.066' WHERE "uuid" = '172e4a83-8fdd-452c-b4f2-7fc53c937e50'
[3.154ms] [ROWS:1] UPDATE "bills" SET "amount"=0 WHERE "uuid" = '172e4a83-8fdd-452c-b4f2-7fc53c937e50' AND "uuid" = '172e4a83-8fdd-452c-b4f2-7fc53c937e50'

Apparently, GORM doesn’t handle well this kind of mixture, so we should do something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestUpdateBill(t *testing.T) {
    db, err := connectToDB()
    require.NoError(t, err)

    billID := "172e4a83-8fdd-452c-b4f2-7fc53c937e50"
    err = db.
        Model(Bill{UUID: billID}).
        UpdateColumns(map[string]interface{}{
            "amount":   0,
            "due_date": time.Now(),
        }).
        Error
    require.NoError(t, err)
}

Ok. Now we got what we want:

1
[3.927ms] [ROWS:1] UPDATE "bills" SET "amount"=0,"due_date"='2023-03-04 02:31:57.576' WHERE "uuid" = '172e4a83-8fdd-452c-b4f2-7fc53c937e50'

Getting familiar with GORM and its different nuances is really important, especially when dealing with complex objects.
In the next part, we will see some of the painful pitfalls of GORM which look innocent at first sight but can do real damage to your data.

– Alexander

Oh hi there πŸ‘‹
It’s nice to meet you.

Sign up to receive a notification when new posts are published!

We don’t spam!

Check your inbox or spam folder to confirm your subscription.

Golang

Monolith or Microservices?

Using GORM - Part 2: Transactions and Save Points

Leave a Reply Cancel reply

About Me

Principal Software Engineer and an industry leader with startup and FAANG experience. I specialize in distributed systems, storage, data protection services and payment processors.

Beyond technical expertise, I am passionate about supporting fellow engineers in their careers. Through approachable blogs and hands-on guidance, I help navigate the ever-evolving landscape of technology, empowering individuals to thrive in their professional journeys.

Open LinkedIn

Recent Posts

  • Building a Delayed Message System with Redis and FastAPI
  • Go Concurrency, Practical Example
  • Using GORM – Part 3: Models and Idempotency
  • Using GORM – Part 2: Transactions and Save Points
  • Using GORM – Part 1: Introduction

Archives

  • January 2025
  • December 2024
  • March 2023
  • February 2023
  • September 2022
  • July 2022
  • July 2021
  • June 2021
  • February 2021
  • April 2018
  • March 2018
  • January 2018
  • July 2017
  • June 2017
  • May 2017

Categories

  • AWS
  • Career Growth
  • Cyber Security
  • Debugging
  • Development
  • Storage
  • Tips & Tricks

Tags

API AWS Azure Bash Brainfuck C++ Challenge Cloud Cloud Bursting Concurrency Database DevOps Disassembly DLL Documentation DynamoDB Go Golang Guice Java Jenkins Mossad NoSQL OOP Performance Programming Python Redis Security Serverless Singleton Streams Testing Unit Tests WebService

All Rights Reserved 2025 Β© Sirotin Enterprises Inc.
Proudly powered by WordPress | Theme: Doo by ThemeVS.