Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polymorphism in GraphQL with a one-to-many relationship

Tags:

graphql

I have the following entities in my schema.

  • User
  • Link Post
  • Normal Post

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.

like image 540
UVic Avatar asked Jan 11 '19 13:01

UVic


People also ask

What are the three types of operations in GraphQL?

GraphQL works by sending operations to an endpoint. There are three types of operations: queries, mutations, and subscriptions.

What is __ Typename in GraphQL?

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).

Does GraphQL support double?

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.

What is input in GraphQL?

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).


1 Answers

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.

Schema Design

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.

Interfaces

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

Unions

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

Querying

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.

Interfaces

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

Unions

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

Conclusion

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.

like image 125
peteb Avatar answered Oct 26 '22 07:10

peteb