Posted on: 14/12/2019
Introduction
This tutorial is written with GitChapter. This allows you to clone down the tutorial from https://github.com/chrissound/HaskellPersistentTutorial, and hack along from any chapter. You can checkout a specific chapter from any of the chapter commit references that are shown as:
Chapter offset
Start Commit:
SHA: c22ab94b09f020344d44a4a6f7336e7f992f0a9f
Tag(s): gch-begin-1
-------
End commit:
SHA: 1cfedcb7383853e5d2f5315078e3d7a61b420405
Tag(s): gch-end-1
I decided to learn the Haskell persistent library, and what better way to learn something, than write a tutorial in the midst! So here you go! Bit of an experiment from my part.
Lets begin! I’ve based off this tutorial from https://www.yesodweb.com/book/persistent which is released under https://creativecommons.org/licenses/by/4.0/ - which I’m very thankful for!
So we start off with a very simple example involving an in-memory sqlite database:
package.yaml
name: HaskellNixCabalStarter
version: '0.1.0.0'
author: HaskellNixCabalStarter
maintainer: HaskellNixCabalStarter
license: OtherLicense
dependencies:
- base >=4.12 && <4.13
- persistent
- persistent-sqlite
- persistent-template
executables:
app:
source-dirs: src
main: Main.hs
Main.hs
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Monad.IO.Class (liftIO)
import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
name String
age Int Maybe
deriving Show
BlogPost
title String
authorId PersonId
deriving Show
|]
main :: IO ()
main = runSqlite ":memory:" $ do
runMigration migrateAll
johnId <- insert $ Person "John Doe" $ Just 35
janeId <- insert $ Person "Jane Doe" Nothing
insert $ BlogPost "My fr1st p0st" johnId
insert $ BlogPost "One more for good measure" johnId
oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
liftIO $ print (oneJohnPost :: [Entity BlogPost])
john <- get johnId
liftIO $ print (john :: Maybe Person)
delete janeId
deleteWhere [BlogPostAuthorId ==. johnId]
Great so with the above setup we can run cabal run
which outputs:
Resolving dependencies...
Build profile: -w ghc-8.6.5 -O1
In order, the following will be built (use -v for more details):
- HaskellNixCabalStarter-0.1.0.0 (exe:app) (configuration changed)
Configuring executable 'app' for HaskellNixCabalStarter-0.1.0.0..
Preprocessing executable 'app' for HaskellNixCabalStarter-0.1.0.0..
Building executable 'app' for HaskellNixCabalStarter-0.1.0.0..
[1 of 2] Compiling Paths_HaskellNixCabalStarter ( /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/autogen/Paths_HaskellNixCabalStarter.hs, /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app-tmp/Paths_HaskellNixCabalStarter.o ) [/home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/autogen/cabal_macros.h changed]
[2 of 2] Compiling Main ( src/Main.hs, /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app-tmp/Main.o )
Linking /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app ...
[Entity {entityKey = BlogPostKey {unBlogPostKey = SqlBackendKey {unSqlBackendKey = 1}}, entityVal = BlogPost {blogPostTitle = "My fr1st p0st", blogPostAuthorId = PersonKey {unPersonKey = SqlBackendKey {unSqlBackendKey = 1}}}}]
Just (Person {personName = "John Doe", personAge = Just 35})
Switching to PostgreSQL
Chapter offset
Start Commit:
SHA: 1cfedcb7383853e5d2f5315078e3d7a61b420405
Tag(s): gch-end-1
-------
End commit:
SHA: 20c03034e9579196d5af677887d4738d55d0671d
Tag(s): gch-begin-2,gch-end-2
We can setup a simple postgres database with the following docker-compose config:
version: '3'
services:
adminer:
image: "adminer"
ports:
- 8080:8080
database:
image: "postgres:alpine"
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=mysecretpassword
volumes:
- ./_data2/:/var/lib/postgresql/data
diff --git a/package.yaml b/package.yaml
index 0f0679e..874fc0e 100644
--- a/package.yaml
+++ b/package.yaml
@@ -7,7 +7,9 @@ dependencies:
- base >=4.12 && <4.13
- persistent
- persistent-sqlite
+- persistent-postgresql
- persistent-template
+- monad-logger
executables:
app:
source-dirs: src
diff --git a/src/Main.hs b/src/Main.hs
index 7feb34b..0343c48 100644
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -9,8 +9,10 @@
{-# LANGUAGE TypeFamilies #-}
import Control.Monad.IO.Class (liftIO)
import Database.Persist
-import Database.Persist.Sqlite
import Database.Persist.TH
+import Database.Persist.Postgresql
+import Control.Monad.IO.Class (liftIO)
+import Control.Monad.Logger (runStderrLoggingT)
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
@@ -23,21 +25,25 @@ BlogPost
deriving Show
|]
+dbConStr :: ConnectionString
+dbConStr = "host=127.0.0.1 port=5432 user=postgres dbname=postgres password=mysecretpassword"
+
main :: IO ()
-main = runSqlite ":memory:" $ do
- runMigration migrateAll
+main = runStderrLoggingT $ withPostgresqlPool dbConStr 10 $ \pool -> liftIO $ do
+ flip runSqlPersistMPool pool $ do
+ runMigration migrateAll
- johnId <- insert $ Person "John Doe" $ Just 35
- janeId <- insert $ Person "Jane Doe" Nothing
+ johnId <- insert $ Person "John Doe" $ Just 35
+ janeId <- insert $ Person "Jane Doe" Nothing
- insert $ BlogPost "My fr1st p0st" johnId
- insert $ BlogPost "One more for good measure" johnId
+ insert $ BlogPost "My fr1st p0st" johnId
+ insert $ BlogPost "One more for good measure" johnId
- oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
- liftIO $ print (oneJohnPost :: [Entity BlogPost])
+ oneJohnPost <- selectList [BlogPostAuthorId ==. johnId] [LimitTo 1]
+ liftIO $ print (oneJohnPost :: [Entity BlogPost])
- john <- get johnId
- liftIO $ print (john :: Maybe Person)
+ john <- get johnId
+ liftIO $ print (john :: Maybe Person)
- delete janeId
- deleteWhere [BlogPostAuthorId ==. johnId]
+ delete janeId
+ deleteWhere [BlogPostAuthorId ==. johnId]
Great so with the above setup we can run cabal run
which outputs:
Resolving dependencies...
Build profile: -w ghc-8.6.5 -O1
In order, the following will be built (use -v for more details):
- HaskellNixCabalStarter-0.1.0.0 (exe:app) (configuration changed)
Configuring executable 'app' for HaskellNixCabalStarter-0.1.0.0..
Preprocessing executable 'app' for HaskellNixCabalStarter-0.1.0.0..
Building executable 'app' for HaskellNixCabalStarter-0.1.0.0..
[1 of 2] Compiling Paths_HaskellNixCabalStarter ( /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/autogen/Paths_HaskellNixCabalStarter.hs, /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app-tmp/Paths_HaskellNixCabalStarter.o ) [/home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/autogen/cabal_macros.h changed]
[2 of 2] Compiling Main ( src/Main.hs, /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app-tmp/Main.o )
Linking /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app ...
[Entity {entityKey = BlogPostKey {unBlogPostKey = SqlBackendKey {unSqlBackendKey = 13}}, entityVal = BlogPost {blogPostTitle = "My fr1st p0st", blogPostAuthorId = PersonKey {unPersonKey = SqlBackendKey {unSqlBackendKey = 13}}}}]
Just (Person {personName = "John Doe", personAge = Just 35})
Real world example - updating a database record
Chapter offset
Start Commit:
SHA: 20c03034e9579196d5af677887d4738d55d0671d
Tag(s): gch-begin-2,gch-end-2
-------
End commit:
SHA: a843f457cbcb0136e03bb0725a3c1d172c53f8d3
Tag(s): gch-begin-3,gch-end-3
Lets try make a simple function, that takes an Int
and returns a Maybe Person
function.
So in the initiual tutorial they were using a get
function… However looking this function up in the Hackage index for persistent-posgresql
(http://hackage.haskell.org/package/persistent-postgresql-2.10.1/docs/doc-index-All.html) has no link suprisingly? Probably some type hackery of some sort going on… Grr
Oh no actually it’s just defined in the persistent
hackage index (so maybe it’s re-exported?). http://hackage.haskell.org/package/persistent-2.10.4/docs/Database-Persist-Class.html#v:get
If we define a function as:
getUser :: Key Person -> ReaderT backend0 m0 ()
getUser x = do
maybePerson <- get x
case maybePerson of
Nothing -> liftIO $ putStrLn "Just kidding, not really there"
Just person -> liftIO $ print (person :: Person)
The above gives an error of:
• Couldn't match type ‘BaseBackend backend0’ with
‘SqlBackend’
arising from a use of ‘get’
• In a stmt of a 'do' block: maybePerson <- get x
In the expression:
do maybePerson <- get x
case maybePerson of
Nothing -> liftIO $ putStrLn "Just kidding, not
really there"
Just person -> liftIO $ print (person :: Person)
Googling this error shows up with a stackoverflow question I literally posted nearly 2+ years ago… (My last attempt with persistent ahem…)
Okay so taking the suggestion from stackoveflow we end up with:
getUser :: Key Person -> ReaderT SqlBackend m ()
Which errors with a missing monad instance for m
… So filling those out we end up with:
getUser :: (MonadIO m) => Key Person -> ReaderT SqlBackend m ()
Yay!!! But wait we wanted an Int
as input…
Hoogle shows 0 results for:
Int -> Key +persistent
Int -> Key +persistent-postgresql
A quick web search shows up with https://stackoverflow.com/questions/28068447/haskell-persistent-how-get-entity-from-db-by-key-if-i-have-key-in-integer-varia
Which points to use the following function:
http://www.stackage.org/haddock/lts-1.2/persistent-2.1.1.4/Database-Persist-Sql.html#v:toSqlKey
toSqlKey :: ToBackendKey SqlBackend record => Int64 -> Key record
AND FINALLY we end up with:
getUser :: (MonadIO m) => Int -> ReaderT SqlBackend m ()
getUser x = do
maybePerson <- get $ toSqlKey x
case maybePerson of
Nothing -> liftIO $ putStrLn "Just kidding, not really there"
Just person -> liftIO $ print (person :: Person)
Which errors with:
• Couldn't match expected type ‘GHC.Int.Int64’
with actual type ‘Int’
• In the first argument of ‘toSqlKey’, namely
‘x’
Which we can use fromIntegral
to fix, though I’m not 100% sure of any potential limitations due to it.
So to summarize this chapter we’ve ended up with:
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Monad.IO.Class (liftIO)
import Database.Persist
import Database.Persist.TH
import Database.Persist.Postgresql
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (runStderrLoggingT)
import Data.List (find)
import User
import Data.String
dbConStr :: ConnectionString
dbConStr = "host=127.0.0.1 port=5432 user=postgres dbname=postgres password=mysecretpassword"
main :: IO ()
main = do
let input = [
("id", "1")
, ("name", "Chris")
, ("age", "100")
]
let x = Person "" Nothing
print x
let inputId = case lookup "id" input of
Just x -> read x
Nothing -> error "no id specified"
runStderrLoggingT $ withPostgresqlPool dbConStr 10 $ \pool -> liftIO $ do
flip runSqlPersistMPool pool $ do
runMigration migrateAll
maybePerson <- getUser inputId
liftIO $ print $ maybePerson
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module User where
import Database.Persist
import Database.Persist.TH
import Database.Persist.Postgresql
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
name String
age Int Maybe
deriving Show
BlogPost
title String
authorId PersonId
deriving Show
|]
-- :: (MonadBaseControl IO m, MonadIO m, IsSqlBackend backend)
-- => Text
-- getUser :: Key Person -> ReaderT backend (NoLoggingT (ResourceT m)) a
getUser :: (MonadIO m) => Int -> ReaderT SqlBackend m Person
getUser x = do
maybePerson <- get $ toSqlKey $ fromIntegral x
case maybePerson of
Nothing -> error "Just kidding, not really there"
Just person -> pure person
diff --git a/package.yaml b/package.yaml
index 874fc0e..09e53fe 100644
--- a/package.yaml
+++ b/package.yaml
@@ -10,6 +10,7 @@ dependencies:
- persistent-postgresql
- persistent-template
- monad-logger
+- mtl
executables:
app:
source-dirs: src
And the output from cabal run
:
Resolving dependencies...
Build profile: -w ghc-8.6.5 -O1
In order, the following will be built (use -v for more details):
- HaskellNixCabalStarter-0.1.0.0 (exe:app) (configuration changed)
Configuring executable 'app' for HaskellNixCabalStarter-0.1.0.0..
Preprocessing executable 'app' for HaskellNixCabalStarter-0.1.0.0..
Building executable 'app' for HaskellNixCabalStarter-0.1.0.0..
[1 of 3] Compiling Paths_HaskellNixCabalStarter ( /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/autogen/Paths_HaskellNixCabalStarter.hs, /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app-tmp/Paths_HaskellNixCabalStarter.o ) [/home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/autogen/cabal_macros.h changed]
[2 of 3] Compiling User ( src/User.hs, /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app-tmp/User.o )
[3 of 3] Compiling Main ( src/Main.hs, /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app-tmp/Main.o )
Linking /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app ...
Person {personName = "", personAge = Nothing}
Person {personName = "John Doe", personAge = Just 35}
Real real world example - updating a database record
Okay lets try a real world example, updating a database record based on some user input (like something we’d get from a HTTP request).
To keep things simple we’ll just have a [(String,String)]
input.
In order to update a record we need to use a “Query update combinators”. I’ll be using (https://hackage.haskell.org/package/persistent-2.10.4/docs/Database-Persist.html#v:-61-.)
(=.) :: forall v typ. PersistField typ => EntityField v typ -> typ -> Update v
And then with the following defined:
usermapper :: String -> String -> Update Person
usermapper "name" = (PersonName =.)
usermapper "age" = (PersonAge =.) . Just . read
usermapper _ = error "Invalid field"
We can use the above function as below, the important bits being:
let input = [
("id", "1")
, ("name", "Chris")
, ("age", "100")
]
let updates = fmap (\(a,b) -> usermapper a b) $ tail input
Nice and simple! So our Main.hs
ends up with:
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Monad.IO.Class (liftIO)
import Database.Persist
import Database.Persist.TH
import Database.Persist.Postgresql
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (runStderrLoggingT)
import Data.List (find)
import User
import Data.String
dbConStr :: ConnectionString
dbConStr = "host=127.0.0.1 port=5432 user=postgres dbname=postgres password=mysecretpassword"
usermapper :: String -> String -> Update Person
usermapper "name" = (PersonName =.)
usermapper "age" = (PersonAge =.) . Just . read
usermapper _ = error "Invalid field"
main :: IO ()
main = do
let input = [
("id", "1")
, ("name", "Chris")
, ("age", "100")
]
let x = Person "" Nothing
print x
let inputId = case lookup "id" input of
Just x -> read x
Nothing -> error "no id specified"
let updates = fmap (\(a,b) -> usermapper a b) $ tail input
runStderrLoggingT $ withPostgresqlPool dbConStr 10 $ \pool -> liftIO $ do
flip runSqlPersistMPool pool $ do
runMigration migrateAll
maybePerson <- getUser inputId
liftIO $ print "Found user:"
liftIO $ print $ maybePerson
update (toSqlKey 10) updates
And the output from cabal run
:
Build profile: -w ghc-8.6.5 -O1
In order, the following will be built (use -v for more details):
- HaskellNixCabalStarter-0.1.0.0 (exe:app) (file src/Main.hs changed)
Preprocessing executable 'app' for HaskellNixCabalStarter-0.1.0.0..
Building executable 'app' for HaskellNixCabalStarter-0.1.0.0..
[3 of 3] Compiling Main ( src/Main.hs, /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app-tmp/Main.o )
Linking /home/chris/NewProjects/HaskellTutorials/Persistent/dist-newstyle/build/x86_64-linux/ghc-8.6.5/HaskellNixCabalStarter-0.1.0.0/x/app/build/app/app ...
Person {personName = "", personAge = Nothing}
"Found user:"
Person {personName = "John Doe", personAge = Just 35}
Hopefully you’ve found this tutorial useful! Personally I’m happy to have finally gotten something simple working without battling the various types, something that scared me off from using persistent in the first place (and all those language extensions!).
No comments, yet!