db icon indicating copy to clipboard operation
db copied to clipboard

How do I share a transaction across different structs / methods?

Open fr3fou opened this issue 5 years ago • 0 comments

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.

fr3fou avatar Jan 16 '20 16:01 fr3fou