Представление взаимосвязи внешнего ключа в 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)

как более безопасный вариант.

Другие вопросы по тегам