QuinnF
QuinnF

Reputation: 2209

GORM create record that might already exist

I'm using gorm with postgres in my Go app.

I want to create a new user in the database, but there is a good chance that that user will already exist. If so, I want to not do anything with the database, but I want know about it so I can tell the user.

The good news is, that's already what gorm.Create(..) does. Trying to create a record with a duplicate unique key will return an error. There are two problems:

  1. I want better error messages. I want to write custom user-facing error messages that are different for "This email address already exists" than "There was an actual internal error." I don't know how to diferentiate between these two events other than trying to parse the error string returned by Create() which seems bug prone.
  2. I don't want to clutter my logs. Calling Create() with an object that already exists logs an error message to stdout. I don't really consider this an "error" since I was expecting it to happen and I don't want to flood my logs with a bunch of these warnings.

I know I could use a transaction to first check for a user with the given id and then create them if one does not already exist, but it seems like there should be a simpler solution to such a basic thing. How are you supposed to do this?


I'm currently doing this:

func (self databaseWrapper) CreateUser(user *User) error {
    db := self.db
    db.NewRecord(*user)
    err := db.Create(user).Error
    if err != nil {
        if db.Where(user.ID).Take(&User{}).Error == nil {
            return fmt.Errorf("A user already exists with id %v", user.ID)
        }

        if db.Where(User{Email: user.Email}).Take(&User{}).Error == nil {
            return fmt.Errorf("A user already exists with the given email address: %v", user.Email)
        }

        return fmt.Errorf("Error creating user")
    }
    return nil
}

Which is a little inefficient and gives the ugly output:

go test

(/home/quinn/workspace/aev/sensor/backend/server/database.go:125)
[2019-09-01 14:45:40]  pq: duplicate key value violates unique constraint "users_pkey"

(/home/quinn/workspace/aev/sensor/backend/server/database.go:125)
[2019-09-01 14:45:40]  pq: duplicate key value violates unique constraint "uix_users_email"
PASS
ok          3.215s

even when everything worked as expected.

Upvotes: 11

Views: 11224

Answers (3)

tim-montague
tim-montague

Reputation: 17412

In Gorm v1.21, I believe you could...

Configure the logger

import (
  gormLogger "gorm.io/gorm/logger"
)

func main() {
  db, err := gorm.Open(
    sqlite.Open("test.db"),
    &gorm.Config{
      Logger: gormLogger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags),
        gormLogger.Config {
          LogLevel: gormLogger.Silent,
          // IgnoreRecordNotFoundError: true,
        },
      ),
    },
  )
}

Catch and print your own error messages

result := db.Where("email = ?", "[email protected]").Take(&user)

if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
  fmt.Println("record already exists")
  // Or use a logger
  // logger.Info("record already exists")
}

Upvotes: 2

JonyD
JonyD

Reputation: 1359

To add to the previous answer, currently you can also use something like this to check for error codes (in the example: checking for duplicate keys).

import "github.com/jackc/pgx"
...
func isDuplicateKeyError(err error) bool {
    pgErr, ok := err.(pgx.PgError)
    if ok {
        // unique_violation = 23505
        return pgErr.Code == "23505"

    }
    return false
}

Upvotes: 5

Jessie
Jessie

Reputation: 2495

lib/pq is the standard postgres driver. If there is a query error, it will return a pq.Error object (even if you're using GORM). The pq.Error type has a Code field you can inspect to see the cause of the error.

if err, ok := err.(*pq.Error); ok && err.Code.Name() == "unique_violation" {
    // handle error
}

Error code reference

lib/pq Go doc

Upvotes: 4

Related Questions