How do I share a transaction across different structs / methods?
I don't know if this is the correct place to ask this, but I'm clueless on how to achieve this.
I've got the following code
package app
type FooService interface {
Foo(id uint) *Foo
UpdateFoo(id uint, *Foo) error
BusinessOperation(fooID uint, barID uint) error // -> this will have to call BarService.UpdateBar() somewhere in the implementation
}
type BarService interface {
Bar(id uint) *Bar
UpdateBar(id uint, *Bar) error
}
As you can see, I basically have 2 structs that contain db / business logic and one of them has to use the other one. The problem occurs in the actual implementation, as the structs would have to share the same database (not an issue for regular use, but for transactions it becomes an issue).
package mysql
type fooService struct {
db *sqlbuilder.Database
bs app.BarService // important
}
type barService struct {
db *sqlbuilder.Database
}
func (fs *fooService) BusinessOperation(fooID uint, barID uint) error {
tx, err := fs.db.NewTx(nil)
...
foo := fs.Foo(fooId) // can't use this as it uses the db field (not a part of the transaction)
foo := &Foo{}
tx.Collection("foo").Find("id", id).One(foo) // instead i have to copy paste code in order the use the transaction
...
fs.bs.Bar(barID) // can't use this as it uses the db field (not a part of the transaction)
bar := &Bar{}
tx.Collection("bar").Find("id", id).One(bar) // instead i have to copy paste code in order the use the transaction
}
The BusinessOperation method interally creates a Transaction to be used across all the database operations, but I've yet to find a way to actually share that transaction.
Suggested solutions I've seen were to create an interface that wraps both sql.DB and sql.Tx like that:
type executor interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
}
... and have different structs implement it so then you can pass it around as an argument to the aforementioned methods:
func (fs *fooService) BusinessOperation(db executor, fooID uint, barID uint) error {
...
fs.bs.Bar(db, barID) // this would work
}
The problem with this approach is that those methods on the interface wouldn't work for upper as the methods are quite a bunch more to implement.
What I came up with was to try and compose sqlbuilder.Database or sqlbuilder.Tx and add noop methods for my db or tx implemenation.
type DB interface {
sqlbulider.Database
Commit() error
Rollback() error
}
type db struct {
sqlbuilder.Database
}
// noop
func (d *db) Commit() error { return nil }
func (d *db) Rollback() error { return nil }
type tx struct {
sqlbuilder.Tx
}
... and the issue occurs right here. Tx has to implement all methods of sqlbuilder.Database, some of which I have no idea how to:
NewTx(ctx context.Context) (Tx, error)
Tx(ctx context.Context, fn func(sess Tx) error) error
Context() context.Context
WithContext(context.Context) Database
SetTxOptions(sql.TxOptions)
TxOptions() *sql.TxOptions
I have no idea how should those be implemented.
I tried doing the opposite and simply wrap sqlbulider.Tx, but the same issue occurs, except for the db struct:
Context() context.Context
WithContext(context.Context) Tx
SetTxOptions(sql.TxOptions)
TxOptions() *sql.TxOptions
I'd be very glad if I could get some help regarding this issue.