Представление взаимосвязи внешнего ключа в JSON с использованием Servant и Persistent
Этим утром я следовал вместе с этим интересным руководством по использованию 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
|]
Учебник строит отдельный Person
тип данных для Servant API, поэтому я добавил один 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
на самом деле, и мое заблудшее приближение к Google не приблизило меня.
Как мне справиться с этими отношениями с внешними ключами?
Рабочая версия
Отредактировано с ответом благодаря hao
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
2 ответа
Для некоторого типа записи r
, Entity r
это тип данных, содержащий Key r
а также r
, Вы можете думать об этом как о наброшенном кортеже (Key r, r)
,
(Вы можете спросить, что Key r
является. Разные бэкэнды имеют разные виды Key r
, Для Postgres это будет 64-разрядное целое число. Для MongoDB существуют идентификаторы объектов. Документация идет более подробно. Это абстракция, которая позволяет Persistent поддерживать несколько хранилищ данных.)
Ваша проблема здесь в том, что у вас есть Key User
, Наша стратегия будет заключаться в том, чтобы Entity User
из которого мы сможем вытащить User
, К счастью, из Key User
в Entity User
легко с selectFirst
- поездка в базу данных. И собирается из 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, особенно если в модели базы данных есть вещи, которые вы не отправляете по проводам. Я не хотел, чтобы пароли были включены в Users, поэтому я сделал тип данных Person.
Книга Йесод имеет хорошее объяснение Entity
вещи здесь
Если вы просто хотите получить один элемент, и у вас есть ключ для него, то классные пиксы типа Persistent сообщают нам о 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)
как более безопасный вариант.