I have the following entities in my schema.
How do I model the one to many relationship between the User
and Post
types where Post
can be of the two types LinkPost
or NormalPost
.
GraphQL works by sending operations to an endpoint. There are three types of operations: queries, mutations, and subscriptions.
The __typename field returns the object type's name as a String (e.g., Book or Author ). GraphQL clients use an object's __typename for many purposes, such as to determine which type was returned by a field that can return multiple types (i.e., a union or interface).
GraphQL scalar types Here are the primitive data types that GraphQL supports: Int – A signed 32-bit integer. Float – A signed double precision floating point value.
input types are used as a query parameters, e.g., payload for creating a user. In graphql-js library we basically have two different types, which can be used as objects. GraphQLObjectType (an output type), and GraphQLInputObjectType (an input type).
I would model these schemas utilizing one of the Polymorphic types that exist within GraphQL. That approach would give you the most flexibility in querying and extension in the future.
Its very convenient for a User
to have an array of Posts
as such:
type User {
id: Int!
email: String!
posts: [Posts!]
username: String!
}
This means that a Post
needs to be either an interface
or a union
type. Either of these two types allow us to leverage the fact that NormalPost
& LinkedPost
are both still a type of Post
. It will also allow us to query them in the same place just like in user.posts
above.
An interface
type is very similar in behavior to that of an interface
in OOP. It defines the base set of fields than any implementing type must also have. For instance, all Post
objects might look like this:
interface Post {
id: Int!
author: String!
dateCreated: String!
dateUpdated: String!
title: String!
}
Any type that implements
the Post
interface then must also have the fields id
, author
, dateCreated
, dateUpdated
& title
implemented in addition to any fields specific to that type. So using an interface
, NormalPost
& LinkedPost
might look as follows:
type NormalPost implements Post {
id: Int!
author: String!
body: String!
dateCreated: String!
dateUpdated: String!
title: String!
}
type LinkedPost implements Post {
id: Int!
author: String!
dateCreated: String!
dateUpdated: String!
link: String!
title: String!
}
A union
type allows dissimilar types that have no requirement of implementing similar fields to be return together. A union
type is structured differently than how it would using interfaces since a union
type does not specify any fields. The only thing defined within a union
schema definition are the unioned types separated by a |
.
The only caveat to that rule is any types within a union
that have with the same field name defined would need to have the same nullability (ie. Since NormalPost.title
is non-nullable (title: String!
) then LinkedPost.title
must also be non-nullable.
union Post = NormalPost | LinkedPost
type NormalPost implements Post {
id: Int!
author: String!
body: String!
dateCreated: String!
dateUpdated: String!
title: String!
}
type LinkedPost implements Post {
id: Int!
author: String!
dateCreated: String!
dateUpdated: String!
link: String!
title: String!
}
The above introduces the question of how to differentiate a LinkedPost
from a NormalPost
when querying them from user.posts
. In both cases, you would need to use a Conditional Fragment.
A Conditional Fragment allows a specific set of fields to be queried from an interface
or union
type. They look the same as a regular Query Fragment as they are defined using the ... on <Type>
syntax within your Query body. There is a slight difference in how a Conditional Fragment can be structured for an interface
vs a union
type.
In addition to querying using the Conditional Fragment it tends to be useful to add the __typename
Meta Field to your polymorphic queries so the consumer of the query can better identify the type of the resulting object in code.
Since an interface
defines a specific set of fields that all implementing types have in common those fields can be queried like any other normal query on a type. The difference is when the interface
types have different fields that are specific to their type, like NormalPost.body
vs LinkedPost.link
. A query that selects the entire Post
interface and then the NormalPost.body
and LinkedPost.link
would look as follows:
query getUsersNormalAndLinkedPosts {
user(id: 123) {
id
name
username
posts {
__typename
id
author
dateCreated
dateUpdated
title
... on NormalPost {
body
}
... on LinkedPost {
link
}
}
}
}
Since a union
doesn't define any common fields between its types, all of the fields that to be selected must exist in each Conditional Fragment. This is the only difference between querying interface
and a union
. Querying a union
type looks as follows:
query getUsersNormalAndLinkedPosts {
user(id: 123) {
id
name
username
posts {
__typename
... on NormalPost {
id
author
body
dateCreated
dateUpdated
title
}
... on LinkedPost {
id
author
dateCreated
dateUpdated
link
title
}
}
}
}
Both of the polymorphic types have strengths and weaknesses and its up to you to decide which is the best for your use case. I have utilized both when building out a GraphQL schema and the specific difference in when I use interface
or union
is if there are common fields between the implementing types.
Interfaces make a great deal of sense when the only difference between implementing types is only a small set of fields while the rest are shared between them. This leads to smaller queries and potentially less Conditional Fragments needed.
Unions really shine when you have a type that is a mask for multiple different types that are unrelated but come back together, like in a set of search results. Depending on the type of searches it return may return many different types that look nothing alike. For instance a search on a CMS that could yield both a User and a Post. In that case, it would make sense to have a the following type:
union SearchResult = User | Post
.
This can then be returned from a query with the signature
search(phrase: String!): [SearchResult!]
In the context of this specific question I would go with the interface
approach as it makes the most sense from a relationship and querying perspective.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With