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.
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:
Now we can go and do the GraphQL part of changes.
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.
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.
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.
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.
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.