问题 编写Database.Esqueleto查询,条件连接和计数


我该如何撰写 Database.Esqueleto 以模块化方式查询,以便在定义“基本”查询和相应的结果集之后,我可以通过添加额外的内部联接和表达式来限制结果集。

此外,如何将返回实体(或字段元组)列表的基本查询转换为计算结果集的查询,因为基本查询不是这样执行的,而是使用LIMIT和OFFSET修改它的版本。

采用以下不正确的Haskell代码片段 Yesod书 希望澄清我的目标。

{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, OverloadedStrings #-}
{-# LANGUAGE GADTs, FlexibleContexts #-}
import qualified Database.Persist as P
import qualified Database.Persist.Sqlite as PS
import Database.Persist.TH
import Control.Monad.IO.Class (liftIO)
import Data.Conduit
import Control.Monad.Logger
import Database.Esqueleto
import Control.Applicative

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
    name String
    age Int Maybe
    deriving Show
BlogPost
    title String
    authorId PersonId
    deriving Show
Comment
    comment String
    blogPostId BlogPostId
|]

main :: IO ()
main = runStdoutLoggingT $ runResourceT $ PS.withSqliteConn ":memory:" $ PS.runSqlConn $ do
    runMigration migrateAll

    johnId <- P.insert $ Person "John Doe" $ Just 35
    janeId <- P.insert $ Person "Jane Doe" Nothing

    jackId <- P.insert $ Person "Jack Black" $ Just 45
    jillId <- P.insert $ Person "Jill Black" Nothing

    blogPostId <- P.insert $ BlogPost "My fr1st p0st" johnId
    P.insert $ BlogPost "One more for good measure" johnId
    P.insert $ BlogPost "Jane's" janeId

    P.insert $ Comment "great!" blogPostId

    let baseQuery = select $ from $ \(p `InnerJoin` b) -> do 
        on (p ^. PersonId ==. b ^. BlogPostAuthorId)
        where_ (p ^. PersonName `like` (val "J%"))
        return (p,b)

    -- Does not compile
    let baseQueryLimited = (,) <$> baseQuery <*> (limit 2)

    -- Does not compile
    let countingQuery = (,) <$> baseQuery <*> (return countRows)

    -- Results in invalid SQL 
    let commentsQuery = (,) <$> baseQuery
                <*> (select $ from $ \(b `InnerJoin` c) -> do
                        on (b ^. BlogPostId ==. c ^. CommentBlogPostId)
                        return ())

    somePosts <- baseQueryLimited
    count <- countingQuery
    withComments <- commentsQuery
    liftIO $ print somePosts
    liftIO $ print ((head count) :: Value Int)
    liftIO $ print withComments
    return ()

10214
2018-05-16 19:30


起源



答案:


对于 LIMIT 和 COUNT,哈马尔的答案是完全正确的,所以我不会深入研究它们。一旦你使用,我会重申一下 select 您将无法再以任何方式更改查询。

对于 JOINs,目前你无法做到 INNER JOIN 使用另一个定义的查询 from (也不 (FULL|LEFT|RIGHT) OUTER JOINS)。但是,你 能够 做隐式连接。例如,如果您已定义:

baseQuery = 
  from $ \(p `InnerJoin` b) -> do 
  on (p ^. PersonId ==. b ^. BlogPostAuthorId)
  where_ (p ^. PersonName `like` val "J%")
  return (p, b)

然后你可以说:

commentsQuery = 
  from $ \c -> do
  (p, b) <- baseQuery
  where_ (b ^. BlogPostId ==. c ^. CommentBlogPostId)
  return (p, b, c)

然后,Esqueleto将产生以下内容:

SELECT ...
FROM Comment, Person INNER JOIN BlogPost
ON    Person.id = BlogPost.authorId
WHERE Person.name LIKE "J%"
AND   BlogPost.id = Comment.blogPostId

不漂亮,但完成工作 INNER JOIN秒。如果你需要做一个 OUTER JOIN 那么你将不得不重构你的代码以便所有的代码 OUTER JOINs是相同的 from (请注意,您可以在两者之间进行隐式连接 OUTER JOIN很好)。


7
2018-05-17 12:45



感谢您填补空白并提供确凿的答案。 - Tero
我还应该注意到 commentsQuery 你也可以用 baseQuery 在使用之前 from 正好。 - Felipe Lessa
另外,请将任何导致无效SQL的esqueleto查询报告为错误,以便我们可以调查其根。你看到的那个与治疗有关 (),一个已知但未修复的错误。作为一种解决方法,你可以做一些像 return (val True)。 - Felipe Lessa
esqueleto-1.2.1 应该解决你的问题 return () 无需任何变通方法=)。 - Felipe Lessa
感谢您提供额外的提示和错误修复。我在标题中引用的“条件连接”意味着我有许多连接可能会或可能不会在查询中用于过滤结果。现在似乎内连接已经足够了,所以我可以使用隐式连接语法。但是,我无法按照您的描述编写它们,因为下一个查询组合步骤需要指定上一个查询的实体,例如“(p,b)< - baseQuery”如上所述。我能做的是在同一个块中包含所有的from语句,并使用case语句来有条件地包含它们。 - Tero


答案:


对于 LIMIT 和 COUNT,哈马尔的答案是完全正确的,所以我不会深入研究它们。一旦你使用,我会重申一下 select 您将无法再以任何方式更改查询。

对于 JOINs,目前你无法做到 INNER JOIN 使用另一个定义的查询 from (也不 (FULL|LEFT|RIGHT) OUTER JOINS)。但是,你 能够 做隐式连接。例如,如果您已定义:

baseQuery = 
  from $ \(p `InnerJoin` b) -> do 
  on (p ^. PersonId ==. b ^. BlogPostAuthorId)
  where_ (p ^. PersonName `like` val "J%")
  return (p, b)

然后你可以说:

commentsQuery = 
  from $ \c -> do
  (p, b) <- baseQuery
  where_ (b ^. BlogPostId ==. c ^. CommentBlogPostId)
  return (p, b, c)

然后,Esqueleto将产生以下内容:

SELECT ...
FROM Comment, Person INNER JOIN BlogPost
ON    Person.id = BlogPost.authorId
WHERE Person.name LIKE "J%"
AND   BlogPost.id = Comment.blogPostId

不漂亮,但完成工作 INNER JOIN秒。如果你需要做一个 OUTER JOIN 那么你将不得不重构你的代码以便所有的代码 OUTER JOINs是相同的 from (请注意,您可以在两者之间进行隐式连接 OUTER JOIN很好)。


7
2018-05-17 12:45



感谢您填补空白并提供确凿的答案。 - Tero
我还应该注意到 commentsQuery 你也可以用 baseQuery 在使用之前 from 正好。 - Felipe Lessa
另外,请将任何导致无效SQL的esqueleto查询报告为错误,以便我们可以调查其根。你看到的那个与治疗有关 (),一个已知但未修复的错误。作为一种解决方法,你可以做一些像 return (val True)。 - Felipe Lessa
esqueleto-1.2.1 应该解决你的问题 return () 无需任何变通方法=)。 - Felipe Lessa
感谢您提供额外的提示和错误修复。我在标题中引用的“条件连接”意味着我有许多连接可能会或可能不会在查询中用于过滤结果。现在似乎内连接已经足够了,所以我可以使用隐式连接语法。但是,我无法按照您的描述编写它们,因为下一个查询组合步骤需要指定上一个查询的实体,例如“(p,b)< - baseQuery”如上所述。我能做的是在同一个块中包含所有的from语句,并使用case语句来有条件地包含它们。 - Tero


查看文档和类型 select

select :: (...) => SqlQuery a -> SqlPersistT m [r]

在打电话时很明显 select,我们离开了纯粹的可组合查询的世界(SqlQuery a)并进入副作用的世界(SqlPersistT m [r])。所以我们只需要在我们之前进行构图 select

let baseQuery = from $ \(p `InnerJoin` b) -> do 
      on (p ^. PersonId ==. b ^. BlogPostAuthorId)
      where_ (p ^. PersonName `like` (val "J%"))
      return (p,b)

let baseQueryLimited = do r <- baseQuery; limit 2; return r
let countingQuery    = do baseQuery; return countRows

somePosts <- select baseQueryLimited
count     <- select countingQuery

这适用于限制和计数。我还没想出如何为连接做这件事,但它看起来应该是可能的。


8
2018-05-16 22:09



感谢您部分解决问题。在我的用例中,我也需要编写连接,所以暂时我会使用连接原始SQL字符串。 - Tero
干杯@hammar。对于遇到麻烦的其他任何人来说,要编译它的countingQuery版本:我必须指定结果类型,(对于我的持久配置)是 [Value Int64]。即它评估一个单例列表,其成员是计数。 - Christian Brink