【问题标题】:Haskell, how to implement SQL like operations?Haskell,如何实现类似 SQL 的操作?
【发布时间】:2014-08-22 02:55:53
【问题描述】:

我正在尝试使用 Haskell 执行一些类似 SQL 的操作,但我不知道要使用什么数据结构。我有 3 个不同的表:客户、销售和订单。架构如下:

客户

  • custid — 整数(主键)
  • name — 字符串

例子:

1|Samson Bowman
2|Zelda Graves
3|Noah Hensley
4|Noelle Haynes
5|Paloma Deleon

销售

  • orderid — 整数(主键)
  • custid — 整数
  • date — 字符串

例子:

1|3|20/3/2014
2|4|25/4/2014
3|5|17/7/2014
4|9|5/1/2014
5|5|9/6/2014

订单

  • orderid — 整数
  • item — 字符串

例子:

2|gum
4|sandals
3|pen
1|gum
2|pen
3|chips
1|pop
5|chips

我要做的是将这三个表“合并”成一个新表,新表的架构是:

Customername    Order#     Date       Items
Samson Bowman   17      20/3/2014 shoes, socks, milk
Samson Bowman   34      19/5/2014 gum, sandals, butter, pens, pencils
Paloma Deleon   41      6/1/2014  computer
…

是的,它非常类似于 SQL。我知道 SQL 很简单,但是如果没有 SQL 而是使用内置数据结构,我该如何实现呢?

文本打印错误 当我运行该函数时,它显示以下错误:

Couldn't match type `[Char]' with `Char'
    Expected type: Customer -> String
      Actual type: Customer -> [String]
    In the first argument of `map', namely `formatCustomer'
    In the second argument of `($)', namely `map formatCustomer result'

我认为 condense 的返回类型是 [Customer],但 formatCustomer 仅使用 Customer。是这个原因吗?

【问题讨论】:

  • 我曾经写过这篇关于使用箭头进行类似 SQL 的列表处理的文章:groups.google.com/forum/#!msg/haskell-cafe/vLgYxvVP2mM/…
  • 但是map只对两个元素的元组起作用,在saleDB中,我们有三个元素,如何处理呢?谢谢
  • 我建议您阅读 Yesod 书中的 persistent 章节。
  • 它不是 SQL 应用程序。所有数据都存储在简单的 txt 文件中。所以我想用简单的数据结构来收集它们,让它变得简单:)

标签: list haskell vector


【解决方案1】:

你所有的关联都是一对多的,它们不相互引用;它是严格分层的。客户有销售,销售有订单。鉴于此,您可能不会单独存储每一位信息,而是按真实的分层存储。我可能会把它变成这样的数据类型:

data Customer = Customer { customerName :: String
                         , sales        :: [Sale]
                         } deriving (Eq, Read, Show)

data Sale = Sale { saleDate  :: Day
                 , soldItems :: [String]
                 } deriving (Eq, Read, Show)

这可能很容易在 Haskell 中进行操作,而且,作为奖励,它很容易变成您想要最终得到的表,因为它一开始就非常接近。


但也许我误解了你的问题,你不只是要求最好的数据结构来保存它,而是如何从你的平面数据结构转换成这种结构。幸运的是,这很容易。由于所有内容都已键入,因此我将构建一个 Map 并开始 unionWith 输入内容,或者更好的是,同时使用 fromListWith 进行这两项操作。更具体地说,假设您有以下数据结构:

data DBCustomer = DBCustomer { dbCustomerName :: String
                             , dbCustomerID   :: Int
                             } deriving (Eq, Read, Show)

data DBSale = DBSale { saleOrderID    :: Int
                     , saleCustomerID :: Int
                     , dbSaleDate     :: Day
                     } deriving (Eq, Read, Show)

data DBOrder = DBOrder { dbOrderID   :: Int
                       , dbOrderItem :: String
                       } deriving (Eq, Read, Show)

如果我想要一个 [DBSale] -> [DBOrder] -> [Sale] 类型的函数,我可以很容易地编写它:

condense :: [DBSale] -> [DBOrder] -> [Sale]
condense dbSales dbOrders = flip map dbSales $ \dbSale ->
    Sale (dbSaleDate dbSale)
       $ fromMaybe [] (Map.lookup (saleOrderID dbSale) ordersByID) where
  ordersByID = Map.fromListWith (++) . flip map dbOrders
             $ \dbOrder -> (dbOrderID dbOrder, [dbOrderItem dbOrder])

这里我放弃了客户 ID,因为 Sale 中没有插槽,但您当然可以放入另一个 Map 并取出整个 Customer 对象:

condense :: [DBCustomer] -> [DBSale] -> [DBOrder] -> [Customer]
condense dbCustomers dbSales dbOrders = flip map dbCustomers $ \dbCustomer ->
    Customer (dbCustomerName dbCustomer)
           $ lookupDef [] (dbCustomerID dbCustomer) salesByCustomerID where
  lookupDef :: (Ord k) => a -> k -> Map.Map k a -> a
  lookupDef def = (fromMaybe def .) . Map.lookup
  salesByCustomerID = Map.fromListWith (++) . flip map dbSales
                    $ \dbSale -> (saleCustomerID dbSale,
                                  [ Sale (dbSaleDate dbSale)
                                  $ lookupDef [] (saleOrderID dbSale)
                                              ordersByID])
  ordersByID = Map.fromListWith (++) . flip map dbOrders
             $ \dbOrder -> (dbOrderID dbOrder, [dbOrderItem dbOrder])

打印

这应该相当容易。我们将使用Text.Printf,因为它使将内容放在列中更容易。总的来说,结果中的每一行都是一个Sale。首先,我们可以尝试格式化单行:

formatSale :: Customer -> Sale -> String
formatSale customer sale = printf "%-16s%-8d%-10s%s"
                                  (customerName customer)
                                  (orderID sale)
                                  (show $ saleDate sale)
                                  (intercalate "," $ soldItems sale)

(实际上,我们丢弃了订单 ID;如果您想在输出中保留它,则必须将其添加到 Sale 数据结构中。)然后为每个客户获取行列表很容易:

formatCustomer :: Customer -> [String]
formatCustomer customer = map (formatSale customer) $ sales customer

如果customerscondense 的输出,则为所有客户执行此操作并打印出来:

putStr . unlines $ concatMap formatCustomer customers

【讨论】:

  • 谢谢!起初我没有注意到我可以创建自己的数据,但只是考虑使用 list of tuple 来做。它可能只适用于两个元素,但我的销售数据库有三个。你能提供更多细节吗?因为我想看看如何连接三个数据结构
  • @user3889372:我编辑了我的答案,以展示加入这三个人的样子。我注意到你的问题的 cmets 你说你这样做是为了数据持久性——如果你只是想快速存储东西而不需要与任何 特定 格式兼容,你可以使用@987654348 @ 和 show,如果您的数据类型派生 ReadShow。然后您的文件将看起来像 Haskell 文字。
  • 谢谢!我尝试了您的代码,但发生了一些小错误。它说“不在范围内:'销售'”,在这个地方: $ \dbSale -> (saleCustomerID sale, [ Sale (dbSaleDate dbSale) $lookupDef [] (saleOrderID dbSale) ordersByID]) 。我会在您回复之前尝试修复它。
  • 顺便说一句,我在 Google 上搜索“翻转地图”和 Map.Map,但只找到了 Map.map。这里的地图和地图有什么区别?我是 haskell 的新手,非常感谢您的帮助!
  • @user3889372: 很抱歉 sale 应该是 dbSale。在搜索 Haskell 文档时,您通常会发现 Hoogle 比 Google 更好。 Map 来自Data.Map 是一种将键“映射”到值的数据结构。 map 是一个转换列表中每个元素的函数。 flip 交换函数的参数。
【解决方案2】:

我有一些类似的问题,我发现执行 SQL Join 操作的最佳方法是使用 these 包中的 align 函数,结合 Map(其中关键是关于你想加入什么)。

align 的结果将为您提供These a b 的映射或列表,它可以是ab 或两者兼有。挺好看的。

【讨论】:

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