Haskell中关系数据的安全建模
我发现在我的函数程序中建立关系数据模型是很常见的。例如,在开发网站时,我可能希望使用以下数据结构来存储有关用户的信息:Haskell中关系数据的安全建模,haskell,relational-database,type-safety,in-memory-database,Haskell,Relational Database,Type Safety,In Memory Database,我发现在我的函数程序中建立关系数据模型是很常见的。例如,在开发网站时,我可能希望使用以下数据结构来存储有关用户的信息: data User = User { name :: String , birthDate :: Date } 接下来,我想存储有关用户在我的网站上发布的消息的数据: data Message = Message { user :: User , timestamp :: Date , content :: String } 此数据结构存在多个
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
}
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) ]
user1 = User "John Doe" undefined
user2 = User "John Smith" undefined
messageSet =
foldr insert empty
[ Message user1 undefined "bla"
, Message user2 undefined "blu"
]
data User =
User
{ name :: String
, birthDate :: Date
, messages :: [Message]
} deriving (Ord, Typeable)
data Message =
Message
{ users :: [User]
, timestamp :: Date
, content :: String
} deriving (Ord, Typeable)
但是,您可以将数据形状设置为DAG(想象任意多对多关系),甚至可以设置为一般图形(好的,可能不是)。在这种情况下,我倾向于通过将数据存储在Map
s中来模拟关系数据库:
newtype Id a = Id Integer
type Table a = Map (Id a) a
这种方法可行,但由于多种原因不安全且丑陋:
- 您只是一个
构造函数调用,远离无意义的查找Id
- 在查找时,您会得到
可能是一个
,但通常数据库在结构上确保存在一个值
- 它很笨拙
- 很难确保数据的引用完整性
- 管理索引(这对于性能非常必要)并确保其完整性更加困难和笨拙李>
看起来Template Haskell可以解决这些问题(通常是这样),但我不想重新发明轮子。我没有一个完整的解决方案,但我建议看一下软件包;它提供了一个集合类型,其中包含任意数量的索引,可以使用这些索引执行查找。(它打算与一起使用以实现持久性。) 您仍然需要为每个表手动维护一个“主键”,但您可以通过以下几种方式使其变得更加简单:
Id
,以便,例如,用户
包含Id用户
,而不仅仅是Id
。这可确保您不会将Id
s与单独的类型混淆Id
类型抽象化,并提供一个安全的接口,以便在某些上下文中生成新的State
monad,它跟踪相关的IxSet
和当前最高的Id
)用户
,其中查询中需要Id用户
,并强制执行不变量(例如,如果每条消息
都持有有效用户
的密钥,则可以允许您查找相应的用户
,而无需处理可能
值;此帮助器函数中包含“不安全性”)关于
ixset
的一点是,它自动管理数据条目的“键”
例如,可以为您的数据类型创建一对多关系,如下所示:
data User = User
{ name :: String
, birthDate :: Date
, messages :: [(String, Date)] -- you get the idea
}
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) ]
user1 = User "John Doe" undefined
user2 = User "John Smith" undefined
messageSet =
foldr insert empty
[ Message user1 undefined "bla"
, Message user2 undefined "blu"
]
data User =
User
{ name :: String
, birthDate :: Date
, messages :: [Message]
} deriving (Ord, Typeable)
data Message =
Message
{ users :: [User]
, timestamp :: Date
, content :: String
} deriving (Ord, Typeable)
然后,您可以找到特定用户的消息。如果您建立了如下IxSet
:
data User = User
{ name :: String
, birthDate :: Date
, messages :: [(String, Date)] -- you get the idea
}
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) ]
user1 = User "John Doe" undefined
user2 = User "John Smith" undefined
messageSet =
foldr insert empty
[ Message user1 undefined "bla"
, Message user2 undefined "blu"
]
data User =
User
{ name :: String
, birthDate :: Date
, messages :: [Message]
} deriving (Ord, Typeable)
data Message =
Message
{ users :: [User]
, timestamp :: Date
, content :: String
} deriving (Ord, Typeable)
…然后,您可以通过user1
查找消息,方法是:
user1Messages = toList $ messageSet @= user1
如果您需要查找消息的用户,只需像正常情况一样使用user
功能。这将建立一对多关系模型
现在,对于多对多关系,在这样的情况下:
data User = User
{ name :: String
, birthDate :: Date
, messages :: [(String, Date)] -- you get the idea
}
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) ]
user1 = User "John Doe" undefined
user2 = User "John Smith" undefined
messageSet =
foldr insert empty
[ Message user1 undefined "bla"
, Message user2 undefined "blu"
]
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
字段):
创建一组这些关系将允许您通过消息和用户消息查询用户,而无需更新任何内容
考虑到它的功能,该库有一个非常简单的界面
编辑:关于您的“需要比较的昂贵数据”:ixset
仅比较您在索引中指定的字段(因此,要查找第一个示例中用户的所有消息,它会比较“整个用户”)
您可以通过更改Ord
实例来调整它所比较的索引字段的哪些部分。因此,如果比较用户对您来说代价高昂,您可以添加userId
字段,并修改实例Ord User
,以仅比较此字段,例如
这也可以用来解决鸡和蛋的问题:如果您有一个id,但既没有用户
,也没有消息
,该怎么办
然后,您可以简单地为id创建一个显式索引,通过该id找到用户(使用
userSet@=(12423::id)
),然后进行搜索