3

今天早上我跟着这个有趣的教程学习如何使用 Servant 构建一个简单的 API 服务器。

在教程的最后,作者建议添加一个博客类型,所以我想我会试一试,但是我在尝试实现和序列化扩展教程中逻辑的外键关系时遇到了困难(也许是一个重要的此处披露:我对仆人和持久性都是新手)。

这是我的持久定义(我添加了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 }

但是,现在,当我尝试创建一个将 aPost转换为的函数时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

最终,我不太确定 a 到底是什么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
4

2 回答 2

4

对于某些记录类型rEntity r是包含Key rand的数据类型r。你可以把它想象成一个 tuple (Key r, r)

(您可能想知道Key r是什么。不同的后端有不同的类型Key r。对于 Postgres,它将是一个 64 位整数。对于 MongoDB,有对象 ID。文档更详细。这是一个允许 Persistent 支持多个数据存储的抽象.)

你的问题是你有一个Key User. 我们的策略是给你一个Entity User,我们可以从中取出一个User。幸运的是,从数据库Key User到数据库Entity User很容易selectFirst。从Entity UsertoUser是一种模式匹配。

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 后端,您可能需要它。

于 2016-02-28T00:16:31.180 回答
2

您实际上不需要为每个模型创建单独的数据类型。解耦数据库模型和 API 模型会很有帮助,尤其是当数据库模型包含您不通过网络发送的内容时。我不希望用户中包含密码,所以我创建了 Person 数据类型。

Yesod 书对这里Entity的东西有很好的解释。

如果您只想获得一个项目并且您有一个密钥,那么Persistent 类型类 haddocks会告诉我们一个get可以做到这一点的方法。

因此,如果您确实想制作Article类型,那么有几个选项。您可以将其更改articleUserKey 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)

作为更安全的选择。

于 2016-02-28T20:28:56.357 回答