I was asked the question about how to make relationships in Dgraph’s graphql. There was some confusion about creating a non-directional relationship vs. a directional relationship. And how to do one-to-one vs. one-to-many. So as I usually do, I put more words into it then probably was necessary. But I always believe the explaing something is a good way to understand it better yourself.
TL;DR;: Directional relationships are all in the naming of the edge. To do a one-to-many simply wrap one or both sides in brakets to form an Array (uh, I mean set.)
Let’s look at different relationships:
- Types
- One-to-One
- One-to-Many
- Many-to-Many
- Direction
- Parent->Child # same for many other types such as Employer->Employee, etc.
- Child->Parent
- Parent<->Child # same as above but uses hasInverse to keep relationships in sync.
- Person<->Spouse # similar to simple friend link
- Person->Crush
- Linking Node Relationships
- Person<->Ex-Spouse
- Employer<->Employee Department
- [non]Mutual Relationships
- Person->Requested Friends
- Person<-Has Friend Requests
- Person<->Mutual Friends
First of all, most everything that I am gonna do and explain is usually not the only way to do it. If you solve a needed relationship a different way using Dgraph, then go for it. There sometimes are multiple right answers in how relationships should be made depending on the need.
Types
The types of relationships are easy to remember and use. Simply put the the many side into an array. If an employer can have many employees it would look like:
type Employer {
hasEmployees: [Employee]
}
For Many-to-Many relationship types, it makes a difference if the related type is itself or another type. For its own type it would look similar to above, and for another type, it would need the inverse to keep track of the inverse relationships. Here are two different many-to-many relationships (friends, hobbies)
type Person {
hasFriends: [Person]
hasHobbies: [Hobby]
}
type Hobby {
hasInterests: [Person]
}
Definately on many to many relationships, you would normally want to keep them in sync by adding the @hasInverse
directives
type Person {
hasFriends: [Person] @hasInverse(field: hasFriends)
hasHobbies: [Hobby] @hasinverse(field: hasInterests)
}
type Hobby {
hasInterests: [Person] # hasInverse not needed here since it is the opposite of the above.
}
To make one-to-one relationships just add the related type without the brackets to form a link to a singular node and not a set of nodes.
type Person {
hasSSN: SSN @hasinverse(field: isPerson)
}
type SSN {
isPerson: Person # hasInverse not needed here since it is the opposite of the above.
}
type DL {
isPerson: Person # making a one way link here so no hasInverse needed. Cannot get to DL from Person this way though.
}
NOTE: It is somewhat misunderstood that Dgraph handles arrays like sets. This ensures that one node is only included once in the same relationship to another node. (ie: Node A can have a relationship through a single edge to Node B only once, but it could also be related to Node C on that same edge)
Direction
Direction can also be accomplished by declaring only edges and types. It is all a matter of what the inverse of the type is and what we name it. Let’s create People have children and the opposite direction of People have parents.
type Person {
hasChildren: [People] @hasInverse(field: hasParents)
hasParents: [People] # hasInverse not needed here, opposite of above.
}
There you have a direction of a relationship. If we did not include the hasInverse directive then the relationships would not be in sync. Sometimes that may be the desired effect, but not usually in a Parent<->Child relationship.
An example of a non-directional relationship would be spouses.
type Person {
hasSpouse: Person @hasInverse(field: hasSpouse)
}
If we did not include the hasInverse directive on the hasSpouse, then when a spouse was added, it would not automatically link from the spouse back to the original link, as is the usual desired case for spouse relationships.
How about something that is directional linked that is not in sync. An example of this would be as children have a crush on another child, but they are not in a mutual relationship. In the adult social world, we might consider this a following relationship, but usually we want to know who is following where crushes are secretive and not usually inversely aware. We will discuss more about [non]Mutual Relationships further below.
type Person {
hasCrush: Person
}
That is it. It is really simple and it a matter of what we name the edges, and we link the inverses that show the direction.
Linking Node Relationships
The concept here is that some relationships have conditional data. A spouse relationship could have a anniversary date and a divorce date. This way we could keep track of how long a couple has been married and also keep track of any previous relationships.
type Person {
hasWives: [Marriage] @hasInverse(field: husband)
hasHusbands: [Marriage] @hasInverse(field: wife)
}
type Marriage {
husband: Person
wife: Person
date: DateTime
divorceDate: DateTime
}
Normally, a person would not have both the edges hasWives and hasHusbands. How could this be translated into a single edge instead of two? This could be done, if you needed to put it in a single edge. Note that this would remove the direction of the relationship, and remove the quantity of the related entities. (There is no way to limit a Marraige in the DB now to just two people, and it could have many people) The direction and quantity of a relationship can both be preserved, but it does make the logic more complex, we will learn how next.
type Person {
spouse: [Marriage] @hasInverse(field: between)
}
type Marriage {
between: [Person]
date: DateTime
divorceDate: DateTime
}
To preserve quanitity of related objects, direction of relationship, and keep the relationship in a single edge, we would have every related item linked multiple times to the same edge. So in our example below, we would need to find the relationships where the person being related to was not on the direction to which we wanted. This example does not explain it fully, but the concept is between friendships. There is a better way to do this IMO which we will explain in the next section.
type Person {
friends: [Friendship] @hasInverse(field: requestedBy, field: requestedTo)
}
type Friendship {
requestedBy: Person
requestedTo: Person
requestedOn: DateTime
}
What about categorizing relationships? An employer has employees that serve in departments. We can show this three way relationship with Linking Nodes
type Person {
employer: Employment
}
type Business {
employees: Employment
hasDepartments: [Department] @hasInverse(field: ofBusiness)
}
type Department {
hasEmployees: [Employment]
ofBusiness: Business
}
type Employment {
employer: Business @hasInverse(field: employees)
employee: Person @hasInverse(field: employer)
inDepartment: Department @hasInverse(field: hasEmployees)
}
[non]Mutual Relationships
A common thing is to keep information about a relationship and later persisting it as a two way directional relationship instead of a one way relationship. There are multiple ways to do this as well in a graphDB. We will see first how it could be done with a linking Node. A linking Node is simply a node in the middle approach.
The relationship concept is that we can store relationships without the hasInverse directive and they would be one way relationships until confirmed and that would manually make the inverse relationship. Let’s get the simple schema and then look at how we find the answers to our questions.
type Person {
friends: [Person]
}
I told you, very simple schema, of course you would need ids, and names or other identifying information. Now let’s answer our questions. First who has sent me a friend request that I have not confirmed? (Note: This is dependent on the has()
for graphql function to find non cascading which is in current development in the graphql endpoint, so I will write these queries in DQL using var blocks)
{
var(func: uid("0x1")) { # my uid
fromMe as Person.friends
}
toMe as var(func: type(Person)) @cascade {
Person.friends @filter(uid("0x1")) { # my uid
uid
}
}
confirmedFriends(func: type(Person)) @filter(uid(fromMe) AND uid(toMe)) { uid }
pendSentReq(func: uid(fromMe)) @filter(NOT uid(toMe)) { uid }
pendRecvdReq(func: uid(toMe)) @filter(NOT uid(fromMe)) { uid }
allFriendsAndReq(func: uid(toMe, fromMe)) {uid }
}