Relations

Relations define how entities are connected with one another. You probably encountered those while working with databases. In GraphQL (and Sangria) relations are strictly connected with deferred resolvers and have a similar role. When you want to find related entities, the query can be optimized and all needed data fetched at once.

In other words: Relations expand Fetchers, allows for finding entities not only by their id field, but also by ids quite often stored in fields of another entity.

Lets try to define how many relations we have in our schema.

User has links and votes fields.

Link has postedBy and votes fields.

Vote has user and link

How do those relations work? Link is a main entity, first created by us and the most important. Link is added by a (single) user. On the other hand, a user can have more than one link.

User also can vote for a link. He can vote for a single link once, but a link can have more than one votes.

So, in our app we have 3 one-to-many relations.

Preparing database

First change slightly Link model:

Update LinksTable.

Add foreign keys.

Votes model already has proper fields for storing external ids, we only have to add foreign keys in database setup.

Because domain models has slightly changed, we also have to redefine our data.

I think we’re done with the Database part of changes. The following code represents the current state of DBSchema file:

DBSchema.scala

Now we can go and do the GraphQL part of changes.

Defining User->Link relation

Let’s begin with User-Link relation. In the first entity we have to add the field links and in the second the field postedBy. Both fields uses the same Relation model.

Actually a Link entity has to have two defined relations. First because we can lookup the database to find a link with a particular Id, Second, when we want to filter links by user ids stored in postedBy column. Our Fetcher accepts the provided id already, so we have what covers the first case but we still have to define the second one:

This relation is of type SimpleRelation and has only two arguments: the first is the name, the second is a function which extracts a sequence of user ids from the link entity. Our case is super easy, because postedBy has such id. All we need to do is wrap it into the sequence.

Now we have to add this relation to the fetcher. To do this, we have to use Fetcher.rel function instead of the previously used apply

What do we have here? As I mentioned above, now we’re using .rel function. It needs the second function to be passed as the argument. This function is for fetching related data from a datasource. In our case it uses a function getLinksByUserIds that we have to add to our dao. ids(linkByUserRel) extracts user ids by the defined in relation way and passes it into the DAO function.

Actually we’ve simplified the code above a little. When you look into the part ctx.dao.getLinksByUserIds(ids(linkByUserRel)) a bit, you can wonder “And what if link has two relations? Could getLinkByUserIds be replaced by another function?” Be patient, such case will be covered later in this chapter. In our case we have only one relation, so we can retrieve all userId’s by calling ids(linkByUserRel) functions.

Add fields to GraphQL Objects

Let’s begin with LinkType. Link already has a postedBy field, but for now it’s only an Int and we need the entire user. To achieve this we have to replace the entire field definition and instruct resolver to use already defined fetcher to do this.

In similar way we will change the UserType but User entity hasn’t links property so we have to add such field manually to the ObjectType. AddField type class is for such reason:

Now you can see that another fetcher function is being called. All .deferRel... functions needs two arguments instead of one. We have to add the relation object as the first argument, the second is a function which will get a mapping value from entity.

We just added two relations to both User and Link object types. If you have tried to run this, you have probably experienced some issues. It’s because now we have a circular reference in the Object type declaration. There are two things we have to do to avoid this issue:

Now open the graphiql console in browser and try to execute this query: (tip: if the autocomplete doesn’t work for the new fields, try to refresh a page)

query {
  link(id: 1){
    id
    url
    createdAt
    postedBy {
      name
      links {
        id
        url
      }
    }
  }
}

As you can see, both relations work perfectly.

Time to add the rest of them.

Vote - User Relation

Before I go further, try to do it yourself. All steps you need to do, are similar to the those we have already done.

To be honest, half of work you have already done :D There is userId field in the Vote model. Database is also prepared, there is not much work to do here.

Ok. Let’s begin from proper database function.

The rest of the changes will be applied in the GraphQLSchema file.

Don’t forget in Relation we always have to return a sequence! Also we have to change the fetcher definition.

Change UserType:

Also modify the defined VoteType:

That’s all. After this changes you should be able to execute the query like this:

query {
  link(id: 1){
    id
    url
    createdAt
    postedBy {
      name
      links {
        id
        url
      }
      votes {
        id
        user {
          name
        }
      }
    }
  }
}

As you can see we can ask for users who vote for links posted by the author of the current link. Simple like that.

Vote - Link Relation

One relation is still missing in our example. In my opinion you have enough knowledge to try and write it yourself. After that I’ll do it step by step. Reminder: case classes and database setup support this relation, you do not need to change anything there.

Lets start from defining relation object:

Now we can add the votes field to the LinkType.

You see the similarities between both votes fields, don’t you?

//UserType
Field("votes", ListType(VoteType), resolve = c => votesFetcher.deferRelSeq(voteByUserRel, c.value.id))

//LinkType
Field("votes", ListType(VoteType), resolve = c => votesFetcher.deferRelSeq(voteByLinkRel, c.value.id))

Both are almost the same, the only difference is the type of Relation we’re using as the first argument. Actually in this way you can add any relation you want.

Now you should be able to query for this field.


The second part won’t be as easy.

Please look at the existing votesFetcher definition:

val votesFetcher = Fetcher.rel(
    (ctx: MyContext, ids: Seq[Int]) => ctx.dao.getVotes(ids),
    (ctx: MyContext, ids: RelationIds[Vote]) => ctx.dao.getVotesByUserIds(ids(voteByUserRel))
)

The first function fetches votes by their id. Nothing to comment here. The second function, on the other hand, fetches votes by relation. Actually by voteByUserRel relation. There is no fetcher API that supports more than one relation function, so we have to refactor it a little bit.

In our case, we want to fetch votes by any relation, either with User or with Link.

ids(voteByUserRel) extracts the users’ ids and passes those to the db function, we have to change it. It is a good idea to pass ids down to the function, and in DAO decide which field it should use to filter.

There is one missing part: DAO.getVotesByRelationIds function, let’s create it now. This function should match the kind of relation we’re asking for, and filter by field depends on that relation.

The last thing to do is to change VoteType definition. We have to remove linkId property and instead add link field which returns the entire Link object.

Now you’re ready to execute a query like that:

query {
  links(ids :[1,2]){
    url
    votes {
      user{
        name
      }
    }
  }
}

You can also delete DAO.getVotesByUserIds function, we won’t need it anymore.

Recap

We achieved our goal for this chapter, our models have new functions:

User has links and votes fields.
Link has postedBy and votes fields.
Vote has user and link fields.

Now we can fetch the related data…

The current state of fileds we’ve changed in this chapter you can compare with those gists:

DAO.scala
models/package.scala
DBSchema.scala
GraphQLSchema.scala

In the next chapter you will learn how to add and save entities with GraphQL mutations.

Unlock the next chapter
Are we limited to amount of defined relations per model?
Yes, 1 per model
Yes, 2 per model
No, there is no such limitation
No, but relation could be based only on models's id