Represent Foreign Key Relationship in JSON using Servant and Persistent

522 Views Asked by At

This morning I was following along with this interesting tutorial on using Servant to build a simple API server.

At the end of the tutorial, the author suggests adding a Blog type, so I figured I would give it a shot, but I got stuck trying to implement and serialize a foreign key relationship that extends upon the logic in the tutorial (perhaps an important disclosure here: I'm new to both Servant and Persistent).

Here are my Persistent definitions (I added the 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
|]

The tutorial builds a separate Person data type for the Servant API, so I added one called Article as well:

-- 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 }

Now, however, when I attempt to create a function that turns a Post into an Article, I get stuck trying to deal with the User foreign key:

postToArticle :: Post -> Article
postToArticle Post{..} = Article {
  title = postTitle
  , author = userToPerson postUser -- this fails
  , summary = postSummary
  , content = postContent
  }

I tried a number of things, but the above seemed to be close to the direction I'd like to go in. It doesn't compile, however, due to the following the error:

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

Ultimately, I'm not really sure what a PersistEntity.Key User really is and my errant googling has not gotten me much closer.

How do I deal with this foreign-key relationship?


Working Version

Edited with an answer thanks to 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
2

There are 2 best solutions below

4
On BEST ANSWER

For some record type r, Entity r is the datatype containing Key r and r. You can think of it as a dollied-up tuple (Key r, r).

(You might wonder what Key r is. Different backends have different kinds of Key r. For Postgres it'll be a 64-bit integer. For MongoDB there are object IDs. The documentation goes into more detail. It's an abstraction that allows Persistent to support multiple datastores.)

Your problem here is that you have a Key User. Our strategy will be to get you an Entity User, from which we'll be able to pull out a User. Fortunately, going from Key User to Entity User is easy with a selectFirst – a trip to the database. And going from Entity User to User is one pattern match.

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

Gross, more generic version

We assumed a SQL backend above, but that function also has the more generic type

postToArticle ::
  (MonadIO m, PersistEntity val, backend ~ PersistEntityBackend val) =>
  Post -> ReaderT backend m (Maybe Article)

Which you might need if you're not using a SQL backend.

1
On

You don't really need to create a separate data type for each model. It can be helpful to decouple the database model and the API model, especially if the database model has stuff you don't sending over the wire. I didn't want passwords to be included with Users, so I made the Person data type.

The Yesod book has a good explanation of the Entity stuff here.

If you just want to get a single item and you have a key for it, the Persistent type class haddocks tell us about a get method that does precisely that.

So, if you do want to make the Article type, then there are a few options. You could change the articleUser to be a Key User or Int64 or whatever. This is probably what I'd do -- if I wanted to send a list of articles, I wouldn't want to include the user information for each one!

If you want to keep it as an actual user object, then we'll want to extract the query out of the postToArticle function. Ideally that should be a pure function: postToArticle :: Post -> Article. We can do that by also passing in the Person:

postToArticle :: Person -> Post -> Article
postToArticle person Post{..} = Article
    { ...
    }

Of course, this function can't verify that you passed in the right person. You could do:

postToArticle' :: Entity User -> Post -> Maybe Article
postToArticle' (Entity userKey user) post
    | userKey /= postUser post =
        Nothing
    | otherwise =
        Just (postToArticle (userToPerson user) post)

as a safer option.