我觉得想要在我的功能程序中建模关系数据是很常见的。例如,在开发网站时,我可能希望使用以下数据结构来存储有关我的用户的信息:
data User = User
{ name :: String
, birthDate :: Date
}
接下来,我想存储有关用户在我网站上发布的消息的数据:
data Message = Message
{ user :: User
, timestamp :: Date
, content :: String
}
此数据结构存在多个问题:
User
字段的更新很脆弱 - 您可能忘记更新数据结构中User
的所有出现。这些问题是可管理的,而我们的数据可以表示为树。例如,您可以像这样重构:
data User = User
{ name :: String
, birthDate :: Date
, messages :: [(String, Date)] -- you get the idea
}
然而,可以将您的数据整形为DAG(想象任何多对多关系),甚至可以作为一般图形(好的,也许不是)。在这种情况下,我倾向于通过将我的数据存储在Map
s:
newtype Id a = Id Integer
type Table a = Map (Id a) a
这种作品,但由于多种原因而不安全和丑陋:
Id
构造函数,可以远离无意义的查找。Maybe a
,但数据库通常会在结构上确保存在值。是否有克服这些问题的工作?
看起来模板Haskell可以解决它们(就像通常那样),但我不想重新发明轮子。
答案 0 :(得分:25)
ixset
库可以帮助您解决此问题。它是支持acid-state
的关系部分的库,它还处理数据和/或并发保证的版本化序列化,以备不时之需。
关于ixset
的事情是它自动管理数据条目的“键”。
对于您的示例,可以为您的数据类型创建一对多关系,如下所示:
data User =
User
{ name :: String
, birthDate :: Date
} deriving (Ord, Typeable)
data Message =
Message
{ user :: User
, timestamp :: Date
, content :: String
} deriving (Ord, Typeable)
instance Indexable Message where
empty = ixSet [ ixGen (Proxy :: Proxy User) ]
然后,您可以找到特定用户的消息。如果您已经构建了IxSet
,请执行以下操作:
user1 = User "John Doe" undefined
user2 = User "John Smith" undefined
messageSet =
foldr insert empty
[ Message user1 undefined "bla"
, Message user2 undefined "blu"
]
...然后您可以通过user1
找到消息:
user1Messages = toList $ messageSet @= user1
如果您需要查找消息的用户,只需像正常一样使用user
功能。这模拟了一对多的关系。
现在,对于多对多关系,情况如下:
data User =
User
{ name :: String
, birthDate :: Date
, messages :: [Message]
} deriving (Ord, Typeable)
data Message =
Message
{ users :: [User]
, timestamp :: Date
, content :: String
} deriving (Ord, Typeable)
...您使用ixFun
创建索引,该索引可以与索引列表一起使用。像这样:
instance Indexable Message where
empty = ixSet [ ixFun users ]
instance Indexable User where
empty = ixSet [ ixFun messages ]
要查找用户的所有消息,您仍然使用相同的功能:
user1Messages = toList $ messageSet @= user1
此外,只要您拥有用户索引:
userSet =
foldr insert empty
[ User "John Doe" undefined [ messageFoo, messageBar ]
, User "John Smith" undefined [ messageBar ]
]
...您可以找到所有用户的消息:
messageFooUsers = toList $ userSet @= messageFoo
如果您不想在添加新用户/消息时更新消息的用户或用户的消息,您应该创建一个中间数据类型来模拟用户和消息之间的关系,就像在SQL中一样(并删除users
和messages
字段):
data UserMessage = UserMessage { umUser :: User, umMessage :: Message }
instance Indexable UserMessage where
empty = ixSet [ ixGen (Proxy :: Proxy User), ixGen (Proxy :: Proxy Message) ]
创建一组这些关系可以让您通过消息和消息为用户查询用户,而无需更新任何内容。
该库有一个非常简单的界面,考虑它的作用!
编辑:关于“需要比较的代价高昂的数据”:ixset
仅比较您在索引中指定的字段(以便查找用户的所有消息)第一个例子,它比较“整个用户”)。
通过更改Ord
实例来规范它所比较的索引字段的哪些部分。因此,如果比较用户对您而言代价很高,则可以添加userId
字段并修改instance Ord User
以仅比较此字段。
这也可以用来解决鸡与蛋的问题:如果你有一个id,但是User
和Message
都没有?
然后您可以简单地为id创建一个显式索引,按该id找到用户(使用userSet @= (12423 :: Id)
),然后进行搜索。
答案 1 :(得分:7)
IxSet是门票。为了帮助那些可能偶然发现这篇文章的人,这是一个更充分表达的例子,
{-# LANGUAGE OverloadedStrings, DeriveDataTypeable, TypeFamilies, TemplateHaskell #-}
module Main (main) where
import Data.Int
import Data.Data
import Data.IxSet
import Data.Typeable
-- use newtype for everything on which you want to query;
-- IxSet only distinguishes indexes by type
data User = User
{ userId :: UserId
, userName :: UserName }
deriving (Eq, Typeable, Show, Data)
newtype UserId = UserId Int64
deriving (Eq, Ord, Typeable, Show, Data)
newtype UserName = UserName String
deriving (Eq, Ord, Typeable, Show, Data)
-- define the indexes, each of a distinct type
instance Indexable User where
empty = ixSet
[ ixFun $ \ u -> [userId u]
, ixFun $ \ u -> [userName u]
]
-- this effectively defines userId as the PK
instance Ord User where
compare p q = compare (userId p) (userId q)
-- make a user set
userSet :: IxSet User
userSet = foldr insert empty $ fmap (\ (i,n) -> User (UserId i) (UserName n)) $
zip [1..] ["Bob", "Carol", "Ted", "Alice"]
main :: IO ()
main = do
-- Here, it's obvious why IxSet needs distinct types.
showMe "user 1" $ userSet @= (UserId 1)
showMe "user Carol" $ userSet @= (UserName "Carol")
showMe "users with ids > 2" $ userSet @> (UserId 2)
where
showMe :: (Show a, Ord a) => String -> IxSet a -> IO ()
showMe msg items = do
putStr $ "-- " ++ msg
let xs = toList items
putStrLn $ " [" ++ (show $ length xs) ++ "]"
sequence_ $ fmap (putStrLn . show) xs
答案 2 :(得分:5)
数据库包haskelldb使用另一种完全不同的表示关系数据的方法。它不像您在示例中描述的类型那样工作,但它旨在允许SQL查询的类型安全接口。它具有从数据库模式生成数据类型的工具,反之亦然。如果您总是希望使用整行,那么您描述的数据类型可以很好地工作。但是,如果您只想选择某些列来优化查询,则它们不起作用。这是HaskellDB方法有用的地方。
答案 3 :(得分:5)
我被要求用Opaleye写一个答案。事实上,没有太多可说的,因为一旦你有了数据库模式,Opaleye代码就相当标准。无论如何,这里假设有user_table
列user_id
,name
和birthdate
以及message_table
列user_id
, time_stamp
和content
。
这种设计在the Opaleye Basic Tutorial中有更详细的解释。
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE Arrows #-}
import Opaleye
import Data.Profunctor.Product (p2, p3)
import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
import Control.Arrow (returnA)
data UserId a = UserId { unUserId :: a }
$(makeAdaptorAndInstance "pUserId" ''UserId)
data User' a b c = User { userId :: a
, name :: b
, birthDate :: c }
$(makeAdaptorAndInstance "pUser" ''User')
type User = User' (UserId (Column PGInt4))
(Column PGText)
(Column PGDate)
data Message' a b c = Message { user :: a
, timestamp :: b
, content :: c }
$(makeAdaptorAndInstance "pMessage" ''Message')
type Message = Message' (UserId (Column PGInt4))
(Column PGDate)
(Column PGText)
userTable :: Table User User
userTable = Table "user_table" (pUser User
{ userId = pUserId (UserId (required "user_id"))
, name = required "name"
, birthDate = required "birthdate" })
messageTable :: Table Message Message
messageTable = Table "message_table" (pMessage Message
{ user = pUserId (UserId (required "user_id"))
, timestamp = required "timestamp"
, content = required "content" })
将用户表连接到user_id
字段上的消息表的示例查询:
usersJoinMessages :: Query (User, Message)
usersJoinMessages = proc () -> do
aUser <- queryTable userTable -< ()
aMessage <- queryTable messageTable -< ()
restrict -< unUserId (userId aUser) .== unUserId (user aMessage)
returnA -< (aUser, aMessage)
答案 4 :(得分:3)
我没有完整的解决方案,但我建议您查看ixset包。它提供了一个set类型,其中包含可以执行查找的任意数量的索引。 (它旨在与acid-state一起用于持久性。)
您仍然需要为每个表手动维护“主键”,但您可以通过以下几种方式使其更加轻松:
将类型参数添加到Id
,例如,User
包含Id User
而不仅仅是Id
。这样可以确保您不会将Id
混合为不同的类型。
制作Id
类型的摘要,并提供一个安全的界面来在某些上下文中生成新的(如State
monad,跟踪相关的IxSet
和当前最高Id
)。
编写包装函数,例如,您可以提供User
,其中查询中需要Id User
,并强制执行不变量(例如,每Message
{}持有有效User
的密钥,它可以让您在不处理User
值的情况下查找相应的Maybe
;此辅助函数中包含“不安全”。) p>
作为补充说明,您实际上不需要树结构来使常规数据类型起作用,因为它们可以表示任意图形;但是,这使得更新用户名称等简单操作变得不可能。