【问题标题】:Dynamically build SQL-Queries wit Esqueleto and Template Haskell?使用 Esqueleto 和 Template Haskell 动态构建 SQL 查询?
【发布时间】:2018-02-15 06:08:01
【问题描述】:

我正在编写一个带有 Yesod 和 Persistent 的 webapp。我有一个带有多个表的 SQL 数据库,其中包含我的“项目”的特征。我有一个主表和带有多个值的选项的附加表与 id 链接。

用户应该能够选择他想要过滤的那些特征并指定过滤器值。如果用户过滤操作系统,SQL-Query 的许可证和编码将如下所示:

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

但我不想总是加入所有表,因为当有很多表但用户只过滤少数几个时,这对性能非常不利。但我也不想为每个可查询功能的组合编写一个查询,因为这意味着要编写 N² 个大部分相同的查询。

“on”和“where”子句可以根据我们是否要过滤而动态完成。但是连接在 Lambda 函数的参数范围内。我发现没有办法建立这个依赖于外部变量。

所以我认为 Template Haskell 可能会成功……我开始学习 TH 并将查询的核心转换为 TH。但是现在我被困住了,不确定 TH 是否可以帮助我以及它是否是正确的方法?

以下是我在使用 Template 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) |]

所以我想请你帮忙:

  • 我可以/应该用 Template Haskell 做这个吗?
  • 如果是这样:如何使用参数调用函数 foo?
  • 如果不是:正确的解决方案是什么?

【问题讨论】:

  • 如果你打算用 TH 来做这件事,那么你不需要为整个 runquery 函数编写一个单独的 TH 函数 - 你可以只用一个 TH 函数来产生一个模式.所以你可以有类似from $ \ $(mkInnerJoin ["l", "o", "c"]) -> ... 的东西。 TH 可能不是做到这一点的最佳方式(这是做一些事情的最佳方式......);您可以先定义一个数据类型来表示查询的结构。

标签: haskell template-haskell esqueleto haskell-persistent


【解决方案1】:

所以我发现在我的情况下使用子查询比连接快得多,如果需要,您可以这样做:

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

为了减少大量重复代码,我使用了 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-26
    • 2018-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多