Question

Assume a simple 1 -> N relationship in your model.

I want to render a table with rows made of data from the main entity and an aggregated function of the dependant entity (the N), be it sum, avg, max or custom.

I have been trying dozens of handlers but did not find any that compiles...

Any hint?

Example:

Entity A

Key Value
a   b
c   d
e   f

Entity B

ForeignKey Num Value
a          1   13.0
a          2   25.4
a          3   10.2
c          1   33.33
c          2   50.0
e          1   100.0
e          2   1000.0

And want to obtain Value from A and Value from B with max Num in the same "html row"

b   10.2 
d   50.0
f   1000.0
Was it helpful?

Solution

I'm not sure what have you already tried, but since this is practically a join (and persistent's type-safe API doesn't support them), running a custom SQL query should be the most efficient solution. If your model looks something like this

EntityA
    key Text
    value Text
    deriving Show
EntityB
    foreignKey EntityAId
    num Int
    value Double
    deriving Show

a query for joining data with a max function would look something like this:

SELECT a.value, max(b.num), b.value
FROM entity_a a LEFT OUTER JOIN entity_b b
ON a.id = b.foreign_key
GROUP BY (a.id);

Combining that with a rawSql function should do the trick.

If, however, you want to do it with persistent's type-safe API, it can result in a lot of queries to the database, depending on the size of your data. Here's how it could look like:

-- a custom maximum function for EntityB
customFunction :: [EntityB] -> Maybe EntityB
customFunction = foldr customFold Nothing

customFold :: EntityB -> Maybe EntityB -> Maybe EntityB
customFold a Nothing = Just a
customFold a (Just b) = if (entityBNum a) > (entityBNum b) then (Just a) else (Just b)

main :: IO ()
main = runSqlite ":memory:" $ do
    runMigration migrateAll

    -- insert some data
    firstAId <- insert $ EntityA "a" "b"
    secondAId <- insert $ EntityA "c" "d"
    thirdAId <- insert $ EntityA "e" "f"
    -- let's put this one just for demonstration
    fourthAId <- insert $ EntityA "g" "h"

    bId1 <- insert $ EntityB firstAId 1 13.0
    bId2 <- insert $ EntityB firstAId 2 25.4
    bId3 <- insert $ EntityB firstAId 3 10.2
    bId4 <- insert $ EntityB secondAId 1 33.33
    bId5 <- insert $ EntityB secondAId 2 50.0
    bId6 <- insert $ EntityB thirdAId 1 100.0
    bId7 <- insert $ EntityB thirdAId 2 1000.0

    -- select all EntityA's
    as <- selectList [] [Asc EntityAId]
    -- aKeys are used as foreign keys for EntityB's
    let aKeys = map entityKey as
        -- these values will be used for "joining" data together
        aVals = map (entityAValue . entityVal) as
    -- this will produce a number of queries which is
    -- equal to a number of groups (in your simple case, 3)
    bs <- mapM (\bKey -> selectList [EntityBForeignKey ==. bKey] []) aKeys
    -- extract what's needed from a list of lists of EntityB's
    let bEntities = map (customFunction . (map entityVal)) bs
        -- ... and zip them together
        joined = zip aVals bEntities

    liftIO $ print joined

The output I get for this piece of code is:

[("b",Just (EntityB {entityBForeignKey = Key {unKey = PersistInt64 1}, entityBNum = 3, entityBValue = 10.2})),("d",Just (EntityB {entityBForeignKey = Key {unKey = PersistInt64 2}, entityBNum = 2, entityBValue = 50.0})),("f",Just (EntityB {entityBForeignKey = Key {unKey = PersistInt64 3}, entityBNum = 2, entityBValue = 1000.0})),("h",Nothing)]

Extracting the table like in your question and putting it into Handler monad should be straightforward now.

I'm aware that this is not the prettiest piece of code to look at, but it may be useful for you to compare the two approaches shown here.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top