add cookbook recipe for using a PostgreSQL connection pool
This commit is contained in:
parent
8543e00aaa
commit
e2314aa059
4 changed files with 174 additions and 0 deletions
|
@ -1,6 +1,7 @@
|
||||||
packages:
|
packages:
|
||||||
basic-auth/
|
basic-auth/
|
||||||
db-sqlite-simple/
|
db-sqlite-simple/
|
||||||
|
db-postgres-pool/
|
||||||
../../servant
|
../../servant
|
||||||
../../servant-server
|
../../servant-server
|
||||||
../../servant-client-core
|
../../servant-client-core
|
||||||
|
|
142
doc/cookbook/db-postgres-pool/PostgresPool.lhs
Normal file
142
doc/cookbook/db-postgres-pool/PostgresPool.lhs
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
# PostgreSQL connection pool
|
||||||
|
|
||||||
|
Let's see how we can write a simple web application that uses a
|
||||||
|
[PostgreSQL](https://www.postgresql.org/) database to store simple textual
|
||||||
|
messages, just like in the SQLite cookbook recipe. The main difference,
|
||||||
|
besides the database technology, is that in this example we will be using
|
||||||
|
a pool of connections to talk to the database server. The pool abstraction
|
||||||
|
will be provided by the
|
||||||
|
[resource-pool](https://hackage.haskell.org/package/resource-pool) library.
|
||||||
|
|
||||||
|
As usual, we start with a little bit of throat clearing.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
import Data.ByteString (ByteString)
|
||||||
|
import Control.Concurrent
|
||||||
|
import Control.Exception (bracket)
|
||||||
|
import Control.Monad.IO.Class
|
||||||
|
import Data.Pool
|
||||||
|
import Database.PostgreSQL.Simple
|
||||||
|
import Network.HTTP.Client (newManager, defaultManagerSettings)
|
||||||
|
import Network.Wai.Handler.Warp
|
||||||
|
import Servant
|
||||||
|
import Servant.Client
|
||||||
|
|
||||||
|
type DBConnectionString = ByteString
|
||||||
|
```
|
||||||
|
|
||||||
|
We will only care about a single type here, the messages. We want to
|
||||||
|
be able to add a new one and retrieve them all, using two different
|
||||||
|
endpoints.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
type Message = String
|
||||||
|
|
||||||
|
type API = ReqBody '[PlainText] Message :> Post '[JSON] NoContent
|
||||||
|
:<|> Get '[JSON] [Message]
|
||||||
|
|
||||||
|
api :: Proxy API
|
||||||
|
api = Proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
We proceed with a simple function for creating a table
|
||||||
|
for holding our messages if it doesn't already exist, given
|
||||||
|
a PostgreSQL connection string.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
initDB :: DBConnectionString -> IO ()
|
||||||
|
initDB connstr = bracket (connectPostgreSQL connstr) close $ \conn -> do
|
||||||
|
execute_ conn
|
||||||
|
"CREATE TABLE IF NOT EXISTS messages (msg text not null)"
|
||||||
|
return ()
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, our server implementation. It will be parametrised (take as
|
||||||
|
argument) by the pool of database connections that handlers can use to
|
||||||
|
talk to the PostgreSQL database. The resource pool abstraction allows us
|
||||||
|
to flexibly set up a whole bunch of PostgreSQL connections tailored to our
|
||||||
|
needs and then to forget about it all by simply asking for a connection
|
||||||
|
using `withResource`.
|
||||||
|
|
||||||
|
The handlers are straighforward. One takes care of inserting a new
|
||||||
|
value in the database while the other fetches all messages and returns
|
||||||
|
them. We also provide a function for serving our web app given a PostgreSQL
|
||||||
|
connection pool, which simply calls servant-server's `serve` function.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
server :: Pool Connection -> Server API
|
||||||
|
server conns = postMessage :<|> getMessages
|
||||||
|
|
||||||
|
where postMessage :: Message -> Handler NoContent
|
||||||
|
postMessage msg = do
|
||||||
|
liftIO . withResource conns $ \conn ->
|
||||||
|
execute conn
|
||||||
|
"INSERT INTO messages VALUES (?)"
|
||||||
|
(Only msg)
|
||||||
|
return NoContent
|
||||||
|
|
||||||
|
getMessages :: Handler [Message]
|
||||||
|
getMessages = fmap (map fromOnly) . liftIO $
|
||||||
|
withResource conns $ \conn ->
|
||||||
|
query_ conn "SELECT msg FROM messages"
|
||||||
|
|
||||||
|
runApp :: Pool Connection -> IO ()
|
||||||
|
runApp conns = run 8080 (serve api $ server conns)
|
||||||
|
```
|
||||||
|
|
||||||
|
We will also need a function for initialising our connection pool.
|
||||||
|
`resource-pool` is quite configurable, feel free to wander in
|
||||||
|
[its documentation](https://hackage.haskell.org/package/resource-pool)
|
||||||
|
to gain a better understanding of how it works and what the configuration
|
||||||
|
knobs are. I will be using some dummy values in this example.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
initConnectionPool :: DBConnectionString -> IO (Pool Connection)
|
||||||
|
initConnectionPool connStr =
|
||||||
|
createPool (connectPostgreSQL connStr)
|
||||||
|
close
|
||||||
|
2 -- stripes
|
||||||
|
60 -- unused connections are kept open for a minute
|
||||||
|
10 -- max. 10 connections open per stripe
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's finally derive some clients for our API and use them to
|
||||||
|
insert two messages and retrieve them in `main`, after setting up
|
||||||
|
our pool of database connections.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
postMsg :: Message -> ClientM NoContent
|
||||||
|
getMsgs :: ClientM [Message]
|
||||||
|
postMsg :<|> getMsgs = client api
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
-- you could read this from some configuration file,
|
||||||
|
-- environment variable or somewhere else instead.
|
||||||
|
-- you will need to either change this connection string OR
|
||||||
|
-- set some environment variables (see
|
||||||
|
-- https://www.postgresql.org/docs/9.5/static/libpq-envars.html)
|
||||||
|
-- to point to a running PostgreSQL server for this example to work.
|
||||||
|
let connStr = ""
|
||||||
|
pool <- initConnectionPool connStr
|
||||||
|
initDB connStr
|
||||||
|
mgr <- newManager defaultManagerSettings
|
||||||
|
bracket (forkIO $ runApp pool) killThread $ \_ -> do
|
||||||
|
ms <- flip runClientM (ClientEnv mgr (BaseUrl Http "localhost" 8080 "")) $ do
|
||||||
|
postMsg "hello"
|
||||||
|
postMsg "world"
|
||||||
|
getMsgs
|
||||||
|
print ms
|
||||||
|
```
|
||||||
|
|
||||||
|
This program prints `Right ["hello","world"]` the first time it is executed,
|
||||||
|
`Right ["hello","world","hello","world"]` the second time and so on.
|
||||||
|
|
||||||
|
You could alternatively have the handlers live in `ReaderT (Pool Connection)`
|
||||||
|
and access the pool using e.g `ask`, but this would be more complicated
|
||||||
|
than simply taking the pool as argument.
|
||||||
|
|
||||||
|
The entire source for this example is available [here](...).
|
30
doc/cookbook/db-postgres-pool/db-postgres-pool.cabal
Normal file
30
doc/cookbook/db-postgres-pool/db-postgres-pool.cabal
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
name: cookbook-db-postgres-pool
|
||||||
|
version: 0.1
|
||||||
|
synopsis: Simple PostgreSQL connection pool cookbook example
|
||||||
|
homepage: http://haskell-servant.readthedocs.org/
|
||||||
|
license: BSD3
|
||||||
|
license-file: ../../../servant/LICENSE
|
||||||
|
author: Servant Contributors
|
||||||
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
|
|
||||||
|
executable cookbook-db-postgres-pool
|
||||||
|
main-is: PostgresPool.lhs
|
||||||
|
build-depends: base == 4.*
|
||||||
|
, bytestring
|
||||||
|
, text
|
||||||
|
, aeson
|
||||||
|
, servant
|
||||||
|
, servant-client
|
||||||
|
, servant-server
|
||||||
|
, warp
|
||||||
|
, wai
|
||||||
|
, http-types
|
||||||
|
, markdown-unlit >= 0.4
|
||||||
|
, http-client
|
||||||
|
, postgresql-simple
|
||||||
|
, resource-pool
|
||||||
|
default-language: Haskell2010
|
||||||
|
ghc-options: -Wall -pgmL markdown-unlit
|
||||||
|
build-tool-depends: markdown-unlit:markdown-unlit
|
|
@ -19,3 +19,4 @@ you name it!
|
||||||
|
|
||||||
basic-auth/BasicAuth.lhs
|
basic-auth/BasicAuth.lhs
|
||||||
db-sqlite-simple/DBConnection.lhs
|
db-sqlite-simple/DBConnection.lhs
|
||||||
|
db-postgres-pool/PostgresPool.lhs
|
||||||
|
|
Loading…
Reference in a new issue