Repositories
Introduction
Repositories are part of the Data layer. They implement the methods that will be called to work with the data (fetch, create, update, delete, etc) and will use models. Models are a Go representation of your database schema. An instance of a model is a single database record.
A repository should not have any dependency to a service, but can depend on other repositories. Therefore, it must interact only with the database: its purpose is to execute the necessary unitary database operation for data manipulation. Therefore, each repository method executes only one database operation. No business logic should be implemented in the repositories.
For example, if we have a system that tracks user actions (user history) and we want a "register" history entry to be created when the user creates their account, we would have two repositories that don't depend on each other:
- the
user.Createmethod should only create the user, not the associated history entry. - the
history.Createmethod should only create the history entry.
It is the services job to handle these kind of scenarios, using a transaction.
Models
Models are defined in the database/model package. Each resource has its own file. Models are usually just normal Golang structs, basic Go types, or pointers of them. sql.Scanner and driver.Valuer interfaces are also supported.
Example:
// database/model/user.go
package model
import (
"time"
"gopkg.in/guregu/null.v4"
"gorm.io/gorm"
)
type User struct {
ID int64 `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt null.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Email string `gorm:"uniqueIndex"`
}
func (User) TableName() string {
return "users"
}INFO
It is recommended to always define a TableName() method.
TIP
Learn more about model definition on Gorm's documentation.
Implementation
Each repository should have its own file in database/repository, named after the resource it is using, in singular form.
Full example:
// database/repository/user.go
package repository
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"goyave.dev/goyave/v5/database"
"goyave.dev/goyave/v5/util/errors"
"goyave.dev/template/database/model"
)
// User repository for user manipulation in the database.
type User struct {
DB *gorm.DB
}
// NewUser create a new user repository.
func NewUser(db *gorm.DB) *User {
return &User{
DB: db,
}
}
// Paginate returns a paginator after executing it.
func (r *User) Paginate(ctx context.Context, page int, pageSize int) (*database.Paginator[*model.User], error) {
users := []*model.User{}
paginator := database.NewPaginator(r.DB, page, pageSize, &users)
err := paginator.Find()
return paginator, err
}
// GetByID returns the user identified by the given ID, or `nil`.
// If not found, returns `gorm.ErrRecordNotFound`.
func (r *User) GetByID(ctx context.Context, id int64) (*model.User, error) {
var user *model.User
db := r.DB.Where("id", id).First(&user)
return user, errors.New(db.Error)
}
func (r *User) Create(ctx context.Context, user *model.User) (*model.User, error) {
db := r.DB.Omit(clause.Associations).Create(&user)
return user, errors.New(db.Error)
}Naming conventions
- Use
Get,GetByID,GetBy...for operations returning a single record. If the record is not found, these methods should return an error, usuallygorm.ErrRecordNotFound. - Use
Find,FindByID,FindBy...for operations returning zero or multiple records. If no record is found, these methods should return an empty slice and no error.- You can use
PaginateorIndexfor methods returning all records or records from a generic search, for example using the filter library.
- You can use
- For write methods, use descriptive actions such as
Create,Update,Delete. - Any other method is generally for specific use-cases. These methods' name should evoke the use-case. For example:
func (r *User) FindInactive(ctx context.Context, inactivityPeriod time.Duration) ([]*model.User, error)