Cookies management by TermsFeed Cookie Consent

GORM repository

19/21

The last repository we want to create in this tutorial is a repository based on the ORM package GORM. Create a new file repository_postgresql_gorm.go in our domain package website and copy the contents of the repository there.

You will need the gorm.io/gorm package to make everything work, so add it to your project:

go get gorm.io/gorm

Let’s go through what our repository looks like.

Check out GORM great documentation if you want to learn more about how to use it.

The gormWebsite structure

GORM is based on models, i.e., structs that represent a database row and describe the table schema using struct tags. In our repository, we have defined the gormWebsite model, which describes the structure of the websites table. Look at how the struct tags define the table columns, and the TableName() method sets the table name. As you can probably guess, since the entire table is defined in code, we do not have to use SQL to perform operations on the database, even to create the table.

If you want to learn more about declaring models in GORM, check out the documentation.

The repository constructor

The repository structure PostgreSQLGORMRepository (and the constructor NewPostgreSQLGORMRepository) takes only one argument, the gorm.DB object that represents, as in the previous cases, a concurrent safe connection to the database.

Migrate() method

GORM has the AutoMigrate() method to perform automatic migration, which is, creating or modifying a table schema as defined in the model struct. Look at the line 34. We create a database object that respects the passed context.Context using the WithContext() function, and then call the AutoMigrate() method with the model object as an argument. It is enough to create websites table in the database with the columns defined in the model.

Create() method

To insert records into a DB table, we can use the GORM DB.Create() method, which takes a model object as an argument. Since we are operating on a domain object of type Website, we should remember to convert it to gormWebsite, which we do in lines 38-42. After successfully inserting the record, the GORM function DB.Create() also updates the value of the inserted object with the generated ID. So we just need to convert the inserted value back to Website (line 54) and return this object from our Create() method as an inserted Website record. In this method, as with previous repository implementations, we also handle the unique constraint violation error. Since the PostgreSQL driver for GORM internally uses the pgx package, all we need to do is check that we get an appropriate error of type pgconn.PgError.

All() method

To get all the records from the table, we just need to initialize an empty slice of gormWebsite objects for results and set it as an argument to GORM’s Find() method (line 61). Then, to be able to return a slice of type []Website, the result needs to be converted, which we do in lines 65-68.

GetByName() method

The GetByName() method is very similar to All() except that we use the GORM Where() function to add the SQL WHERE clause to our query, and we handle the gorm.ErrRecordNotFound error that is returned when there is no record with the given criteria. Note that the GORM requires ? instead of $1 as a placeholder in the Where() query. As with the All() method, we use the Find() function here to get the result, but this time with a single gormWebsite object as an argument. In the end, we convert the result to our Website domain object.

Update() method

If you have followed the previous methods, then Update() is nothing new for you. We use the GORM Save() function here to update the value of a gormWebsite object in the database. Finally, in addition to checking that we did not create a duplicate by updating, we also get the number of rows affected by this change. If it is equal to 0, we return the ErrUpdateFailed error, as in previous repository implementations.

Delete() method

To delete a record, we use the GORM Delete() function in a variant that removes a row with the specified primary key. If no row has been affected by the delete, we return the ErrDeleteFailed error.

website/repository_postgresql_gorm.go

  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
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package website

import (
    "context"
    "errors"

    "github.com/jackc/pgconn"
    "gorm.io/gorm"
)

type gormWebsite struct {
    ID   int64  `gorm:"primary_key"`
    Name string `gorm:"uniqueIndex;not null"`
    URL  string `gorm:"not null"`
    Rank int64  `gorm:"not null"`
}

func (gormWebsite) TableName() string {
    return "websites"
}

type PostgreSQLGORMRepository struct {
    db *gorm.DB
}

func NewPostgreSQLGORMRepository(db *gorm.DB) *PostgreSQLGORMRepository {
    return &PostgreSQLGORMRepository{
        db: db,
    }
}

func (r *PostgreSQLGORMRepository) Migrate(ctx context.Context) error {
    m := &gormWebsite{}
    return r.db.WithContext(ctx).AutoMigrate(&m)
}

func (r *PostgreSQLGORMRepository) Create(ctx context.Context, website Website) (*Website, error) {
    gormWebsite := gormWebsite{
        Name: website.Name,
        URL:  website.URL,
        Rank: website.Rank,
    }

    if err := r.db.WithContext(ctx).Create(&gormWebsite).Error; err != nil {
        var pgxError *pgconn.PgError
        if errors.As(err, &pgxError) {
            if pgxError.Code == "23505" {
                return nil, ErrDuplicate
            }
        }
        return nil, err
    }

    result := Website(gormWebsite)

    return &result, nil
}

func (r *PostgreSQLGORMRepository) All(ctx context.Context) ([]Website, error) {
    var gormWebsites []gormWebsite
    if err := r.db.WithContext(ctx).Find(&gormWebsites).Error; err != nil {
        return nil, err
    }

    var result []Website
    for _, gw := range gormWebsites {
        result = append(result, Website(gw))
    }
    return result, nil
}

func (r *PostgreSQLGORMRepository) GetByName(ctx context.Context, name string) (*Website, error) {
    var gormWebsite gormWebsite
    if err := r.db.WithContext(ctx).Where("name = ?", name).Find(&gormWebsite).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, ErrNotExist
        }
        return nil, err
    }
    website := Website(gormWebsite)
    return &website, nil
}

func (r *PostgreSQLGORMRepository) Update(ctx context.Context, id int64, updated Website) (*Website, error) {
    gormWebsite := Website(updated)
    updateRes := r.db.WithContext(ctx).Where("id = ?", id).Save(&gormWebsite)
    if err := updateRes.Error; err != nil {
        var pgxError *pgconn.PgError
        if errors.As(err, &pgxError) {
            if pgxError.Code == "23505" {
                return nil, ErrDuplicate
            }
        }
        return nil, err
    }

    rowsAffected := updateRes.RowsAffected
    if rowsAffected == 0 {
        return nil, ErrUpdateFailed
    }

    return &updated, nil
}

func (r *PostgreSQLGORMRepository) Delete(ctx context.Context, id int64) error {
    deleteRes := r.db.WithContext(ctx).Delete(&gormWebsite{}, id)
    if err := deleteRes.Error; err != nil {
        return err
    }

    if deleteRes.RowsAffected == 0 {
        return ErrDeleteFailed
    }

    return nil
}
19/21