使用 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 到底是什么,而且我错误的谷歌搜索并没有让我更接近。

如何处理这种外键关系?


工作版本

感谢haoformayor

编辑了答案
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

对于某些记录类型 rEntity r 是包含 Key rr 的数据类型。你可以把它想象成一个拼凑而成的元组 (Key r, r)

(你可能想知道 Key r 是什么。不同的后端有不同种类的 Key r。对于 Postgres 它将是一个 64 位整数。对于 MongoDB 有对象IDs。documentation goes into more detail。它是允许 Persistent 支持多个数据存储的抽象。)

你的问题是你有一个Key User。我们的策略是为您提供 Entity User,我们可以从中提取 User。幸运的是,通过 selectFirstKey UserEntity User 很容易——访问数据库。从 Entity UserUser 是一种模式匹配。

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 UserInt64 或其他任何内容。这可能是我要做的——如果我想发送文章列表,我不想包含每篇文章的用户信息!

如果您想将其保留为实际的用户对象,那么我们需要将查询从 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)

作为更安全的选择。