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