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...")
}

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 {
...
}

which allows me to write code like:

db := NewDBConnection()
DB.CreateA(a)
DB.CreateB(b)

instead of:

db := NewDBConnection()
CreateA(db, a)
CreateB(db, b)

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)
})

or something like:

tx := db.DB.Begin()

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

if err != nil {
  tx.Rollback()
} else {
  tx.Commit()
}

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)
})

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

Posted on Mar 31 '18 by:

rhymes profile

rhymes

@rhymes

Software developer @ DEV

Discussion

markdown guide
 

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.

 

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)