2020-10-19 23:56:17 +02:00
---
title: DataObject inheritance
summary: Learn how inheritance is handled in DataObject types
---
# Working with DataObjects
[CHILDREN asList]
[alert]
You are viewing docs for a pre-release version of silverstripe/graphql (4.x).
Help us improve it by joining #graphql on the [Community Slack ](https://www.silverstripe.org/blog/community-slack-channel/ ),
and report any issues at [github.com/silverstripe/silverstripe-graphql ](https://github.com/silverstripe/silverstripe-graphql ).
Docs for the current stable version (3.x) can be found
[here ](https://github.com/silverstripe/silverstripe-graphql/tree/3 )
[/alert]
## DataObject inheritance
The inheritance pattern used in the ORM is a tricky thing to navigate in a GraphQL API, mostly owing
to the fact that there is no concept of inheritance in GraphQL types. The main tools we have at our
2021-06-30 09:05:18 +02:00
disposal are [interfaces ](https://graphql.org/learn/schema/#interfaces ) and [unions ](https://graphql.org/learn/schema/#union-types ) to deal with this type of architecture, and we leverage both of them when
working with dataobjects.
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
### Key concept: Querying types that have descendants
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
When you query a type that has descendant classes, you are by definition getting a polymorphic return. There
is no guarantee that each result will be of one specific type. Take this example:
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
```graphql
query {
readPages {
nodes {
title
content
}
}
}
```
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
This is fine when the two fields are common to across the entire inheritance chain, but what happens
when we need the `date` field on `BlogPage` ?
```graphql
query {
readPages {
nodes {
title
content
date # fails!
}
}
}
2020-10-19 23:56:17 +02:00
```
2021-06-30 09:05:18 +02:00
To solve this problem, the graphql module will automatically change these types of queries to return interfaces.
```graphql
query {
readPages {
nodes { # < --- [ PageInterface ]
title
content
}
}
}
2020-10-19 23:56:17 +02:00
```
2021-06-30 09:05:18 +02:00
But what about when we want more than `title` and `content` ? In some cases, we'll want fields that are specific to
`BlogPage` . When accessing fields for a specific implementation, we need to use an [inline fragment ](https://graphql.org/learn/queries/#inline-fragments ) to select them.
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
```graphql
query {
readPages {
nodes {
2021-06-30 09:38:11 +02:00
title # Common field
content # Common field
2021-06-30 09:05:18 +02:00
... on HomePage {
heroImage {
url
}
}
... on BlogPage {
date
author {
firstName
}
}
}
}
}
```
So the fields that are common to every possible type in the result set can be directly selected (with no `...on`
syntax), because they're part of the common interface. They're guaranteed to exist on every type. But for fields
that only appear on some types, we need to be explicit.
But let's take this a step further. What if there's another class in between? Imagine this ancestry:
```
2021-06-30 09:38:11 +02:00
Page
-> EventPage
-> ConferencePage
-> WebinarPage
2021-06-30 09:05:18 +02:00
```
We can use the intermediary interface `EventPageInterface` to consolidate fields that are unique to
`ConferencePage` and `WebinarPage` .
2021-06-30 09:38:11 +02:00
```graphql
2021-06-30 09:05:18 +02:00
query {
readPages {
nodes {
2021-06-30 09:38:11 +02:00
title # Common to all types
content # Common to all types
2021-06-30 09:05:18 +02:00
... on EventPageInterface {
2021-06-30 09:38:11 +02:00
# Common fields for WebinarPage, ConferencePage, EventPage
2021-06-30 09:05:18 +02:00
numberOfTickets
featuredSpeaker {
firstName
email
}
}
... on WebinarPage {
zoomLink
}
... on ConferencePage {
venueSize
}
... on BlogPage {
date
author {
firstName
}
}
}
}
}
```
You can think of interfaces in this context as abstractions of *parent classes* .
[info]
A good way to determine whether you need an inline fragment is to ask,
"Can this field appear on any other types in the query?" If the answer is yes, you want to use an interface,
which is usually the parent class with the "Interface" suffix.
[/info]
### Inheritance: A deep dive
2021-06-30 09:38:11 +02:00
There are several ways inheritance is handled at build time:
2021-06-30 09:05:18 +02:00
* Implicit field / type exposure
2021-06-30 09:38:11 +02:00
* Interface generation
* Assignment of generated interfaces to types
* Assignment of generated interfaces to queries
2021-06-30 09:05:18 +02:00
We'll look at each of these in detail.
#### Inherited fields / implicit exposures
Here are the rules for how inheritance affects types and fields:
* Exposing a type implicitly exposes all of its ancestors.
* Ancestors receive any fields exposed by their descendants, if applicable.
* Exposing a type applies all of its fields to descendants only if they are explicitly exposed also.
All of this is serviced by: `SilverStripe\GraphQL\Schema\DataObject\InheritanceBuilder`
##### Example:
```yaml
BlogPage:
fields:
2020-10-19 23:56:17 +02:00
title: true
content: true
2021-06-30 09:05:18 +02:00
date: true
GalleryPage:
2020-10-19 23:56:17 +02:00
fields:
2021-06-30 09:05:18 +02:00
images: true
urlSegment: true
```
This results in these two types being exposed with the fields as shown, but also results in a `Page` type:
2021-06-30 09:38:11 +02:00
```graphql
2021-06-30 09:05:18 +02:00
type Page {
id: ID! # always exposed
title: String
content: String
urlSegment: String
}
```
2021-06-30 09:38:11 +02:00
#### Interface generation
2021-06-30 09:05:18 +02:00
Any type that's part of an inheritance chain will generate interfaces. Each applicable ancestral interface is added
to the type. Like the type inheritance pattern shown above, interfaces duplicate fields from their ancestors as well.
Additionally, a **base interface** is provided for all types containing common fields across the entire DataObject
schema.
All of this is serviced by: `SilverStripe\GraphQL\Schema\DataObject\InterfaceBuilder`
##### Example
```
2021-06-30 09:38:11 +02:00
Page
-> BlogPage extends Page
-> EventsPage extends Page
-> ConferencePage extends EventsPage
-> WebinarPage extends EventsPage
2021-06-30 09:05:18 +02:00
```
This will create the following interfaces:
2021-06-30 09:38:11 +02:00
```graphql
2021-06-30 09:05:18 +02:00
interface PageInterface {
title: String
content: String
}
interface BlogPageInterface {
id: ID!
title: String
content: String
date: String
}
interface EventsPageInterface {
id: ID!
title: String
content: String
numberOfTickets: Int
}
interface ConferencePageInterface {
id: ID!
title: String
content: String
numberOfTickets: Int
venueSize: Int
venurAddress: String
}
interface WebinarPageInterface {
id: ID!
title: String
content: String
numberOfTickets: Int
zoomLink: String
}
```
2021-06-30 09:38:11 +02:00
#### Interface assignment to types
2021-06-30 09:05:18 +02:00
2021-06-30 09:38:11 +02:00
The generated interfaces then get applied to the appropriate types, like so:
```graphql
2021-06-30 09:05:18 +02:00
type Page implements PageInterface {}
type BlogPage implements BlogPageInterface & PageInterface {}
type EventsPage implements EventsPageInterface & PageInterface {}
type ConferencePage implements ConferencePageInterface & EventsPageInterface & PageInterface {}
type WebinarPage implements WebinarPageInterface & EventsPageInterface & PageInterface {}
```
Lastly, for good measure, we create a `DataObjectInterface` that applies to everything.
2021-06-30 09:38:11 +02:00
```graphql
2021-06-30 09:05:18 +02:00
interface DataObjectInterface {
id: ID!
# Any other fields you've explicitly exposed in config.modelConfig.DataObject.base_fields
}
```
2021-06-30 09:38:11 +02:00
```graphql
2021-06-30 09:05:18 +02:00
type Page implements PageInterface & DataObjectInterface {}
2020-10-19 23:56:17 +02:00
```
2021-06-30 09:38:11 +02:00
#### Interface assignment to queries
Queries, both at the root, and nested as fields on types, will have their types
updated if they refer to a type that has had any generated interfaces added to it.
```graphql
type Query {
readPages: [Page]
}
type BlogPage {
download: File
}
```
Becomes:
```graphql
type Query {
readPages: [PageInterface]
}
type BlogPage {
download: FileInterface
}
```
All of this is serviced by: `SilverStripe\GraphQL\Schema\DataObject\InterfaceBuilder`
2021-06-30 09:05:18 +02:00
#### Elemental
Almost by definition, content blocks are always abstractions. You're never going to query for a `BaseElement` type
specifically. You're always asking for an assortment of its descendants, which adds a lot of polymorphism to
the query.
2020-10-19 23:56:17 +02:00
```graphql
2021-06-30 09:05:18 +02:00
query {
readElementalPages {
nodes {
elementalArea {
elements {
nodes {
title
id
... on ContentBlock {
html
}
... on CTABlock {
link
linkText
}
}
}
2020-10-19 23:56:17 +02:00
}
}
}
}
```
2021-06-30 09:05:18 +02:00
### Optional: Use unions instead of interfaces
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
You can opt out of using interfaces as your return types for queries and instead use a union of all the concrete
types. This comes at a cost of potentially breaking your API unexpectedly (described below), so it is not enabled by
default. There is no substantive advantage to using unions over interfaces for your query return types. It would
typically only be done for conceptual purposes.
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
To use unions, turn on the `useUnionQueries` setting.
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
```yaml
SilverStripe\GraphQL\Schema\Schema:
schemas:
default:
config:
modelConfig:
DataObject:
plugins:
inheritance:
useUnionQueries: true
```
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
This means that models that have descendants will create unions that include themselves and all of their descendants.
For queries that return those models, a union is put in its place.
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
Serviced by: `SilverStripe\GraphQL\Schema\DataObject\InheritanceUnionBuilder`
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
##### Example
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
```
type Page implements PageInterface {}
type BlogPage implements BlogPageInterface & PageInterface {}
type EventsPage implements EventsPageInterface & PageInterface {}
type ConferencePage implements ConferencePageInterface & EventsPageInterface & PageInterface {}
type WebinarPage implements WebinarPageInterface & EventsPageInterface & PageInterface {}
```
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
Creates the following unions:
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
```
union PageInheritanceUnion = Page | BlogPage | EventsPage | ConferencePage | WebinarPage
union EventsPageInheritanceUnion = EventsPage | ConferencePage | WebinarPage
```
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
"Leaf" models like `BlogPage` , `ConferencePage` , and `WebinarPage` that have no exposed descendants will not create
unions, as they are functionally useless.
2020-10-19 23:56:17 +02:00
2021-06-30 09:05:18 +02:00
This means that queries for `readPages` and `readEventsPages` will now return unions.
2020-10-19 23:56:17 +02:00
```graphql
2021-06-30 09:05:18 +02:00
query {
readPages {
nodes {
... on PageInterface {
id # in theory, this common field could be done on DataObjectInterface, but that gets a bit verbose
title
content
}
... on EventsPageInterface {
numberOfTickets
}
... on BlogPage {
date
}
... on WebinarPage {
zoomLink
2020-10-19 23:56:17 +02:00
}
}
}
}
```
2021-06-30 09:05:18 +02:00
#### Lookout for the footgun!
Because unions are force substituted for your queries when a model has exposed descendants, it is possible that adding
a subclass to a model will break your queries without much warning to you.
For instance:
```php
class Product extends DataObject
{
private static $db = ['Price' => 'Int'];
}
```
We might query this with:
```graphql
query {
readProducts {
nodes {
price
}
}
}
```
But if we create a subclass for product and expose it to graphql:
```php
class DigitalProduct extends Product
{
private static $db = ['DownloadURL' => 'Varchar'];
}
```
Now our query breaks:
```
query {
readProducts {
nodes {
price # Error: Field "price" not found on ProductInheritanceUnion
}
}
}
```
We need to revise it:
```
query {
readProducts {
nodes {
... on ProductInterface {
price
}
... on DigitalProduct {
downloadUrl
}
}
}
}
```
Had we used interfaces, this wouldn't have broken, because the `price` field would have been on `ProductInterface`
and directly queryable (without the inline fragment).
2020-10-19 23:56:17 +02:00
### Further reading
[CHILDREN]