UVic
UVic

Reputation: 1801

Polymorphism in GraphQL with a one-to-many relationship

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.

Upvotes: 6

Views: 3792

Answers (2)

peteb
peteb

Reputation: 19418

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.

Upvotes: 12

masmerino
masmerino

Reputation: 1036

Have you considered to have a single Post entity and define the post type using a enum type?

User

type User {
  id: ID!
  posts: [Post!]!
}

Post

type Post {
  id: ID!
  title: String!
  createdBy: User!
  type: PostType!
}

and the enum type for the Post

PostType

enum PostType {
  NORMAL
  LINK
}

----- Update! -----

If you want to have separated entities, you can just do that:

User

type User {
  id: ID!
  linkPosts: [LinkPost!]!
  normalPosts: [NormalPost]
}

LinkPost type

type LinkPost {
  description: String!
  url: String!
  createdBy: User!
}

NormalPost type

type NormalPost {
  title: String!
  description: String!
  createdBy: User!
}

Let me know if this works for you,

Regards

Upvotes: -1

Related Questions