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)
    ),然后进行搜索