如何处理任意长度的元组来为 Haskell 的 postgresql-simple 的查询函数构建复杂的 SQL 查询?

问题描述 投票:0回答:1

https://hackage.haskell.org/package/postgresql-simple-0.7.0.0/docs/Database-PostgreSQL-Simple.html#v:query

query :: (ToRow q, FromRow r) => Connection -> Query -> q -> IO [r]

我在

query
方面面临的挑战是
q
参数,它可以以某种方式神奇地接受任何长度的元组(
(a,b)
(a,b,c,d,e,...)
等)。

我正在尝试实现一个构建复杂sql查询的函数,例如,基于

includeLarger = Just 123
,我们想将
where value => ?
添加到sql查询(参数化sql)中,但可能有多个这样的'过滤器。

对于 SQL 查询,我们可以轻松地将其附加到字符串中,然后使用

Query
将其转换为
fromString
。但是对于 postgresql-simple 参数化值,我不知道如何“构建”这个
q
参数。我无法将其设为列表,因为值可能属于不同类型。

query connection (fromString ("SELECT * FROM example WHERE " ++ f)) undefined

因此,如果我真的想使用这种方法,我必须跟踪需要为

q
传递多少个值,但这变得非常麻烦且容易出错,因为我认为它是组合增长的(请参见下面的示例代码) .

这只是

postgresql-simple
设计方式的一个不幸的限制还是有解决方案?

{-# LANGUAGE OverloadedStrings #-}

module Main where

import Database.PostgreSQL.Simple
import Data.String (fromString)

connectDB :: IO Connection
connectDB = connect defaultConnectInfo
  { connectUser = "postgres"
  , connectPassword = "example"
}

runQuery :: Connection -> Bool -> Bool -> Bool -> IO ()
runQuery conn useF1 useF2 useF3 = do
    let f1 = "id = ?"
    let f2 = "name = ?"
    let f3 = "age = ?"

    let v1 = 1 :: Int
    let v2 = "Chris" :: String
    let v3 = 30  :: Int

    if useF1 && not useF2 && not useF3
    then
        query conn (fromString $ "SELECT * FROM (VALUES (1, 'Chris', 25)) AS t (id, name, age) WHERE " ++ f1) (Only v1) >>= printResult
    else if useF1 && useF2 && not useF3
    then
        query conn (fromString $ "SELECT * FROM (VALUES (1, 'Chris', 25)) AS t (id, name, age) WHERE " ++ f1 ++ " AND " ++ f2) (v1, v2) >>= printResult
    else if useF1 && not useF2 && useF3
    then
        query conn (fromString $ "SELECT * FROM (VALUES (1, 'Chris', 25)) AS t (id, name, age) WHERE " ++ f1 ++ " AND " ++ f3) (v1, v3) >>= printResult
    else if not useF1 && useF2 && not useF3
    then
        query conn (fromString $ "SELECT * FROM (VALUES (1, 'Chris', 25)) AS t (id, name, age) WHERE " ++ f2) (Only v2) >>= printResult
    else if not useF1 && useF2 && useF3
    then
        query conn (fromString $ "SELECT * FROM (VALUES (1, 'Chris', 25)) AS t (id, name, age) WHERE " ++ f2 ++ " AND " ++ f3) (v2, v3) >>= printResult
    else if useF1 && useF2 && useF3
    then
        query conn (fromString $ "SELECT * FROM (VALUES (1, 'Chris', 25)) AS t (id, name, age) WHERE " ++ f1 ++ " AND " ++ f2 ++ " AND " ++ f3) (v1, v2, v3) >>= printResult
    else if not useF1 && not useF2 && useF3
    then
        query conn (fromString $ "SELECT * FROM (VALUES (1, 'Chris', 25)) AS t (id, name, age) WHERE " ++ f3) (Only v3) >>= printResult
    else
        putStrLn "No filters applied."

printResult :: [(Int, String, Int)] -> IO ()
printResult = mapM_ print

main :: IO ()
main = do
    conn <- connectDB
    putStrLn "Running query with f1 and f2..."
    runQuery conn True True False
    putStrLn "Running query with f1 and f3..."
    runQuery conn True False True
    putStrLn "Running query with f2 and f3..."
    runQuery conn False True True
    putStrLn "Running query with only f3..."
    runQuery conn False False True
    putStrLn "Running query with all filters..."
    runQuery conn True True True
    putStrLn "Running query with no filters..."
    runQuery conn False False False
haskell postgresql-simple
1个回答
0
投票

好吧,只是不要使用元组。如果您事先不知道需要多少元素,则使用错误的类型。

*-simple
库有一个用于此目的的
ToField a => ToRow [a]
实例。难题的另一部分是
toField
的结果本身有一个
ToField
实例,这意味着我们可以将这些实例的列表用于异构行。

这意味着您可以构建

name = ?
age = ?
等片段列表以及这些
toField user.name
toField user.age
等的参数,并将它们组合起来形成更大的查询。

这里是一个示例(使用 sqlite-simple 而不是 postgresql-simple,因为我在这台机器上没有安装 postgres,但 API 非常相似,所以唯一真正的区别是我所指的

SQLData
类型下面的代码片段在 postgresql-simple 中称为
Action
):

我们想要的是能够通过各种属性查找用户(暂时使用相等来保持简单):

 findUser db [ByAge 4]
 findUser db [ByName "Alpha", ByAge 4]
 findUser db [ById 1]

其中

findUser
是执行查询的函数,而
ByUser
等只是标识我们要查找的内容:

data FindUser = ByName String | ByAge Int | ById Int
findUser :: SQL.Connection -> [FindUser] -> IO [(Int, String, Int)]

为了将我们的

FindUser
变成可用的片段,我们定义一个助手:

toFragment (ByName n) = ("name = ?", SQL.toField n)
toFragment (ByAge a) = ("age = ?", SQL.toField a)
toFragment (ById i) = ("id = ?", SQL.toField i)

然后我们可以在另一个帮助器中使用它们将它们组合到实际的查询+参数列表中:

-- reminder: SQL.SQLData here is called SQL.Action in postgresql-simple!
findUserQ :: [FindUser] -> (SQL.Query, [SQL.SQLData])
findUserQ byAttrs = case map toFragment byAttrs of
  -- handle the edge case of not filtering for anything
  [] -> ("SELECT id, name, age FROM users", [])
  -- otherwise just AND our query fragments and remember the parameters
  fragments -> (fromString $ "SELECT id, name, age FROM users WHERE "
          <> intercalate " AND " (map fst fragments)
        , map snd fragments)

然后我们的

findUser
只是将查询应用于参数:

findUser :: SQL.Connection -> [FindUser] -> IO [(Int, String, Int)]
findUser db fu = uncurry (SQL.query db) $ findUserQ f

注意

当然,您可以相对轻松地将其扩展为更加灵活(例如允许不同类型的 where 条件、允许嵌套 AND/OR 组等)。这里值得注意的是,已经有像 esqueleto 这样的查询构建器库,它们或多或少地通过不同的权衡来实现这一点,例如更加类型安全或拥有更方便的 API,以换取更复杂/需要在特定的方式等等,所以如果您发现自己经常必须像这样动态构建查询,我鼓励您研究这些。

完整示例


{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where

import Control.Monad (forM_)
import Database.SQLite.Simple qualified as SQL
import Database.SQLite.Simple.ToField qualified as SQL
import Data.String (IsString(..))
import Data.List(intercalate)

data FindUser = ByName String | ByAge Int | ById Int

findUserQ :: [FindUser] -> (SQL.Query, [SQL.SQLData])
findUserQ byAttrs = case map toFragment byAttrs of
  -- handle the edge case of not filtering for anything
  [] -> ("SELECT id, name, age FROM users", [])
  -- otherwise just AND our query fragments and remember the parameters
  as -> (fromString $ "SELECT id, name, age FROM users WHERE "
          <> intercalate " AND " (map fst as)
        , map snd as)

findUser :: SQL.Connection -> [FindUser] -> IO [(Int, String, Int)]
findUser db fu = uncurry (SQL.query db) $ findUserQ fu

toFragment (ByName n) = ("name = ?", SQL.toField n)
toFragment (ByAge a) = ("age = ?", SQL.toField a)
toFragment (ById i) = ("id = ?", SQL.toField i)

main :: IO ()
main = do
  db <- SQL.open ":memory:"
  SQL.execute_ db "CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"
  forM_ [("Alpha", 9), ("Beta", 23), ("Alpha", 4), ("Beta", 4)] $
    SQL.execute @(String,Int) db "INSERT INTO users (name,age) VALUES (?,?)"
  findUser db [ByAge 4] >>= print
  findUser db [ByName "Alpha", ByAge 4] >>= print
  findUser db [ById 1] >>= print
© www.soinside.com 2019 - 2024. All rights reserved.