Adding Redis to Yesod
It’s been a long time since I’ve worked on the design of this blog. It’s honestly much prettier as a gist, if you’d rather read it there.
This is a quick run-through of how I connected to Redis from a Yesod site (which used the default scaffolding). There isn’t much specific to Redis here, so this information should apply to connecting to any database or service from Yesod.
Background: Basics of Hedis
First, a brief intro of the basics of Hedis:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Database.Redis as R
main :: IO ()
main = do
-- Establish a connection to Redis (actually creates a connection pool)
conn <- R.connect R.defaultConnectInfo
-- Using one of the connections, run commands against Redis.
result <- R.runRedis conn $ do
R.set "key" "value"
R.get "key"
print (result == Right (Just "value")) -- True
If you’re not doing Pub/Sub, that’s all there is to using Hedis. For more information, consult the Hedis Haddocks.
Installation
Add hedis to the cabal file, then install using either Stack or Cabal:
build-depends: base
, yesod
...
, hedis
You saw before that we needed to establish a connection to Redis before issuing commands to it. We’ll want to access that connection in our Handlers instead of making a new connection to Redis for each request. To do this, we’ll add the connection pool as a field on the App data type, which we can later access in Handlers using getYesod:
-- Foundation.hs
import qualified Database.Redis as R
data App = App
{ appSettings :: AppSettings
, appStatic :: Static
, appConnPool :: ConnectionPool
, appHttpManager :: Manager
, appLogger :: Logger
, appRedisPool :: R.Connection -- Our addition
}
When we instantiate our App in the makeFoundation function, we’ll need to add a Redis Connection as one of the fields:
-- Application.hs
import qualified Database.Redis as R
makeFoundation :: AppSettings -> IO App
makeFoundation appSettings = do
...
appRedisPool <- R.connect R.defaultConnectInfo
let mkFoundation appConnPool = App {..} -- RecordWildCards will fill out the appRedisPool field
Now we can access the App in our Handlers using getYesod, and from it the Redis connection pool:
import qualified Database.Redis as R
getHomeR :: Handler Html
getHomeR = do
app <- getYesod
let redisPool = appRedisPool app
liftIO $ R.runRedis redisPool $ do
_ <- R.set "key" "value"
R.get "key"
At this point you’re using Redis from Yesod. The rest of this guide will just improve on the code shown so far.
Make using Redis from a Handler more convenient:
Our current way of calling into Redis is a little tedious. Similar to functions like runDB, we’ll create a function to remove the boilerplate from calling into Redis:
-- I added this to a new module in Handler/Util.hs
handlerRunRedis :: R.Redis a -> Handler a
handlerRunRedis redisAction = do
redisPool <- appRedisPool <$> getYesod
liftIO $ R.runRedis redisPool redisAction
Usage:
handlerRunRedis $ do
_ <- R.set "hello" "hello"
_ <- R.set "world" "world"
hello <- R.get "hello"
world <- R.get "world"
liftIO $ print (hello,world)
Allow configuring Redis from a config file
If you look at makeFoundation, you can see it takes an AppSettings as a parameter to configure the App it returns. Hedis uses a ConnectInfo record for configuration, so we’ll add that as a field to AppSettings:
-- Settings.hs
import qualified Database.Redis as R
data AppSettings = AppSettings
{ appStaticDir :: String
-- ^ Directory from which to serve static files.
, appDatabaseConf :: PostgresConf
-- ^ Configuration settings for accessing the database.
, appRedisConf :: R.ConnectInfo
-- ...
}
The AppSettings record is parsed with a FromJSON instance defined in Settings.hs, so we’ll need a way to create a Hedis ConnectInfo from JSON as well. Typically you’d derive an instance, but we’d like to use the default values of ConnectInfo and just override them as necessary.
For this I used the hedis-config package, which provides a newtype wrapper around ConnectInfo called RedisConfig (the newtype wrapper is just used to avoid orphan instances). We’ll parse a RedisConfig from JSON and unwrap it into a ConnectInfo:
import qualified Database.Redis.Config as RC
instance FromJSON AppSettings where
parseJSON = withObject "AppSettings" $ \o -> do
-- ...
appRedisConf <- RC.getConnectInfo <$> (o .: "redis")
return AppSettings {..}
Now we’ll add a redis section to our config file:
redis:
max-connections: 100
Note that we’re writing
FromJSONinstances even though we’re parsing a YAML settings file. This is because theyamlpackage (Data.Yaml) parses the YAML into an aesonValue, which can then be parsed withFromJSONinstances. (This just lets us reuse all our existingFromJSONinstances for convenience).
Back in our makeFoundation function, we’ll just pass the ConnectInfo from the AppSettings when creating our connection pool:
makeFoundation :: AppSettings -> IO App
makeFoundation appSettings = do
...
appRedisPool <- R.connect (appRedisConf appSettings)
To confirm your changes took effect, you can print the configuration before using it:
import Debug.Trace
let redisConf = appRedisConf appSettings
traceShowM $ "Redis Conf is: " ++ show redisConf
appRedisPool <- R.connect redisConf
Conclusion
Connecting to Redis from Yesod is pretty simple—the core of it is:
- Add a connection pool field to your
Appdata type - Create the connection pool in
makeFoundation - Access the connection pool from
Handlers usinggetYesod
Even if you don’t end up using Redis, this pattern applies to connecting to other services as well, so it should be useful regardless. Enjoy using Redis!