#database #development #golang #mysql

I recently started updating some projects from GORM v1 to v2.

While doing so, I found a nasty difference in behaviour which made this much more complex than anticipated.

One of the types of constructs which I use very often is:

 1func FindUser(id int64) (*User, error) {
 2
 3    var user User
 4
 5    err := db.Raw(`select * from user where id=?`, id).Scan(&obj).Error
 6    if err != nil {
 7        if errors.Is(err, gorm.ErrRecordNotFound) {
 8            return nil, nil
 9        }
10        return nil, err
11    }
12
13    return &user, nil
14
15}

So, what the code is supposed to be doing:

  • When id contains a valid value, a user object should be returned without an error
  • When id contains a non-existing value, nil should be returned as the user without an error
  • When something else goes wrong, nil should be returned as the user with an error

Turns out that this no longer works in version 2. In the version 1 documentation, it's defined as follows:

GORM provides a shortcut to handle RecordNotFound errors. If there are several errors, it will check if any of them is a RecordNotFound error.

In version 2 documentation, the definition changed to:

GORM returns ErrRecordNotFound when failed to find data with First, Last, Take, if there are several errors happened, you can check the ErrRecordNotFound error with errors.Is.

Luckily, I have test coverage to check this, but I'm unsure what would be the proper fix. One option would be to change the code to:

 1func FindUser(id int64) (*User, error) {
 2
 3    var user User
 4
 5    err := db.Raw(`select * from user where id=?`, id).Scan(&obj).Error
 6    if err != nil {
 7        return nil, err
 8    }
 9
10    if user.ID == 0 {
11        return nil, nil
12    }
13
14    return &user, nil
15
16}

There are several reasons though why I don't like this type of code. One of them is that queries don't always query for example the ID field which means you have to check different fields. This makes the code less clear for me and make it much more difficult what I'm trying to do.

The best alternative I have found until now is:

 1func FindUser(id int64) (*User, error) {
 2
 3    var user User
 4
 5    res := db.Raw(`select * from user where id=?`, id).Scan(&obj)
 6    if res.Error != nil {
 7        return nil, res.Error
 8    }
 9
10    if res.RowsAffected <= 0 {
11        return nil, nil
12    }
13
14    return &user, nil
15
16}

Since this is spread over several places, I reported it on the issue tracker hoping that they reconsider this change.