-- allows automatic derivation from Entity type class
{-# LANGUAGE DeriveAnyClass #-}
module OneToManySpec
( test,
spec,
)
where
import Data.Maybe (fromJust)
import Database.GP.GenericPersistence
import Database.HDBC
import Database.HDBC.Sqlite3
import GHC.Generics
import Test.Hspec
-- `test` is here so that this module can be run from GHCi on its own. It is
-- not needed for automatic spec discovery.
-- (start up stack repl -- test to bring up ghci and have access to all the test functions)
test :: IO ()
test = hspec spec
prepareDB :: IO Conn
prepareDB = do
conn <- connect AutoCommit <$> connectSqlite3 ":memory:"
setupTableFor @Article SQLite conn
setupTableFor @Author SQLite conn
return conn
data Article = Article
{ articleID :: Int,
title :: String,
authorId :: Int,
year :: Int
}
deriving (Generic, Entity, Show, Eq) -- automatically derives Entity
data Author = Author
{ authorID :: Int,
name :: String,
address :: String,
articles :: [Article]
}
deriving (Generic, Show, Eq)
instance Entity Author where
fieldsToColumns :: [(String, String)] -- ommitting the articles field,
fieldsToColumns = -- as this can not be mapped to a single column
[ ("authorID", "authorID"),
("name", "name"),
("address", "address")
]
fromRow :: Conn -> [SqlValue] -> IO Author
fromRow conn row = do
let authID = head row -- authorID is the first column
articlesBy <- select conn (field "authorId" =. authID) -- retrieve all articles by this author
return rawAuthor {articles = articlesBy} -- add the articles to the author
where
rawAuthor = Author (col 0) (col 1) (col 2) [] -- create the author from row (w/o articles)
col i = fromSql (row !! i) -- helper function to convert SqlValue to Haskell type
toRow :: Conn -> Author -> IO [SqlValue]
toRow conn a = do
mapM_ (persist conn) (articles a) -- persist all articles of this author (update or insert)
return [toSql (authorID a), -- return the author as a list of SqlValues
toSql (name a), toSql (address a)]
article1 :: Article
article1 =
Article
{ articleID = 1,
title = "Persistence without Boilerplate",
authorId = 1,
year = 2018
}
article2 :: Article
article2 =
Article
{ articleID = 2,
title = "Boilerplate for Dummies",
authorId = 2,
year = 2020
}
article3 :: Article
article3 =
Article
{ articleID = 3,
title = "The return of the boilerplate",
authorId = 2,
year = 2022
}
arthur :: Author
arthur =
Author
{ authorID = 2,
name = "Arthur Miller",
address = "Denver",
articles = [article2, article3]
}
spec :: Spec
spec = do
describe "Handling of 1:N References" $ do
it "works like a charm" $ do
conn <- prepareDB
insert conn arthur
insert conn article1
authors <- select conn allEntries :: IO [Author]
length authors `shouldBe` 1
articles' <- select conn allEntries :: IO [Article]
length articles' `shouldBe` 3
author2 <- selectById conn "2" :: IO (Maybe Author)
fromJust author2 `shouldBe` arthur
length (articles $ fromJust author2) `shouldBe` 2