Friday, February 23, 2018

factor withDB out (19)

The code so far has ended up with a six or so repeated blocks that look like:

liftIO $ bracket
  (PG.connectPostgreSQL "user='postgres'")
  PG.close
  \conn -> _something_to_do_with_the_database_

More than three times violates the Rule of Three so I'm going to pull this out into a new file, src/DB.hs.

{-# Language OverloadedStrings #-}

module DB where

import Control.Exception (bracket)
import Control.Monad.IO.Class (liftIO, MonadIO)
import qualified Database.PostgreSQL.Simple as PG

withDB :: MonadIO io => (PG.Connection -> IO b) -> io b
withDB act = liftIO $ bracket
  (PG.connectPostgreSQL "user='postgres'")
  PG.close
  act

Now every occurence of bracket happens to fit into the above pattern, so every place in the code you see this:

  [registration] <- liftIO $ bracket
    (PG.connectPostgreSQL "user='postgres'")
    PG.close
    $ \conn -> do
       PGS.gselectFrom conn "registration where nonce = ?" [identifier]

... replace it with this:

  [registration] <- withDB $ \conn -> do
       PGS.gselectFrom conn "registration where nonce = ?" [identifier]

... which is three lines shorter.

We can go a bit further - this occurs three times:

withDB $ \conn -> PGS.gselectFrom conn "registration where nonce = ?" [identifier]

so replace that with a new selectByNonce:

import qualified Database.PostgreSQL.Simple.SOP as PGS
selectByNonce :: MonadIO io => String -> io [Registration]
selectByNonce identifier = withDB $ \conn -> PGS.gselectFrom conn "registration where nonce = ?" [identifier]

... or go one step further and realise that a selectByNonce is always used where exactly one row is expected to match, like this:

  [registration] <- selectByNonce identifier

and so that pattern matching can move into selectByNonce too, with better error reporting. Now the function will return exactly one Registration or otherwise throw an error (rather than returning a list and the same error manifesting as a pattern match failure).

selectByNonce :: MonadIO io => String -> io Registration
selectByNonce identifier = do
  res <- withDB $ \conn -> PGS.gselectFrom conn "registration where nonce = ?" [identifier]
  case res of
    [r] -> return r
    [] -> error $ "selectByNonce: no rows returned for " ++ identifier
    _ -> error $ "selectByNonce: multiple rows returned for " ++ identifier

... with invocations looking like this:

registration <- selectByNonce identifier

Those are just a couple of small changes but they make database access look much tidier. Specifically if the code moved to some more elaborate database connection handling (for example, a persistent connection shared between requests), then withDB could be rewritten to deal with that but the higher level code would not need to change.

Here's the commit for this post.

No comments:

Post a Comment