Reputation: 188
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
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