Я бы смоделировал эти схемы, используя один из полиморфных типов, существующих в GraphQL. Такой подход даст вам наибольшую гибкость в запросах и расширении в будущем.
Схема проектирования
Для User
очень удобно иметь массив Posts
как таковой:
type User {
id: Int!
email: String!
posts: [Posts!]
username: String!
}
Это означает, что Post
должен иметь тип interface
или union
. Любой из этих двух типов позволяет нам использовать тот факт, что NormalPost
& LinkedPost
все еще являются типом Post
. Это также позволит нам запрашивать их там же, как и в user.posts
выше.
Интерфейсы
Тип interface
очень похож по поведению на тип interface
в ООП. Он определяет базовый набор полей, который также должен иметь любой тип реализации. Например, все Post
объекты могут выглядеть так:
interface Post {
id: Int!
author: String!
dateCreated: String!
dateUpdated: String!
title: String!
}
Любой тип, который implements
интерфейса Post
, должен также иметь поля id
, author
, dateCreated
, dateUpdated
& title
, реализованные в дополнение к любым полям, относящимся к этому типу. Поэтому использование interface
, 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!
}
Союзы
Тип
A union
позволяет разным типам, не требующим реализации аналогичных полей, возвращаться вместе. Тип union
структурирован иначе, чем при использовании интерфейсов, поскольку тип union
не указывает никаких полей. Единственное, что определено в определении схемы union
, - это объединенные типы, разделенные |
.
Единственное предостережение к этому правилу - любые типы в union
, имеющие одно и то же имя поля, должны иметь одинаковую обнуляемость (т. Е. Поскольку NormalPost.title
не может обнуляться (title: String!
), тогда LinkedPost.title
также не должно обнуляться.
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!
}
1063 * Запросы *
Выше вводится вопрос о том, как отличить LinkedPost
от NormalPost
при запросе их от user.posts
. В обоих случаях вам нужно будет использовать условный фрагмент .
Условный фрагмент позволяет запрашивать определенный набор полей типа interface
или union
. Они выглядят так же, как обычный фрагмент запроса, так как они определены с использованием синтаксиса ... on <Type>
в вашем теле запроса. Существует небольшая разница в том, как условный фрагмент может быть структурирован для типа interface
против union
.
В дополнение к запросам с использованием условного фрагмента полезно добавить в свои полиморфные запросы __typename
метаполе , чтобы потребитель запроса мог лучше определить тип получающегося объекта в код.
Интерфейсы
Так как interface
определяет конкретный набор полей, которые объединяют все реализующие типы, эти поля можно запрашивать, как и любой другой обычный запрос к типу. Разница заключается в том, что типы interface
имеют разные поля, специфичные для их типа, например NormalPost.body
против LinkedPost.link
. Запрос, который выбирает весь интерфейс Post
, а затем NormalPost.body
и LinkedPost.link
, будет выглядеть следующим образом:
query getUsersNormalAndLinkedPosts {
user(id: 123) {
id
name
username
posts {
__typename
id
author
dateCreated
dateUpdated
title
... on NormalPost {
body
}
... on LinkedPost {
link
}
}
}
}
Союзы
Поскольку union
не определяет общие поля между его типами, все поля, которые должны быть выбраны, должны существовать в каждом условном фрагменте. Это единственная разница между запросами interface
и union
. Запрос типа union
выглядит следующим образом:
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
}
}
}
}
Заключение
Оба полиморфных типа имеют свои сильные и слабые стороны, и вам решать, какой из них лучше всего подходит для вашего случая использования. Я использовал как при построении схемы GraphQL, так и особую разницу при использовании interface
или union
, если есть общие поля между типами реализации.
Интерфейсы имеют большой смысл, когда единственное различие между реализующими типами состоит только в небольшом наборе полей, в то время как остальные разделены между ними. Это приводит к меньшим запросам и потенциально меньшему количеству условных фрагментов.
Союзы действительно сияют, когда у вас есть тип, который является маской для нескольких разных типов, которые не связаны, но возвращаются вместе, как в наборе результатов поиска. В зависимости от типа поиска, который он возвращает, может возвращаться много разных типов, которые не похожи друг на друга. Например, поиск в CMS, который может дать как пользователя, так и сообщение. В этом случае имеет смысл иметь следующий тип:
union SearchResult = User | Post
.
Это может быть возвращено из запроса с подписью
search(phrase: String!): [SearchResult!]
В контексте этого конкретного вопроса я бы остановился на подходе interface
, так как он наиболее целесообразен с точки зрения отношений и запросов.