Connecting to a database
Setting up the database
Now that we have set up a very primitive stateless server, the next step in any regular tutorial would be to store the state in memory and do some interactions with html. That way it looks like you’re getting results faster. But no! This is Haskell for madmen, not Python for fools!
We will use a database, which is most likely what you’ll want to be
doing in a production setting anyway. Docker lets us run a PostgreSQL
instance in a container. Create
<project root>/db/Dockerfile
with the following:
FROM postgres:latest
ENV POSTGRES_USER "Haskell-student"
ENV POSTGRES_PASSWORD "Why-are-you-putting-credentials-in-code?-You-absolute-potato!"
Obviously we wouldn’t usually put credentials in our code but we’ll do so for now. Going over the right ways to store credentials is boring and out of scope of this tutorial. Generally though, you should consider anything that ever touched the code base to have leaked.
We need to do a few things to use our database and connect to it.
First, we’ll need to create a new docker network. Note that you need to be root to call docker.
docker network create haskell-for-madmen-network
note
To remove the network again once you’re done with it, use
docker network rm haskell-for-madmen-network
Then, we’ll need to build the database image:
docker build -t "haskell-for-madmen-db" ./db
Finally, we run the image once to launch the database and once more to launch psql.
docker run --network haskell-for-madmen-network --name haskell-for-madmen-db-container -d haskell-for-madmen-db
docker run -it --rm --network haskell-for-madmen-network haskell-for-madmen-db psql -h haskell-for-madmen-db-container -U Haskell-student
Type in the password you set as environment variable
(Why-are-you-putting-credentials-in-your-code?-You-absolute-potato!
if you copied the code above verbatim), and you’re in!
Connecting from Haskell
Manually connecting to PostgreSQL is not what we wanted. So let’s go back to Hackage and find a PostgreSQL library. We’ll be using hasql, version 1.3.0.6 in my case. Unfortunately Hasql does not follow semver and 1.4 is not compatible with the code below.
You will need to install the postgres development library to use
hasql. How you do that will vary between operating system. En
Debian-based systems you will need the libpq-dev
package.
For our CI pipeline we will need to add the following to
.gitlab-ci.yml
:
- apt-get update
- apt-get install -y libpq-dev
You could build and run your entire project inside a docker container and add the library there, much like we’re doing in CI. I haven’t done so because this is not a docker tutorial and I would like to keep non-Haskell stuff to a minimum.
If you’re not going to run your Haskell code in a container, we’ll need to expose the port of our database, which requires stopping the container that’s already running.
docker stop haskell-for-madmen-db-container
docker rm haskell-for-madmen-db-container
docker run --network haskell-for-madmen-network --name haskell-for-madmen-db-container -p 5432:5432 -d haskell-for-madmen-db
If you already have an application listening on port 5432 you’ll need
to use a different input port in
-p <input port>:5432
.
You might want to stack build
right after adding the
dependency, as this one will take a while.
Finally back to Haskell, let’s look in the Connection
module and import it:
import qualified Hasql.Connection
We find the functions:
acquire :: Settings -> IO (Either ConnectionError Connection)
release :: Connection -> IO ()
settings :: ByteString -> Word16 -> ByteString -> ByteString -> ByteString -> Settings
So it looks like we’re going to have to declare settings before we
can make a connection. We know ByteString
already, but what
is a Word16
? The Hasql documentation tells us this should
be a port. Clicking through to the documentation of Word16
tells us it’s a 16-bit integer that’s an instances a bunch of classes.
If we search through the classes it implements, we will find
Num
with the interesting function
fromInteger :: Integer -> Word16
. We can use that!
note
You may have realized
fromInteger
would have a type signatureInteger -> a
in the class declaration. So how does Haskell know what type we actually want to get? In this case, it knowsHasql.Connection.settings
needs aWord16
, so it tries to solve the type constraints and manages to do so unambiguously. In some rare cases though, it might not be able to infer what type you actually want to use. For instance, we might writeshow $ fromInteger 42
, but the output offromInteger
and input ofshow
might be any instance of bothNum
andShow
! In such cases, we need to provide the type explicitly:show $ (fromInteger 42 :: Word16)
Now we know enough to put together our connection code (leaving the database field blank for now).
connectToDB :: IO ()
=
connectToDB let
connectionSettings :: Hasql.Connection.Settings
=
connectionSettings
Hasql.Connection.settings"localhost"
fromInteger 5432)
("Haskell-student"
"Why-are-you-putting-credentials-in-code?-You-absolute-potato!"
""
in do
<- Hasql.Connection.acquire connectionSettings
connectionResult case connectionResult of
Left (Just errMsg) -> error $ StrictUTF8.toString errMsg
Left Nothing -> error "Unspecified connection error"
Right connection -> do
putStrLn "Acquired connection!"
Hasql.Connection.release connection
Let’s also change someFunc
to call
connectToDB
before starting the server.
someFunc :: IO ()
= do
someFunc
connectToDB8080 requestHandler run
stack run
and you should see the message
Acquired connection!
.
Populating the DB
We will need to populate the database to query anything. Create an
init file db/docker-entrypoint-initdb.d/init.sql
with the
following content:
CREATE DATABASE todolists;
connect todolists
\
CREATE TABLE todolist_ch5 (
NOT NULL,
task TEXT BOOLEAN NOT NULL
done
);
INSERT INTO todolist_ch5 (
task,
done
)VALUES
'create todo list', TRUE ),
( 'put todo list in database', TRUE ),
( 'invent terror drones', FALSE ),
( 'achieve world domination', FALSE ); (
Usually we’d create proper indices, but this will do for our current requirements.
We’ll also need to add the init file in our
db/Dockerfile
:
COPY docker-entrypoint-initdb.d/ ./docker-entrypoint-initdb.d/
And stop, remove, rebuild and restart the database container:
docker stop haskell-for-madmen-db-container
docker rm haskell-for-madmen-db-container
docker build -t "haskell-for-madmen-db" ./db
docker run --network haskell-for-madmen-network --name haskell-for-madmen-db-container -p 5432:5432 -d haskell-for-madmen-db
The base image we’ve used for our dockerfile will run any scripts in
finds in ./docker-entrypoint-initdb.d/
at startup.
Feel free to use psql to check the content of the database if you wish.
Fetching data in Haskell
The inner workings of Hasql are a wee bit complicated if you’re not used to thinking with functors yet. You could try to figure it out yourself, but I expect this is still a bit too complicated if this tutorial is your first introduction to Haskell’s type classes. So here’s what we can work with:
A Statement
in Hasql.Statement
is a sort-of
operation we can perform on the database. Statement a b
can
be used to fetch data of type b
given data of type
a
.
Using Hasql.Session.statement
we can create a
Hasql.Session.Session
from a Statement
and
input parameters for said statement. We can then use
Hasql.Session.run
with a Connection
, which we
already got in the code above, to get an IO
action.
Think of it like this: a Statement
tells us how to make
a query, a Session
is a query, and using a
Connection
we can run
the query, which is an
IO
action.
We will need a few imports:
import qualified Hasql.Session
import qualified Hasql.Statement
import qualified Hasql.Encoders
import qualified Hasql.Decoders
Let’s start at the bottom and make a Statement
that
takes no arguments and returns out todo list of type
[Task]
. Remember that there’s no such thing in Haskell as
no arguments, we use ()
(unit) instead.
selectTasksStatement :: Hasql.Statement.Statement () Task
The first argument to the Statement
constructor is an
SQL string, with input parameters replaced by
$<number>
. The second is an encoder. The encoder will
turn out custom types into types that Hasql knows how to place in
tables. Then we’ll need to pass an encoder, which does the opposite of
an encoder. Finally we need to pass whether the statement should be
prepared.
We’ll find encoder in Hasql.Encoders
. We don’t have any
parameters, so Hasql.Encoders.unit
works for us.
Let’s postpone the creation of a decoder for a minute, and create our statement and session:
selectTasksSession :: Hasql.Session.Session [Task]
= Hasql.Session.statement () selectTasksStatement
selectTasksSession
selectTasksStatement :: Hasql.Statement.Statement () [Task]
=
selectTasksStatement Hasql.Statement.Statement
"SELECT * FROM todolist_ch5"
Hasql.Encoders.unit
tasksDecoderTrue
tasksDecoder :: Hasql.Decoders.Result [Task]
= error "Didn't actually implement decoder yet" tasksDecoder
With our mental cache cleared, let’s focus entirely on the decoder. A
Result
decoder converters the entire result of an SQL query
to a certain type. It is defined in terms of Row
decoders.
A Row
decoder on the other hand will convert a single row.
It is defined with Column
decoders. Finally, a
Column
is a Value
decoder with known
nullability.
All our decoders are Functor
s (check the docs under
“Instances”). This means that if we have a value of type
decoder a
and a function a -> b
we can get
a decoder b
by using fmap
. That’s useful for
turning the BOOLEAN
in SQL to a TaskStatus
in
our program:
boolToTaskStatus :: Bool -> TaskStatus
True = Done
boolToTaskStatus False = NotDone
boolToTaskStatus
taskStatusDecoder :: Hasql.Decoders.Value TaskStatus
= fmap boolToTaskStatus Hasql.Decoders.bool taskStatusDecoder
Similarly, Hasql has a Value Text
encoder but not a
Value String
. The implementation of sting
String
is now widely considered to have been a mistake, but
it’s still in the default prelude and many libraries use it,
Text
is an improvement upon String
. To convert
Text
to String
, we’ll need the
text
package. Import Data.Text
and use
unpack
, you should know the procedure by now.
stringDecoder :: Hasql.Decoders.Value String
= fmap Data.Text.unpack Hasql.Decoders.text stringDecoder
But we still need to apply the Task
constructor over the
task description and status. Value
gives us nothing to do
that! Indeed, a Value a
only concerns itself with a single
column, whereas a Task
is stored across multiple columns.
So we must create a decoder for a row instead. First we create row
decoders from our value decoders:
stringDecoder_row :: Hasql.Decoders.Row String
= Hasql.Decoders.column stringDecoder
stringDecoder_row
taskStatusDecoder_row :: Hasql.Decoders.Row TaskStatus
= Hasql.Decoders.column taskStatusDecoder taskStatusDecoder_row
Now Row
is an instance of Applicative
!
Although functors allow us to use fmap
to get
fmap Task stringDecoder_row :: (Functor f) => f (TaskStatus -> Task)
they don’t give us a way to combine an f (a -> b)
with an f a
. For applicatives, on the oher hand, we
have:
(<*>) :: f (a -> b) -> f a -> f b
so we can do
fmap Task stringDecoder_row <*> taskStatusDecoder_row
!
We can improve readability using the <$>
operator:
taskDecoder :: Hasql.Decoders.Row Task
= Task <$> stringDecoder_row <*> taskStatusDecoder_row taskDecoder
Using rowList :: Row a -> Result [a]
we can now
easily create the result decoder we’d wanted.
= Hasql.Decoders.rowList taskDecoder tasksDecoder
Finally, we modify our top level functions to fetch the information
from the right database when we connect to the server. I’ve renamed
connectToDB
to fetchFromDB
.
someFunc :: IO ()
= do
someFunc 8080 requestHandler
run
requestHandler :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
=
requestHandler request respond let
= UTF8.fromString $ toHTMLPage htmlAble
htmlPage htmlAble = responseLBS status200 [] $ htmlPage tasks
response tasks in
do
<- fetchFromDB
taskList putStrLn "Received an HTTP request!"
$ response taskList
respond
fetchFromDB :: IO [Task]
=
fetchFromDB let
connectionSettings :: Hasql.Connection.Settings
=
connectionSettings
Hasql.Connection.settings"localhost"
fromInteger 5432)
("Haskell-student"
"Why-are-you-putting-credentials-in-code?-You-absolute-potato!"
"todolists"
in do
<- Hasql.Connection.acquire connectionSettings
connectionResult case connectionResult of
Left (Just errMsg) -> error $ StrictUTF8.toString errMsg
Left Nothing -> error "Unspecified connection error"
Right connection -> do
<- Hasql.Session.run selectTasksSession connection
queryResult
Hasql.Connection.release connectioncase queryResult of
Right result -> return result
Left err -> error $ show err
And the result:
The output does not look like much for the amount of work we put in. I will discuss this issue next chapter, along with some other reflections and cleanup.