GORM Models: The case of Misplaced Identities

Aditya Bhardwaj
3 min readMay 17, 2020

--

How GORM Models are reloaded with the default-value fields during CRUD operations.

Introduction

The post follows the journey of an app data inconsistency bug due to GORM tags. We’ll also see how insertion of records with default values by the DB work in GORM.

Setting the context — there’s a Users table which contains user_id and name columns. The user_id is an auto-generated UUID by default.

CREATE TABLE users ( 
user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name STRING NULL,
)

First Information Report

The issue was first reported by a colleague. He inserted ‘Prem’ in users table and received the UserID for the inserted record. When he tried to fetch the record with the UserID, he received ‘Amar’. A case of misplaced identities.

Investigation

We started with our code. This is how we were creating a new user. As simple as it can be.

user := &User{Name: "Prem"}
dbResponse := gorm.db.Create(user)

Our GORM Model for users table

type User struct {
UserID UUID `gorm:"type:uuid;default:gen_random_uuid()"`
Name string
}

We cross checked the UserIDs from the database. The UserID in dbResponse.Value and the user_id in the database for user ‘Prem’ were non-identical.

Further, when we created more users, all of them returned the UserID of ‘Amar’ while all rows in DB were created with different unique IDs. This meant DB was working fine but something was up the GORM Model.

To debug the issue, I set up a new database on my local machine, it ran like clockwork — no issues. Inserting ‘Prem’ gave same auto-generated UserID as there was in the DB for ‘Prem’.

Deep Dive

We started by turning on the GORM debug logs on the application connected to the affected DB, which meant we could see the queries being executed.

We noticed that immediately after an INSERT statement, there was a follow up SELECT statement.

[2020-05-15 23:54:04] [1.43ms] INSERT INTO “users” (“name”) VALUES (‘Amar’) RETURNING “users”.*
[2020–05–15 23:54:04] [1.02ms] SELECT “user_id” FROM “users”

On digging deeper in the code, we found the following function in GORM library which reloads the columns that having default value, and set it back to current object:

forceReloadAfterCreateCallback function in GORM Library

This meant the UserID being returned is being fetched through the query built using this function. Then we notice the following if statement.

if field.IsPrimaryKey && !field.IsBlank {
db = db.Where(
fmt.Sprintf("%v = ?", field.DBName), field.Field.Interface()
)
}

It was a like a bulb turning on and all the dots connected.

Charges

forceReloadAfterCreateCallback was executing a SELECT statement without a WHERE clause of Primary Key. It was fetching all the rows which were sorted lexicographically by UserID and picking up the last row for reference.We had not set the primary key in the GORM Model.

It was working fine till a new UserID generated was lexicographically bigger than the previous ones. When this chain broke i.e. the new unique random UserID generated is lexicographically smaller than the previous, it started returning an incorrect UserID.

Judgement

type User struct {
UserID UUID `gorm:"primary_key;type:uuid;default:gen_random_uuid()"`
Name string
}

We set the Primary Key tag in GORM Model. As expected, things worked like charm. You don’t need to set it if your primary key is called ‘ID’. However, if you rename the column or migrate, you might forget updating it.

Always set primary keys even if they’re composite, so that ‘Amar’ and ‘Prem’ never misplace their identities again :)

--

--