Dealing with database access in transformer stacks

124 Views Asked by At

This question is about groundhog or persistent, because I believe both share the same problem.

Say I have a transformer Tr m a that provides some functionality f :: Int -> Tr m (). This functionality requires database access. There are a few options I can use here, and none are satisfactory.

I could put a DbPersist transformer somewhere inside of Tr. Actually, I'd need to put it at the top because there are no PersistBackend instances for standard transformers AND I'd still need to write an instance for my Tr newtype. This already sucks because the class is far from minimal. I could also lift every db action I do.

The other option is changing the signature of f to PersistBackend m => Int -> Tr m (). This would again either require a PersistBackend instance on my Tr newtype, or lifting.

Now here's the real issue. How do I run Tr inside of a context that already has a PersistBackend constraint? There's no way to share it with Tr.

I can either do the first option and run the actual DbPersist transformer inside of Tr with some new connection pool (as far as I can tell there's no way to get the pool from the PersistBackend context I'm already in), or I can do the second option and have the run function be runTr :: PersistBackend m => Tr m a -> m a. The second option would actually be completely fine, but the problem here is that the DbPersist, that will eventually have to be somewhere in the stack, is now under the Tr transformer and there are no PersistBackend instances for the standard transformers of which Tr is made of.

What's the correct approach here? At the moment is seems that the best option is to go with a sepatare ReaderT somewhere in the stack that provides me with the connection pool on request and then do runDbConn with that pool everywhere where I want to access the DB. Seeing how DbPersist basically already is just a ReaderT I don't see the sense in having to do that.

1

There are 1 best solutions below

1
On BEST ANSWER

groundhog

I recommend using the latest groundhog from their master branch. Even though the change I'm about to describe appears to have been implemented in Sept. 2015, no release has made it to Hackage. But the authors seemed to have tackled this very problem.

On tip, PersistBackend is now a much simpler class to implement, much reduced from the dozens-of-methods-long behemoth it once was:

class (Monad m, Applicative m, Functor m, MonadIO m, ConnectionManager (Conn m), PersistBackendConn (Conn m)) => PersistBackend m where
  type Conn m
  getConnection :: m (Conn m)

instance (Monad m, Applicative m, Functor m, MonadIO m, PersistBackendConn conn) => PersistBackend (ReaderT conn m) where
  type Conn (ReaderT conn m) = conn
  getConnection = ask

They wrote an instance for ReaderT conn m (DbPersist has been deprecated and aliased to ReaderT conn), and you could as easily write one for Tr (ReaderT conn) if you choose to go the route of putting ReaderT inside rather than outside. It's not quite an mtl monad transformer since you would have to instance Tr m instead of Tr, but this and the associated data type trick they're using should allow you to use a custom monad stack without too much fuss.

Either option you choose will probably require some lifting. In my personal opinion I would stick ReaderT conn on the very outside of the stack. That way, the mtl helpers can still lift through most of your stack and you can glue on an additional lift to take it home. And, if you were to stick with the version on Hackage, this seems to be the only reasonable option since otherwise you would have the (old) monolithic PersistBackend class.

persistent

Persistent is a little more straightforward: as long as the monad transformer stack contains ReaderT SqlBackend and terminates in IO, you can lift a call to runSqlPool :: MonadBaseControlIO m => ReaderT SqlBackend m a -> Pool SqlBackend -> m a. All Persistent operations are defined to return something of type ReaderT backend m a, so the design sort of just works out.