Skip to content

himura/persistent-relational-record

Repository files navigation

persistent-relational-record

CI Hackage Hackage-Deps

About

persistent-relational-record build a bridge between Haskell Relational Record and Persistent. It uses the persistent entities definition instead of obtaining schema from DB at compilation time.

Getting Started

If you already define an entities in persistent's manner, then you are almost ready to use this module. The entities definition in the style of persistent-relational-record are shown below:

Model.hs:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module Model where

import Data.Text (Text)
import Database.Persist.Relational
import Database.Persist.TH
import GHC.Generics

share [mkPersist sqlSettings, mkMigrate "migrateAll", mkHrr, deriveGenericForEntityId] [persistLowerCase|
User
    email       Text
    name        Text

    UniqueUserEmail email
    deriving Show Eq Generic

UserGroup
    name        Text
    pageUrl     Text Maybe

    UniqueUserGroupName  name
    deriving Show Eq Generic

Membership
    userId      UserId
    userGroupId UserGroupId

    UniqueImageTag userId userGroupId
    deriving Show Eq Generic
|]

The main difference from the persistent version is that mkHrr are added to the 1st argument of the share function. mkHrr generates various instances and helper functions from the entities definition to cooperate with HRR.

NOTE: deriveGenericForEntityId is also appended to the list in above example. Currently, persistent does not generate Generic instances for EntityID types (UserId, UserGroupId, and MembershipId in above example). Haskell Relational Record rely on the Generic instance of record types, so persistent-relational-record provides deriveGenericForEntityId for convenience to derive Generic instance via StandaloneDeriving

Now, you can build queries in manner of HRR:

{-# LANGUAGE OverloadedLabels #-}

module Query where

import Data.Text (Text)
import Database.Persist (Entity)
import Database.Relational

import Model

-- ^ query users by UserGroup name list
--
-- @
-- SELECT
--   user.*
-- FROM user
-- INNER JOIN (
--   -- built by userIdFromUserGroupNameList
--   SELECT
--     user_id
--   FROM membership
--   INNER JOIN userGroup ON userGroup.id = membership.userGroup_id
--   WHERE userGroup.name IN (<<userGroupNames>>)
--   GROUP BY membership.user_id
--   HAVING COUNT(membership.user_id) = <<length userGroupNames>>
-- ) uid
-- ON user.id = uid.user_id
-- @
selectUserByUserGroupNameList
    -> [Text] -- ^ list of UserGroup name
    -> Relation () (Entity User)
selectUserByUserGroupNameList userGroupNames =
    relation $ do
        user <- query userTable
        userId <- query $ userIdFromUserGroupNameList userGroupNames
        on $ #id user .=. userId
        return user

-- ^ query user_id by UserGroup name list
--
-- @
-- SELECT
--   user_id
-- FROM membership
-- INNER JOIN userGroup ON userGroup.id = membership.userGroup_id
-- WHERE userGroup.name IN (<<userGroupNames>>)
-- GROUP BY membership.user_id
-- HAVING COUNT(membership.user_id) = <<length userGroupNames>>
-- @
userIdFromUserGroupNameList
    :: [Text] -- ^ list of userGroup name
    -> Relation () UserId
userIdFromUserGroupNameList userGroupNames =
    aggregateRelation $ do
        membership <- query membershipTable
        userGroup <- query userGroupTable
        on $ #id userGroup .=. #userGroupId membership
        wheres $ #name userGroup `in'` values userGroupNames
        g <- groupBy $ #userId membership
        let c = count $ (#userGroupId membership)
        having $ c .=. value (fromIntegral . length $ userGroupNames)
        return g

Finally, we can execute a query by runQuery:

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad.IO.Class
import Control.Monad.Logger
import Control.Monad.Trans.Resource
import Data.Conduit
import qualified Data.Conduit.List as CL
import Database.Persist.MySQL
import Database.Persist.Relational
import Database.Relational

import Model
import Query

sample :: SqlPersistT (LoggingT IO) [Entity User]
sample = runResourceT . runConduit $ runQuery (relationalQuery $ selectUserByUserGroupNameList True ["tokyo", "haskell"]) () .| CL.consume

main :: IO ()
main = runStderrLoggingT $ withMySQLPool defaultConnectInfo 10 $ runSqlPool $ do
    mapM_ (liftIO . print) =<< sample

runQuery run the HRR Query and gives the result as conduit Source.

For a full runnable example, see examples directory.