使用 Servant 和 Persistent 表示 JSON 中的外键关系
Represent Foreign Key Relationship in JSON using Servant and Persistent
今天早上我跟随 this interesting tutorial 使用 Servant 构建一个简单的 API 服务器。
在教程的最后,作者建议添加一个 Blog 类型,所以我想我会试一试,但我在尝试实现和序列化扩展到教程(也许这里有一个重要的披露:我是 Servant 和 Persistent 的新手)。
这是我的 Persistent 定义(我添加了 Post
):
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
name String
email String
deriving Show
Post
title String
user UserId
summary String
content String
deriving Show
|]
本教程为 Servant API 构建了一个单独的 Person
数据类型,因此我也添加了一个名为 Article
的数据类型:
-- API Data Types
data Person = Person
{ name :: String
, email :: String
} deriving (Eq, Show, Generic)
data Article = Article
{ title :: String
, author :: Person
, summary :: String
, content :: String
} deriving (Eq, Show, Generic)
instance ToJSON Person
instance FromJSON Person
instance ToJSON Article
instance FromJSON Article
userToPerson :: User -> Person
userToPerson User{..} = Person { name = userName, email = userEmail }
然而,现在,当我尝试创建一个将 Post
转换为 Article
的函数时,我在尝试处理 User
外键时遇到了困难:
postToArticle :: Post -> Article
postToArticle Post{..} = Article {
title = postTitle
, author = userToPerson postUser -- this fails
, summary = postSummary
, content = postContent
}
我尝试了很多东西,但上面的似乎接近我想进入的方向。它没有编译,但是,由于以下错误:
Couldn't match expected type ‘User’
with actual type ‘persistent-2.2.2:Database.Persist.Class.PersistEntity.Key
User’
In the first argument of ‘userToPerson’, namely ‘postUser’
In the ‘author’ field of a record
最终,我不太确定 PersistEntity.Key User
到底是什么,而且我错误的谷歌搜索并没有让我更接近。
如何处理这种外键关系?
工作版本
编辑了答案
postToArticle :: MonadIO m => Post -> SqlPersistT m (Maybe Article)
postToArticle Post{..} = do
authorMaybe <- selectFirst [UserId ==. postUser] []
return $ case authorMaybe of
Just (Entity _ author) ->
Just Article {
title = postTitle
, author = userToPerson author
, summary = postSummary
, content = postContent
}
Nothing ->
Nothing
对于某些记录类型 r
,Entity r
是包含 Key r
和 r
的数据类型。你可以把它想象成一个拼凑而成的元组 (Key r, r)
。
(你可能想知道 Key r
是什么。不同的后端有不同种类的 Key r
。对于 Postgres 它将是一个 64 位整数。对于 MongoDB 有对象IDs。documentation goes into more detail。它是允许 Persistent 支持多个数据存储的抽象。)
你的问题是你有一个Key User
。我们的策略是为您提供 Entity User
,我们可以从中提取 User
。幸运的是,通过 selectFirst
从 Key User
到 Entity User
很容易——访问数据库。从 Entity User
到 User
是一种模式匹配。
postToArticle :: MonadIO m => Post -> SqlPersistT m (Maybe Article)
postToArticle Post{..} = do
authorMaybe <- selectFirst [UserId ==. postUser] []
return $ case authorMaybe of
Just (Entity _ author) ->
Article {
title = postTitle
, author = author
, summary = postSummary
, content = postContent
}
Nothing ->
Nothing
恶心,更通用的版本
我们假设上面有一个 SQL 后端,但该函数也有更通用的类型
postToArticle ::
(MonadIO m, PersistEntity val, backend ~ PersistEntityBackend val) =>
Post -> ReaderT backend m (Maybe Article)
如果您不使用 SQL 后端,您可能需要它。
您真的不需要为每个模型创建单独的数据类型。将数据库模型和 API 模型分离会很有帮助,尤其是当数据库模型包含您不通过网络发送的内容时。我不希望用户包含密码,所以我创建了 Person 数据类型。
Yesod 书对 Entity
内容有很好的解释 here。
如果您只想获得一个项目并且您有一个密钥,Persistent type class haddocks 告诉我们一个 get
方法可以做到这一点。
因此,如果您确实想要制作 Article
类型,那么有几个选项。您可以将 articleUser
更改为 Key User
或 Int64
或其他任何内容。这可能是我要做的——如果我想发送文章列表,我不想包含每篇文章的用户信息!
如果您想将其保留为实际的用户对象,那么我们需要将查询从 postToArticle
函数中提取出来。理想情况下,它应该是一个纯函数:postToArticle :: Post -> Article
。我们也可以传入 Person
:
postToArticle :: Person -> Post -> Article
postToArticle person Post{..} = Article
{ ...
}
当然,这个函数无法验证你传入的是对的人。你可以这样做:
postToArticle' :: Entity User -> Post -> Maybe Article
postToArticle' (Entity userKey user) post
| userKey /= postUser post =
Nothing
| otherwise =
Just (postToArticle (userToPerson user) post)
作为更安全的选择。
今天早上我跟随 this interesting tutorial 使用 Servant 构建一个简单的 API 服务器。
在教程的最后,作者建议添加一个 Blog 类型,所以我想我会试一试,但我在尝试实现和序列化扩展到教程(也许这里有一个重要的披露:我是 Servant 和 Persistent 的新手)。
这是我的 Persistent 定义(我添加了 Post
):
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
name String
email String
deriving Show
Post
title String
user UserId
summary String
content String
deriving Show
|]
本教程为 Servant API 构建了一个单独的 Person
数据类型,因此我也添加了一个名为 Article
的数据类型:
-- API Data Types
data Person = Person
{ name :: String
, email :: String
} deriving (Eq, Show, Generic)
data Article = Article
{ title :: String
, author :: Person
, summary :: String
, content :: String
} deriving (Eq, Show, Generic)
instance ToJSON Person
instance FromJSON Person
instance ToJSON Article
instance FromJSON Article
userToPerson :: User -> Person
userToPerson User{..} = Person { name = userName, email = userEmail }
然而,现在,当我尝试创建一个将 Post
转换为 Article
的函数时,我在尝试处理 User
外键时遇到了困难:
postToArticle :: Post -> Article
postToArticle Post{..} = Article {
title = postTitle
, author = userToPerson postUser -- this fails
, summary = postSummary
, content = postContent
}
我尝试了很多东西,但上面的似乎接近我想进入的方向。它没有编译,但是,由于以下错误:
Couldn't match expected type ‘User’
with actual type ‘persistent-2.2.2:Database.Persist.Class.PersistEntity.Key
User’
In the first argument of ‘userToPerson’, namely ‘postUser’
In the ‘author’ field of a record
最终,我不太确定 PersistEntity.Key User
到底是什么,而且我错误的谷歌搜索并没有让我更接近。
如何处理这种外键关系?
工作版本
编辑了答案postToArticle :: MonadIO m => Post -> SqlPersistT m (Maybe Article)
postToArticle Post{..} = do
authorMaybe <- selectFirst [UserId ==. postUser] []
return $ case authorMaybe of
Just (Entity _ author) ->
Just Article {
title = postTitle
, author = userToPerson author
, summary = postSummary
, content = postContent
}
Nothing ->
Nothing
对于某些记录类型 r
,Entity r
是包含 Key r
和 r
的数据类型。你可以把它想象成一个拼凑而成的元组 (Key r, r)
。
(你可能想知道 Key r
是什么。不同的后端有不同种类的 Key r
。对于 Postgres 它将是一个 64 位整数。对于 MongoDB 有对象IDs。documentation goes into more detail。它是允许 Persistent 支持多个数据存储的抽象。)
你的问题是你有一个Key User
。我们的策略是为您提供 Entity User
,我们可以从中提取 User
。幸运的是,通过 selectFirst
从 Key User
到 Entity User
很容易——访问数据库。从 Entity User
到 User
是一种模式匹配。
postToArticle :: MonadIO m => Post -> SqlPersistT m (Maybe Article)
postToArticle Post{..} = do
authorMaybe <- selectFirst [UserId ==. postUser] []
return $ case authorMaybe of
Just (Entity _ author) ->
Article {
title = postTitle
, author = author
, summary = postSummary
, content = postContent
}
Nothing ->
Nothing
恶心,更通用的版本
我们假设上面有一个 SQL 后端,但该函数也有更通用的类型
postToArticle ::
(MonadIO m, PersistEntity val, backend ~ PersistEntityBackend val) =>
Post -> ReaderT backend m (Maybe Article)
如果您不使用 SQL 后端,您可能需要它。
您真的不需要为每个模型创建单独的数据类型。将数据库模型和 API 模型分离会很有帮助,尤其是当数据库模型包含您不通过网络发送的内容时。我不希望用户包含密码,所以我创建了 Person 数据类型。
Yesod 书对 Entity
内容有很好的解释 here。
如果您只想获得一个项目并且您有一个密钥,Persistent type class haddocks 告诉我们一个 get
方法可以做到这一点。
因此,如果您确实想要制作 Article
类型,那么有几个选项。您可以将 articleUser
更改为 Key User
或 Int64
或其他任何内容。这可能是我要做的——如果我想发送文章列表,我不想包含每篇文章的用户信息!
如果您想将其保留为实际的用户对象,那么我们需要将查询从 postToArticle
函数中提取出来。理想情况下,它应该是一个纯函数:postToArticle :: Post -> Article
。我们也可以传入 Person
:
postToArticle :: Person -> Post -> Article
postToArticle person Post{..} = Article
{ ...
}
当然,这个函数无法验证你传入的是对的人。你可以这样做:
postToArticle' :: Entity User -> Post -> Maybe Article
postToArticle' (Entity userKey user) post
| userKey /= postUser post =
Nothing
| otherwise =
Just (postToArticle (userToPerson user) post)
作为更安全的选择。