Bruno Luiz
Bruno Luiz

Reputation: 188

How to properly pass database reference for transaction usage

Usually I split my web projects in domains and each domain has a service (business layers) and repository (data access layer). I am creating a project where I have two domains (Jobs and Headers).

When creating or updating an job, eventually, headers will be updated as well. The process is orchestrated by the job/service, which internally calls header/service. As multiple insert/updates happen, a transaction is used to control the process.

Usually, when creating a transaction in Go, a "Tx" instance is returned and should be used in the further queries, until committed. The only problem is that the DB is injected on the repository creation and it cannot be changed later on as multiple requests will be using the same repository through reference. What are the options in cases like this?

The only option I figured out is to pass the db as a parameter of repositories methods.

pkg/storage/database.go

package storage

import (
    "chronos/pkg/errors"
    "github.com/jinzhu/gorm"
)

// DB Database storage interface
type DB interface {
    Add(interface{}) error
    Delete(interface{}) error

    Begin() (DB, error)
    Rollback() (DB, error)
    Commit() (DB, error)
}


// Gorm Database implementation using GORM
type Gorm struct {
    *gorm.DB
    SQL *gorm.DB
}

// NewGorm Return new gorm storage instance
func NewGorm(db *gorm.DB) *Gorm {
    db = db.
        Set("gorm:association_autocreate", false).
        Set("gorm:association_autoupdate", false).
        Set("gorm:save_associations", false).
        Set("gorm:association_save_reference", false)

    return &Gorm{db, db}
}

// Begin Begin transaction
func (s *Gorm) Begin() (DB, error) {
    t := s.SQL.Begin()
    if err := t.Error; err != nil {
        return s, errors.Database(err)
    }

    return NewGorm(t), nil
}

cmd/app/main.go

db, err := gorm.Open("mysql", config)
st = storage.NewGorm(db)

rJob := job.NewMySQLRepository(log)
sJob := job.NewService(log, st, rJob)

job.Register(log, router, sJob) // register routes

pkg/jobs/interface.go: All methods receive a st.DB interface. Inside the function a type assert is done to get the storage.Gorm implementation

type Repository interface {
    GetByUser(db st.DB, userID int) ([]*model.Job, error)
    GetByID(db st.DB, userID int, id int) (*model.Job, error)
    AddHeaders(db st.DB, job *model.Job, headers []*model.Header) (err error)
    UpdateHeaders(db st.DB, job *model.Job, headers []*model.Header) (err error) // extract this to headers repository
}

Another option suggested in this article is to use a context (or maybe a struct?) down the methods, with the sql DB in it. But it doesn't feel right either. Some people told me that the repository and service patterns are not used in Golang, but for me is weird to have database and business logic mixed together.

What is the best option to pass the database reference through the project?

EDIT 1:

How it was organized before the above solution

cmd/app/main.go

db, err := gorm.Open("mysql", config)

rHeader := header.NewMySQLRepository(log, db)
sHeader := header.NewService(log, rJob)

rJob := job.NewMySQLRepository(log, db) // Inject db in the repository
sJob := job.NewService(log, rJob, sHeader) // Inject the repository here and other related services

job.Register(log, router, sJob) // register routes

pkg/jobs/interface.go:

type Repository interface {
    Add(user *model.Job) (error)
    Delete(user *model.Job) (error)
    GetByUser(userID int) ([]*model.Job, error)
    GetByID(userID int, id int) (*model.Job, error)
    AddHeaders(job *model.Job, headers []*model.Header) (err error)
    UpdateHeaders(job *model.Job, headers []*model.Header) (err error) // extract this to headers repository
}

Upvotes: 7

Views: 2616

Answers (1)

ZAky
ZAky

Reputation: 1307

I don't know about the Tx problem but I'll try to address the repository injection issue.

The users logic component should ask the caller component to implement the users repository interface in order to function properly.

The calle, main() for example should implement the repository and inject it to the users component, when the caller is Test(t *testing.T) a mock repository will be injected.

Anyway the users component is not aware of the repository interface implementation.

I hope the attached code explain the idea.

package main

func main() {
    //Create the repository
    userrep := &DbService{make(map[int]*Job), make(map[int]*Job)}
    //Injecting the repository
    users := Users{userrep}
    job := users.Do(2)
    _ = job

}

//mapdb.go imlement db as map
//Reimplement it for Gorm or anything that apply to the interface.
type DbService struct {
    dbid   map[int]*Job
    dbName map[int]*Job
}
type Job struct {
    name string
}

func (db *DbService) GetByUser(userID int) (*Job, error) {
    return db.dbName[userID], nil
}
func (db *DbService) GetByID(userID int, id int) (*Job, error) {
    return db.dbid[id], nil
}

//users.go

//this interface is part of the users logic.
//The logic component say by that to the caller,
//"If you give me something that implement this interface I will be able to function properly"
type UsersRep interface {
    GetByUser(userID int) (*Job, error) // I think its better to avoid the reference to model.Job here
    GetByID(userID int, id int) (*Job, error)
}

//usersLogic.go
type Users struct {
    UsersRep
}

func (users *Users) Do(id int) *Job {
    //do...
    j, _ := users.GetByUser(id)
    return j
}

Do the same for headers.

The Gorm repository can implement Users and Headers interfaces. In tests the mock will implement only one of them.

Upvotes: 1

Related Questions