DEV Community

loading...

Idiomatic Go and the hardship of running two methods in the same DB transaction

rhymes profile image rhymes ・2 min read

In the effort of learning Go a bit better, I am trying to refactor a series of functions which accept a DB connection as the first argument into struct methods and something a bit more "idiomatically" Go.

Right now my "data store" methods are something like this:

func CreateA(db orm.DB, a *A) error {
    db.Exec("INSERT...")
}

func CreateB(db orm.DB, b *B) error {
    db.Exec("INSERT...")
}
Enter fullscreen mode Exit fullscreen mode

These the functions work perfectly fine. orm.DB is the DB interface of go-pg.

Since the two functions accept a db connection I can either pass an actual connection or a transaction (which implements the same interface). I can be sure that both functions issuing SQL INSERTs run in the same transaction, avoiding having inconsistent state in the DB in case either one of them fails.

The trouble started when I decided to read more about how to structure the code a little better and to make it "mockable" in case I need to.

So I googled a bit, read the article Practical Persistence in Go: Organising Database Access and tried to refactor the code to use proper interfaces.

The result is something like this:

type Store {
    CreateA(a *A) error
    CreateB(a *A) error
}

type DB struct {
    orm.DB
}

func NewDBConnection(p *ConnParams) (*DB, error) {
    .... create db connection ...
    return &DB{db}, nil
}

func (db *DB) CreateA(a *A) error {
...
}

func (db *DB) CreateB(b *B) error {
...
}
Enter fullscreen mode Exit fullscreen mode

which allows me to write code like:

db := NewDBConnection()
DB.CreateA(a)
DB.CreateB(b)
Enter fullscreen mode Exit fullscreen mode

instead of:

db := NewDBConnection()
CreateA(db, a)
CreateB(db, b)
Enter fullscreen mode Exit fullscreen mode

The actual issue is that I lost the ability to run the two functions in the same transaction. Before I could do:

pgDB := DB.DB.(*pg.DB) // convert the interface to an actual connection
pgDB.RunInTransaction(func(tx *pg.Tx) error {
    CreateA(tx, a)
    CreateB(tx, b)
})
Enter fullscreen mode Exit fullscreen mode

or something like:

tx := db.DB.Begin()

err = CreateA(tx, a)
err = CreateB(tx, b)

if err != nil {
  tx.Rollback()
} else {
  tx.Commit()
}
Enter fullscreen mode Exit fullscreen mode

which is more or less the same thing.

Since the functions were accepting the common interface between a connection and a transaction I could abstract from my model layer the transaction logic sending down either a full connection or a transaction. This allowed me to decide in the "HTTP handler" when to create a trasaction and when I didn't need to.

Keep in mind that the connection is a global object representing a pool of connections handled automatically by go, so the hack I tried:

pgDB := DB.DB.(*pg.DB) // convert the interface to an actual connection
err = pgDB.RunInTransaction(func(tx *pg.Tx) error {
    DB.DB = tx // replace the connection with a transaction
    DB.CreateA(a)
    DB.CreateB(a)
})
Enter fullscreen mode Exit fullscreen mode

it's clearly a bad idea, because although it works, it works only once because we replace the global connection with a transaction. The following request breaks the server.

I broke my server trying to improve the code quality basically :-D

Any ideas? I can't find information about this around, probably because I don't know the right keywords :-D

Discussion (2)

pic
Editor guide
Collapse
rhymes profile image
rhymes Author

I solved this thanks to someone on StackOverflow: stackoverflow.com/a/49593544/4186181

I'm not in love with solution, and it's weird there's not much information on this but I guess it's the cleanest one.

Collapse
qm3ster profile image
Mihail Malo

This solutions makes quite a bit of sense.
Reminds me of Firestore's js library:

const transaction = db.runTransaction(async t => {
  const doc = await t.get(cityRef)
  var newPopulation = doc.data().population + 1
  await t.update(cityRef, { population: newPopulation })
})

It's the same substitution of global db inside a callback to the transaction method on our specific db struct.

const whatAmI = async db => {
  const doc = await db.get(cityRef)
  var newPopulation = doc.data().population + 1
  await db.update(cityRef, { population: newPopulation })
}

const notATransaction = whatAmI(db)

const aTransaction = db.runTransaction(whatAmI)