Working with databases in Haskell usually involves using libraries to interface with the database system and to provide a functional and type-safe way of querying and manipulating data. There are several popular libraries for working with databases in Haskell. I’ll discuss a few notable ones and provide some examples.
1. **persistent**
‘persistent‘ is a popular library for working with databases in Haskell. It provides a DSL for defining your data model and can work with different database backends like PostgreSQL, SQLite, and MySQL.
To use ‘persistent‘, first install it and the corresponding backend package, e.g., ‘persistent-postgresql‘. Create a file named ‘Model.hs‘ with a data model description. For example, a data model with a "Person" table might look like this:
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Database.Persist.TH
import Data.Text (Text)
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person json
name Text
age Int
deriving Show
|]
Note the ‘QuasiQuotes‘ and ‘TemplateHaskell‘ language extensions that enable the domain-specific language (DSL) for describing the schema.
Next, connect to the database and run queries:
{-# LANGUAGE OverloadedStrings #-}
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (runStdoutLoggingT)
import Control.Monad.Trans.Resource (runResourceT)
import Database.Persist
import Database.Persist.Postgresql
import Model
connStr = "host=localhost dbname=test_db user=test_user password=test_password port=5432"
main :: IO ()
main = runStdoutLoggingT $ withPostgresqlConn connStr $ backend ->
runResourceT $ runNoLoggingT $ runConn backend
runConn :: SqlBackend -> IO ()
runConn conn = do
runSqlConn (runMigration migrateAll) conn
johnId <- insert' conn $ Person "John Doe" 30
janeId <- insert' conn $ Person "Jane Doe" 28
people <- selectList' conn []
liftIO $ print (people :: [Entity Person])
insert' :: SqlBackend -> Person -> IO (Key Person)
insert' = flip runSqlConn . insert
selectList' :: SqlBackend -> [Filter Person] -> IO [Entity Person]
selectList' = flip runSqlConn . (f -> selectList f [])
This code creates a connection to a PostgreSQL database, applies the migration to create the "Person" table if necessary, and then inserts two example records, and queries them back.
2. **esqueleto**
‘esqueleto‘ is a library built on top of ‘persistent‘ that provides a richer and more flexible DSL for constructing SQL queries. It adds support for joins, group by, aggregates, and more complex SQL constructs that aren’t supported natively by ‘persistent‘.
To try ‘esqueleto‘, install it and import the ‘Database.Esqueleto‘ module. Use the query DSL to write more advanced queries. For example, to retrieve all "Person" records with age above 29:
{-# LANGUAGE OverloadedStrings #-}
import Database.Esqueleto
import Model
-- ...
runConn :: SqlBackend -> IO ()
runConn conn = do
runSqlConn (runMigration migrateAll) conn
johnId <- insert' conn $ Person "John Doe" 30
janeId <- insert' conn $ Person "Jane Doe" 28
people <- selectOlderPeople conn 29
liftIO $ print (people :: [Entity Person])
selectOlderPeople :: SqlBackend -> Int -> IO [Entity Person]
selectOlderPeople conn age = flip runSqlConn conn $ do
select $
from $ person -> do
where_ (person ^. PersonAge >. val age)
return person
3. **opaleye**
‘opaleye‘ is another library for interfacing with PostgreSQL databases specifically. It encourages a more type-safe approach by providing a type-level DSL for constructing queries and mapping them to Haskell types.
Here’s an example of using ‘opaleye‘ to declare a "Person" table, insert a record, and query the table:
{-# LANGUAGE Arrows #-}
import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
import Data.Text (Text)
import Opaleye
data Person' a b = Person
{ personName :: a,
personAge :: b
}
type Person = Person' Text Int
type PersonColumn = Person' (Column PGText) (Column PGInt4)
type PersonTable = Table PersonColumn PersonColumn
personTable :: PersonTable
personTable =
Table
"person"
(p2 (Person {personName = required "name", personAge = required "age"}))
$(makeAdaptorAndInstance "pPerson" ''Person')
personToInsert :: Person -> PersonColumn
personToInsert = pPerson Person
main :: IO ()
main = do
con <- connect defaultConnectInfo {connectDatabase = "test_db"}
runInsert con personTable (personToInsert (Person "John Doe" 30))
people <- runQuery con (queryTable personTable)
mapM_ print people
Choose a library based on the specific requirements of your application, such as the need for complex SQL queries or support for specific database systems. For starters, ‘persistent‘ with ‘esqueleto‘ is a good choice because of its flexibility, its intuitive DSL, and support for multiple database systems.
Remember that examples provided for each library will require specific packages to be added to the ‘.cabal‘ or ‘package.yaml‘ file dependencies.