2017-09-06 4 views
3

J'écris une webapp avec Yesod & Persistant. J'ai une base de données SQL avec plusieurs tables, contenant les caractéristiques de mes "projets". J'ai une table principale et pour l'option avec plusieurs valeurs des tables supplémentaires liées à l'id.Construire dynamiquement des requêtes SQL avec Esqueleto et Template Haskell?

L'utilisateur doit pouvoir choisir parmi les caractéristiques qu'il souhaite filtrer et spécifier la valeur du filtre. Si les filtres utilisateur pour le système d'exploitation, la licence et le codage de la requête SQL se présente comme suit:

runquery :: (YesodPersist site, YesodPersistBackend site ~ SqlBackend) => 
      String -> String -> String 
      -> HandlerT site IO [Entity Project] 
runquery os license coding = runDB 
    $ select $ distinct 
    $ from $ \(p `InnerJoin` pl `InnerJoin` l `InnerJoin` pc 
      `InnerJoin` c `InnerJoin` o `InnerJoin` po) -> do 
    on $ p ^. ProjectId ==. pl ^. ProjectLicenseFkProjectId 
    on $ p ^. ProjectId ==. pc ^. ProjectCodingFkProjectId 
    on $ p ^. ProjectId ==. po ^. ProjectOsFkProjectId 
    on $ l ^. LicenseId ==. pl ^. ProjectLicenseFkLicenseId 
    on $ o ^. OsId  ==. po ^. ProjectOsFkOsId 
    on $ c ^. CodingId ==. pc ^. ProjectCodingFkCodingId 
    where_ (o ^. OsName ==. val (Just (pack os))) 
    where_ (l ^. LicenseName ==. val (Just (pack license))) 
    where_ (c ^. CodingName ==. val (Just (pack coding))) 
    limit 50 
    return p 

mais je ne veux pas toujours rejoindre toutes les tables, car ce serait très mauvais pour les performances lorsque il y a beaucoup de tables mais l'utilisateur ne filtre que sur quelques tables. Mais je ne veux pas non plus écrire une requête pour chaque combinaison d'entités pouvant faire l'objet d'une requête, car cela signifierait écrire N² des requêtes presque identiques.

Les clauses 'on' et 'where' peuvent être exécutées dynamiquement selon que l'on souhaite filtrer ou non. Mais les jointures sont dans les paramètres de la fonction Lambda. Je n'ai trouvé aucun moyen de construire cette dépendance sur les variables externes. J'ai donc pensé que Template Haskell pourrait faire l'affaire ... J'ai commencé à apprendre TH et à convertir le cœur de la requête en TH. Mais maintenant je suis coincé et je ne sais pas si TH peut m'aider et si c'est la bonne façon?

Voici donc mes progrès avec modèle Haskell:

foo os license coding = lamE [pat] (code) 
    where 
     p = mkName "p" 
     po = mkName "po" 
     pl = mkName "pc" 
     pc = mkName "pl" 
     pat = pat' [os, license, coding] [varP po, varP pl, varP pc] 
     pat' []   []  = varP p 
     pat' ((Just _):ws) (q:qs) = infixP q (mkName "InnerJoin") (pat' ws qs) 
     pat' (Nothing:ws) (q:qs) = pat' ws qs 
     code = do 
      case os of 
       Just _ -> [| 
        on $ $(varE p) ^. ProjectId ==. $(varE po) ^. ProjectOsFkProjectId 
        |] 
       Nothing -> [| return() |] 
      case license of 
       Just _ -> [| 
        on $ $(varE p) ^. ProjectId ==. $(varE pl) ^. ProjectLicenseFkProjectId 
        |] 
       Nothing -> [| return() |] 
      case coding of 
       Just _ -> [| 
        on $ $(varE p) ^. ProjectId ==. $(varE pc) ^. ProjectCodingFkProjectId 
        |] 
       Nothing -> [| return() |] 
      [| do 
      limit 50 
      return $(varE p) |] 

Je voudrais donc vous aider:

  • Can/Dois-je faire avec modèle Haskell?
  • Si oui: comment puis-je appeler la fonction foo avec des arguments?
  • Sinon: quelle est la bonne solution?
+0

Si vous allez faire avec TH, alors vous n'avez pas besoin d'écrire une fonction unique de TH pour l'ensemble de la fonction 'runquery' - vous pouvez avoir juste une fonction TH qui produit un motif. Donc vous pouvez avoir quelque chose comme 'from $ \ $ (mkInnerJoin [" l "," o "," c "]) -> ...'. TH n'est probablement pas le meilleur moyen de le faire (c'est la meilleure façon de faire peu de choses ...); vous pourriez commencer par définir un type de données pour représenter la structure de votre requête. – user2407038

Répondre

0

Je trouve que l'utilisation de sous-requêtes est dans mon cas beaucoup plus rapide que rejoint de toute façon et vous pouvez les faire en cas de besoin:

runquery os license coding = runDB 
    $ select $ distinct 
    $ from $ \p -> do 
     case os of 
      Just os' -> 
      where_ $ p ^. ProjectId `in_` 
       subList_select (distinct $ from $ 
       \(o `InnerJoin` po) -> do 
        on $ o ^. OsId  ==. po ^. ProjectOsOId 
        where_ $ o ^. OsName ==. val (Just $ pack os') 
        return $ po ^. ProjectOsPId 
        ) 
      Nothing -> return() 
     case license of 
      Just license' -> 
      where_ $ p ^. ProjectId `in_` 
       subList_select (distinct $ from $ 
       \(l `InnerJoin` pl) -> do 
        on $ l ^. LicenseId  ==. pl ^. ProjectLicenseLId 
        where_ $ l ^. LicenseName ==. val (Just $ pack license') 
        return $ pl ^. ProjectLicensePId 
        ) 
      Nothing -> return() 
     -- ... 
     limit 50 
     return p 

Pour réduire la grande quantité de code dupliqués J'ai ensuite utilisé Template-Haskell:

gencheck t = code 
    where 
    tableId  = mkName $ t ++ "Id" 
    crosstableId = mkName $ "Project" ++ t ++ "XId" 
    crosstablePId = mkName $ "Project" ++ t ++ "PId" 
    tableName  = mkName $ t ++ "Name" 
    var   = mkName $ fmap toLower t 
    code = [| 
     case $(varE var) f of 
      Just _filter -> 
      where_ $ p ^. ProjectId `in_` 
       subList_select (distinct $ from $ 
       \(o `InnerJoin` po) -> do 
        on  $ o ^. $(conE tableId) ==. po ^. $(conE crosstableId) 
        where_ $ o ^. $(conE tableName) ==. val (Just _filter) 
        return $ po ^. $(conE crosstablePId) 
        ) 
      Nothing -> return() 
      |] 

runquery f = runDB 
    $ select $ distinct 
    $ from $ \p -> do 
     $(gencheck "Os") 
     $(gencheck "Cat") 
     $(gencheck "License") 
     $(gencheck "Coding") 
     $(gencheck "Gui") 
     limit 50 
     return p