2013-05-16 11 views
11

Jak mogę skompilować zapytania Database.Esqueleto w sposób modułowy tak, że po zdefiniowaniu zapytania "podstawowego" i odpowiedniego zestawu wyników mogę ograniczyć zestaw wyników przez dodanie dodatkowych wewnętrznych sprzężeń i gdzie wyrażenia.Komponowanie zapytań SQL, warunkowe łączenie i liczenie

Ponadto, w jaki sposób mogę przekonwertować zapytanie podstawowe, które zwraca listę elementów (lub krotek pól) do zapytania, które zlicza zestaw wyników, ponieważ zapytanie podstawowe nie jest wykonywane jako takie, ale zmodyfikowana wersja z LIMITem i OFFSET.

Poniższy niepoprawny fragment kodu Haskella przyjęty z the Yesod Book powinien, mam nadzieję, wyjaśnić, do czego dążę.

{-# 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() 

Odpowiedz

7

Dla LIMIT i COUNT, odpowiedź hammar jest całkowicie poprawna, więc nie będę zagłębiał się w nie. Powtórzę, że po użyciu select nie będzie można ponownie zmienić zapytania w żaden sposób.

Dla JOIN s, aktualnie nie jesteś w stanie zrobić INNER JOIN z kwerendy, która została zdefiniowana w innym from (NOR (FULL|LEFT|RIGHT) OUTER JOIN s). Jednak możesz robić niejawne łączenia. Na przykład, jeśli masz zdefiniowane:

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

Następnie można po prostu powiedzieć:

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

Esqueleto następnie wygeneruje coś wzdłuż linii:

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

Nie dość ale dostaje zadanie wykonane dla INNER JOIN s. Jeśli potrzebujesz zrobić OUTER JOIN, będziesz musiał zmienić swój kod tak, aby wszystkie OUTER JOIN s były w tym samym from (zauważ, że możesz zrobić niejawne sprzężenie między OUTER JOIN s po prostu w porządku).

+1

Dzięki za wypełnienie luk i udzielenie jednoznacznej odpowiedzi. – Tero

+0

Należy również zauważyć, że w 'commentQuery' można również użyć' baseQuery' przed użyciem 'od' po prostu dobrze. –

+0

Proszę również zgłosić jako błąd zapytanie esqueleto, które skutkuje nieprawidłowym SQL, abyśmy mogli zbadać jego korzenie. Ten, który widziałeś, ma związek z traktowaniem '()', znanego, ale nie naprawionego błędu.Jako obejście można zrobić coś takiego jak 'return (val True)'. –

8

Patrząc na dokumentacji oraz rodzaju select:

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

To jasne, że na powołanie select, zostawiamy świat czystych zapytań composable (SqlQuery a) i wejść w świat skutków ubocznych (SqlPersistT m [r]). Musimy więc po prostu komponować przed 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 

Umożliwia to ograniczanie i liczenie. Nie wymyśliłem jeszcze, jak to zrobić, aby dołączyć, ale wygląda na to, że powinno być możliwe.

+0

Dzięki za rozwiązanie problemu częściowo. W moim przypadku użycia muszę również skomponować sprzężenia, więc na razie będę uciekał się do łączenia surowych łańcuchów SQL. – Tero

+3

Pozdrawiam @hammar. Dla każdego, kto wpadł w kłopoty z uzyskaniem wersji countingQuery do kompilacji: musiałem określić typ wyniku, który (dla mojej konfiguracji Persistent) to '[Value Int64]'. To znaczy. ocenia na pojedynczą listę, której członkiem jest liczba. –