Составление запросов 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 ()

Ответ 1

Для LIMIT и COUNT, ответ hammar полностью прав, поэтому я не буду вникать в них. Я просто повторю, что после использования select вы больше не сможете изменить запрос.

Для JOIN s в настоящее время вы не можете выполнить INNER JOIN с запросом, который был определен в другом from(FULL|LEFT|RIGHT) OUTER JOIN s). Однако вы можете делать неявные объединения. Например, если вы определили:

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 s. Если вам нужно сделать OUTER JOIN, вам придется реорганизовать свой код, чтобы все OUTER JOIN находились в одном и том же from (обратите внимание, что вы можете сделать неявное соединение между OUTER JOIN просто штрафом).

Ответ 2

Посмотрите документацию и тип 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

Это работает для ограничения и подсчета. Я еще не понял, как это сделать для присоединений, но похоже, что это должно быть возможно.