【发布时间】:2015-04-27 16:32:10
【问题描述】:
我有一个相当简单的查询,它执行两个外连接。 (一顿饭有很多食谱,而食谱又反过来有很多食物)。
getMeals :: (MonadIO m) => Key DbUser -> SqlPersistT m [Meal]
getMeals user =
fmap deserializeDb $ E.select $
E.from $ \(m `E.InnerJoin` u `E.LeftOuterJoin` r `E.LeftOuterJoin` f) -> do
E.on (r ?. DbRecipeId E.==. f ?. DbFoodRecipeId)
E.on (E.just (m ^. DbMealId) E.==. r ?. DbRecipeMealId)
E.on (m ^. DbMealUserId E.==. u ^. DbUserId)
E.where_ (m ^. DbMealUserId E.==. E.val user )
return (m, r, f)
这个查询很棒,它说明了它需要什么,没有任何其他内容。但是,由于 SQL 的工作方式,它为我返回了一个包含许多重复餐点的表,对于每个匹配的外连接。
例如,一顿饭有两个菜谱,每个菜谱有两种食物变成 4 个元组。
(m1, r1, f1)
(m1, r1, f2)
(m1, r2, f3)
(m1, r2, f4)
我想将这些回滚到单个 Meal 数据类型中。 (这里简化显示结构,其他字段当然存储在数据库中)。
data Meal = Meal { recipes :: [Recipe] }
data Recipe = Recipe { foods :: [Food] }
data Food = Food { name :: String }
我似乎必须完全手动进行此合并,而对于这个单个查询,它最终需要 2 页左右的代码。
忽略不应该这样使用类型类的事实,它看起来像很多(愚蠢的)类型类的实例DeserializeDb:
class DeserializeDb a r | a -> r where
deserializeDb :: a -> r
instance DeserializeDb [(Entity DbMeal, Maybe (Entity DbRecipe))] [Meal] where
deserializeDb items = let grouped = groupBy (\a b -> entityKey (fst a) == entityKey (fst b)) items
joined = map (\list -> ( (fst . head) list
, mapMaybe snd list
)) grouped
in (map deserializeDb joined)
截取大量各种复杂的实例(代码:https://gist.github.com/cschneid/2989057ec4bb9875e2ae)
instance DeserializeDb (Entity DbFood) Food where
deserializeDb (Entity _ val) = Food (dbFoodName val)
问题:
我唯一想公开的是查询签名。其余的都是实施垃圾。有没有我没有注意到的使用 Persistent 的技巧?我是否必须手动将联接合并回 haskell 类型?
【问题讨论】:
-
您的问题似乎与stackoverflow.com/questions/21686579/… 非常相似,但我认为您不会喜欢答案 (-:
-
我不介意手动执行此操作,除非它的复杂性似乎增长得很快。这里的 3 表连接是 7 行查询和 80 行反序列化。必须有更好的方法来处理这个问题。
-
我认为你应该只有属性的反序列化代码,并在元组中获得一个没有食谱的膳食,一个没有食物的食谱:(膳食,食谱,食物)然后将所有结果分组到一个方法中,它会简单得多。
-
对不起?我不跟。问题是元组的
(Meal部分重复,每个独特的食谱/食物一次。所以大部分groupBy垃圾都在解决这个问题。有更好的方法吗? -
我只是在想您将数据库的反序列化和分组结果混为一谈。具有从实体 DbMeal 转换为 Meal 等的基本功能可能会更容易,然后具有无需任何 DB ([(Meal,Recipe,Food)]->[Meal] 即可执行分组的功能。您可能是能够抽象更多并减少样板的数量。
标签: haskell yesod persistent esqueleto