Skip to content
This repository has been archived by the owner on Sep 2, 2022. It is now read-only.

[RFC] Datamodel v1.1 - Polymorphic Relations #3407

Closed
mavilein opened this issue Nov 1, 2018 · 23 comments
Closed

[RFC] Datamodel v1.1 - Polymorphic Relations #3407

mavilein opened this issue Nov 1, 2018 · 23 comments

Comments

@mavilein
Copy link
Contributor

mavilein commented Nov 1, 2018

This part of the spec describes the syntax for polymorphic relations. See the issue #3408 to learn about the other parts of the spec.

tables created with: https://stackedit.io/app

Introduction

Interfaces and unions are both a means to model polymorphic relations. Consider the following two examples. The first example shows a union can be used to model a polymorphic relationship where the types won't be stored in the same table/collection. The second example shows how the types can be stored within the same table/collection.

Example: Modeling a polymorphic relation with a union. In this case the types FacebookUser and GoogleUser will be stored within different tables/collections in the database. This approach is recommended if the types do not have much in common. This approach is very similar to Rails Active Record implementation for polymorphic relations.

type Comment {
  id: ID! @id
  author: User! @relation(link: INLINE)
}

union User = FacebookUser | GoogleUser
type FacebookUser {
  id: ID! @id
  facebookId: String!
}
type GoogleUser {
  id: ID! @id
  googleId: String!
}

Example: Modeling a polymorphic relation with an interface. In this case the type FacebookUser and GoogleUser inherit all the fields from the super type User. In this case the types FacebookUser and GoogleUser will be stored with a single table/collection called User. This approach is recommended if types share common fields and are often accessed together.

type Comment {
  id: ID! @id
  author: User! @relation(link: INLINE)
}

interface User @inheritance {
  id: ID! @id
  comments: [Comment]
}
type FacebookUser implements User {
  facebookId: String!
}
type GoogleUser implements User {
  googleId: String!
}

The inheritance directive

  • valid locations: on interfaces
  • behaviour: This directive marks an interface as an inheritance structure. This type can then be used in relation fields to create polymorphic relations.
  • optional: no
  • validations:
    • There must be at least one type inheriting from this interface.
  • Migrations:
    • Create a new type and add it to the inheritance structure → 1. add new columns to the single table
    • Add an existing type to the inheritance structure → 1. delete old table 2. add new columns to single table
    • Remove a type that was part of the inheritance structure → 1. remove all rows of that type 2. delete obsolete columns
    • Remove a type from inheritance structure but keep it in the data model → 1. remove all rows of that type 2. delete obsolete columns 3. create new table
  • Impact on API:
    • There will be a top level field to query the inheritance type if this one contains unique fields. So there will be a user and users query.
    • All mutations that are referencing the polymorphic type need be adapted so that they allow to specify which subtype should be affected. See the comment below for details.
  • Validations:
    • No subtype may specify a field as non unique which is also part of another subtype where it is unique.

The discriminator directive

  • valid locations:
    • on interfaces that are marked as inheritance
    • on types that implement interfaces that are marked as inheritance
    • on relation fields that reference union types

On types

  • behaviour: Specifies the discriminator value that should be used for this type in polymorphic relations.
  • optional: Yes. Defaults to the type name.
  • arguments:
    • value: String!
      • specifies the discriminator value that should be used for this type in polymorphic relations.
      • optional: no
  • Migrations: changing the discriminator value → update all relations links that used the old discriminator value to the new value

On interfaces and relation fields

  • behaviour: Specifies the column or field where the discriminator value will be stored.
  • optional:
    • interfaces:
      • No if a field called discriminator exists in the inheritance hierarchy.
      • Yes. Defaults to the name discriminator.
    • relation fields:
      • No if the type already contains a field that would clash with the default discriminator name.
      • Yes. Defaults to the field name and _discriminator, e.g. user_discriminator.
  • arguments:
    • name: String!
      • specifies the column or field where the discriminator value will be stored.
      • optional: no
  • validations:
    • Must not overlap with any discriminator value of a type that is part of a union or inheritance hierarchy where this type is also part of.
  • Migrations: changing the discriminator name → update all fields/columns that used the old discriminator name to use the new name

A type union

  • behaviour: Puts multiple types into a new union type. This union type can then be used in relation fields to create polymorphic relations.
  • validations:
    • Types that participate in the union must specify disjoint discriminator values.
  • Impact on API:
    • There will be no top level field to query the union type. So there will be no user or users query.
    • All mutations that are referencing the polymorphic type need be adapted so that they allow to specify which subtype should be affected. See the comment below for details.
  • Migrations:
    • If the name changes.
    • If names of types referenced in the union change.
    • If types are added to the union.
    • If types are removed from the union.
      • Existing polymorphic relation links using the type must be deleted. This must perform a required relation check.

Examples

A Union

In this example FacebookUser and GoogleUser would be stored in different tables or collections. The discriminator is stored in the relation link in this case.

Semantic Subtleties:

  • in SQL databases there will be no foreign key constraint on the column author in the table Comment because it is referencing two tables.
type Comment {
  id: ID! @id
  text: String!
  author: User! @relation(link: INLINE) @discriminator(name: "author_type")
}

union User = FacebookUser | GoogleUser
type FacebookUser @discriminator(value: "facebook"){
  id: ID! @id
  nick: String! @unique
  facebookId: String!
}
type GoogleUser @discriminator(value: "google") {
  id: ID! @id
  nick: String! @unique
  googleId: String!
}

The resulting database schema would look like this:

Comment (**no foreign key constraint on author in SQL**)
|id|text          |author_type|author| 
|--|--------------|-----------|------|
|1 |this is great.|facebook   |11    |

FacebookUser
|id|nick   |facebookId|
|--|-------|----------|
|11|thezuck|cjno...   |

GoogleUser
|id|nick |googleId|
|--|-----|--------|
|12|pichi|cjno... |

An interface

In this example FacebookUser and GoogleUser would be stored in the same table or collection, which would contain the discriminator.

Semantic Subtleties:

  • the field googleId and facebookId will not map to required columns in a SQL database. Prisma will guarantee that this constraint is met.
type Comment {
  id: ID! @id
  text: String!
  author: User! @relation(link: INLINE)
}

interface User @inheritance @discriminator(name:"type"){
  id: ID! @id
  nick: String! @unique
  comments: [Comment]
}
type FacebookUser implements User @discriminator(value: "facebook"){
  facebookId: String!
}
type GoogleUser implements User @discriminator(value: "google"){
  googleId: String!
}

The resulting database schema would look like this:

Comment (**with foreign key constraint on author in SQL**)
|id|text          |author|  |
|--|--------------|------|--|
|1 |this is great.|11    |  |

User
|id|nick   |type    |facebookId|googleId|
|--|-------|--------|----------|--------|
|11|thezuck|facebook|cjno...   |null    |
|12|pichi  |google  |null      |cjno... |

Polymorphic self relations

Polymorphic self relations are only possible with inheritance interfaces because unions do not define any common fields.

interface User @inheritance @discriminator(name:"type"){
  id: ID! @id
  nick: String! @unique
  friends: [User] @relation(name: "Friends")
}
type FacebookUser implements User @discriminator(value: "facebook") {
  facebookId: String!
}
type GoogleUser implements User @discriminator(value: "google") {
  googleId: String!
} 

The resulting database schema would look like this:

Friends (**with foreign key constraint on A and B**)
|A |B |
|--|--|
|11|12|

User 
|id|nick   |type    |facebookId|googleId|
|--|-------|--------|----------|--------|
|11|thezuck|facebook|cjno...   |null    |
|12|pichi  |google  |null      |cjno... |
@mavilein
Copy link
Contributor Author

mavilein commented Nov 1, 2018

Impact of Polymorphic Relations on the API

Generally speaking we need to enable polymorphic input types so that the user can choose which type should be affected by a mutation. But polymorphic input types are not possible in GraphQL. Therefore we try to emulate them through composition of existing input types. E.g.:

input FacebookUserCreateInput {
  nick: String!
  facebookId: String!
}
input GoogleUserCreateInput {
  nick: String!
  googleId: String!
}
# poor mans version of:
# type UserCreateInput = FacebookUserCreateInput | GoogleUserCreateInput 
input UserCreateInput {
  facebookUser: FacebookUserCreateInput
  googleUser: GoogleUserCreateInput
}

This way we can mostly rely on the validation of the GraphQL server for the schema. The only validation that we have to is that exactly one of the fields must be provided in the input type.

The following examples show GraphQL snippets for mutations to demonstrate the impact of polymorphic relations on Prismas OpenCRUD API.

Create

mutation {
	createUser(
		facebookUser: {
			data: {
				nick: "thezuck"
				facebookId: "cjn0..."
			}
		}
	){ id }
}

Update

mutation {
	updateUser(
		facebookUser: {
			where: { nick: "thezuck" }
			data:  { facebookId: "cjn0..." }
		}
	)
}{ id }

Upsert

mutation {
	upsertUser(
		facebookUser: {
			where:  { nick: "thezuck" }
			update: { facebookId: "cjn0..." }
			create: { nick: "thezuck" facebookId: "cjn0..." }
		}
	)
}{ id }

Nested Connect

mutation {
	createComment(data:{
		text: "this is a comment"
		author: {
			connect: {
				facebookUser: { nick: "thezuck" }
                      # OR: googleUser:   { nick: "pichi" }
			}
		}
	})
}

In the case of inheritance structures that specify a unique field on the inheritance type may also use the following:

mutation {
	createComment(data:{
		text: "this is a comment"
		author: {
			connect: {
				user: { nick: "thezuck" }
			}
		}
	})
}

Nested Disconnect

for single relation fields

mutation {
	updateComment(
		where: { id: "cjn0..." }
		data:{
			text: "this is an updated comment"
			author: {
				disconnect: true
			}
		}
	)
}

For list relation fields:

mutation {
	updateComment(
		where: { id: "cjn0..." }
		data:{
			text: "this is an updated comment"
			authors: {
				disconnect: [
					{	facebookUser: { nick: "thezuck" }	},
					{	googleUser:   { nick: "pichi" }	},
				]
			}
		}
	)
}

In the case of inheritance structures that specify a unique field on the inheritance type may also use the following:

mutation {
	updateComment(
		where: { id: "cjn0..." }
		data:{
			text: "this is an updated comment"
			authors: {
				disconnect: [
					{	user: { nick: "pichi" }	},
				]
			}
		}
	)
}

Nested Create

mutation {
	createComment(data:{
		text: "this is a comment"
		author: {
			create: {
				facebookUser: {
					nick: "thezuck"
					facebookId: "cjn0..."
				}
			}
		}
	})
}

Nested Update

mutation {
	updateComment(
		where: { id: "cjn0..." }
		data:{
			text: "this is an updated comment"
			author: {
				update: {				
					facebookUser: {
						where: { nick: "thezuck" }
						data:  { facebookId: "cjn0..." }
					}
				}
			}
		}
	)
}

Nested Upsert

mutation {
	updateComment(
		where: { id: "cjn0..." }
		data:{
			text: "this is an updated comment"
			author: {
				upsert: {				
					facebookUser: {
						where:  { nick: "thezuck" }
						update: { facebookId: "cjn0..." }
						create: { nick: "thezuck" facebookId: "cjn0..." }
					}
				}
			}
		}
	)
}

Nested Delete

for single relation fields

mutation {
	updateComment(
		where: { id: "cjn0..." }
		data:{
			text: "this is an updated comment"
			author: {
				delete: true
			}
		}
	)
}

For list relation fields:

mutation {
	updateComment(
		where: { id: "cjn0..." }
		data:{
			text: "this is an updated comment"
			authors: {
				delete: [
					{	facebookUser: { nick: "thezuck" }	},
					{	googleUser:   { nick: "pichi" }	},
				]
			}
		}
	)
}

In the case of inheritance structures that specify a unique field on the inheritance type may also use the following:

mutation {
	updateComment(
		where: { id: "cjn0..." }
		data:{
			text: "this is an updated comment"
			authors: {
				delete: [
					{	user: { nick: "pichi" }	},
				]
			}
		}
	)
}

@williamluke4
Copy link

All looks good, I'm very eager to get my hands on this!!! 😄

@schickling schickling changed the title [Data Model Spec] Polymorphic Relations [RFC] Datamodel v2 - Polymorphic Relations Dec 3, 2018
@Quadriphobs1
Copy link

So nice i'll be damn right to get my hands on this... just hoping when likely the community can get this in...

@tjpeden
Copy link

tjpeden commented Dec 10, 2018

  1. I'm curious why you guys chose @inheritance. Why not just @inherit?
  2. Would it be possible for @disicriminator values to support enums?
  3. Would it be possible for each type of a polymorphic relation to specify the same field with a different relation.
interface Master @inheritance @discriminator(name: "type") {
  id: ID! @id
  # ...
}
type AType implements Master @discriminator(value: "A") {
  details: BankAccount!
}
type BType implements Master @discriminator(value: "B") {
  details: CreditCardInformation!
}

@williamluke4
Copy link

@schickling Is there any way to try this out? I have tried the 1.24-alpha and 1.23-beta-1 docker images but get errors when using unions

@mavilein
Copy link
Contributor Author

@tjpeden:

  1. We haven't thought about @inherit. I would not favour it though as i would use that verb only in the subclass.
  2. That sounds interesting. But what would be the added value? I generally like it but it seems to require the user to type more than necessary.
  3. Yes that should be possible.

@mavilein
Copy link
Contributor Author

@williamluke4 : This is not implemented yet. This RFC is still in the draft status.

@williamluke4
Copy link

@mavilein Sorry was confused as this was on your website

The new datamodel is currently in Preview and can be used with the MongoDB connector.

@untouchable
Copy link

untouchable commented Dec 12, 2018

This whole Datamodel v1.1 spec is just awesome! Thanks guys! 😎

However, I hope I'm not too late in the game to make a suggestion 🤓

While working on some code to generate GraphQL data models, I've stumbled upon a naming-things-right-problem regarding the discriminator directive. Doing a quick recollection of how I've used APIs that solved a similar problem, I came up with descriptor. A quicklook at the Oxford English Dictionary confirmed my gut feeling defining as: "Descriptor Computing a piece of stored data that indicates how other data is stored.

Just to test my theory I went ahead and generated some of my GraphQL test-models which look identical to this spec, with the renamed directive. What I've found out is that the resulting code is easier to read, and comprehend. What do you think?

interface User @inheritance @descriptor(name:"type") {
  id: ID! @id
  nick: String! @unique
  friends: [User] @relation(name: "Friends")
}

type FacebookUser implements User @descriptor(value: "facebook") {
  facebookId: String!
}

type GoogleUser implements User @descriptor(value: "google") {
  googleId: String!
} 

type Comment {
  id: ID! @id
  text: String!
  author: User! @relation(link: INLINE) @descriptor(name: "author_type")
}

union User = FacebookUser | GoogleUser

type FacebookUser @descriptor(value: "facebook") {
  id: ID! @id
  nick: String! @unique
  facebookId: String!
}

type GoogleUser @descriptor(value: "google")  {
  id: ID! @id
  nick: String! @unique
  googleId: String!
}

@mavilein
Copy link
Contributor Author

mavilein commented Dec 14, 2018

@untouchable : Thanks for your suggestion. 🙏 The term descriptor make sense but our research indicates that the commonly used term for this functionality is discriminator in ORMs. But we will keep your feedback in mind in case more people struggle with it.

@untouchable
Copy link

@mavilein : thanks for you reply. 🙏 but I wouldn't call it a struggle as there's no known movement for the lexicographically challenged 😅 honestly, conceding to historical conventions is part of the job description.

@schickling
Copy link
Member

I think you have a valid point here @untouchable. Let's discuss the pros/cons a bit more. I agree that writing out discriminator is pretty cumbersome since it's so long.

@jasonkuhrt
Copy link
Contributor

jasonkuhrt commented Dec 24, 2018

  1. What would an interface without @inheritance mean?

  2. If we don't go with descriptor consider discriminant instead of discriminator.

  3. On the point of discriminator vs something else (such as descriptor) we should at least note that discriminant as a term seems to appear in programming language nomenclature when discussing union types, example quotes from the TypeScript community:

    TypeScript 2.0 implements a rather useful feature: tagged union types, which you might know as sum types or discriminated union types from other programming languages. A tagged union type is a union type whose member types all define a discriminant property of a literal type.

    TypeScript 2.0: Tagged Union Types

    If you have a class with a literal member then you can use that property to discriminate between union members.

    If you use a type guard style check (==, ===, !=, !==) or switch on the discriminant property

    TypeScript Deep Dive - Discriminated Union

    If the conceptual model we want the Prisma user to have aligns with the conceptual model in these other situations, then we should align the terminology. If the conceptual models however differ significantly, let's not overload.

    IIUC the conceptual models do align, though.

  4. In regards to explicit discriminator being optional, IIUC the following two forms are isomorphic...?

    Explicit:

    interface User @inheritance @discriminator(name: "type"){
      id: ID! @id
    }
    
    type FacebookUser implements User @discriminator(value: "FacebookUser"){
      facebookId: String!
    }
    
    type GoogleUser implements User @discriminator(value: "GoogleUser"){
      googleId: String!
    }

    Implicit:

    interface User @inheritance {
      id: ID! @id
    }
    
    type FacebookUser implements User {
      facebookId: String!
    }
    
    type GoogleUser implements User {
      googleId: String!
    }
  5. I think we may want to consider the following rule to further improve the implied discriminator:

    1. If the type name implementing the interface has the interface name as a suffix

    2. Then the implied discriminator value becomes a slugified version of the type name stripped of said suffix.

      Example (the following would be isomorphic):

      interface User @inheritance @discriminator(name: "type"){
        id: ID! @id
      }
      
      type FacebookUser implements User @discriminator(value: "facebook"){
        facebookId: String!
      }
      
      type GoogleUser implements User @discriminator(value: "google"){
        googleId: String!
      }

      Implicit:

      interface User @inheritance {
        id: ID! @id
      }
      
      type FacebookUser implements User {
        facebookId: String!
      }
      
      type GoogleUser implements User {
        googleId: String!
      }
  6. Is it a design goal that in the majority of cases explicitly setting the discriminant will not be needed?

  7. About:

    But polymorphic input types are not possible in GraphQL

    I realize RFC: inputUnion type is quite old but it is also still active. Also active but much more recent is [RFC] GraphQL Input Union type. It could be effective and natural I think for Prisma to add its use-case into the thread(s) and contribute toward resolving this spec issue once and for all.

    I suspect this feature's roadmap is on a shorter timeline than a new GQL spec could be published with union input feature... but in an ideal world it would be quite cool if the timetables aligned and could launch this feature using real input unions : )

  8. About:

    Polymorphic self relations are only possible with inheritance interfaces because unions do not define any common fields.

    Related spec issue IIUC: [RFC] Union types can implement interfaces. Again this is a situation where Prisma could step forward with use-case data. Also @leebyron mentioned in the thread here that a champion needs to step forward to progress the RFC. Again maybe Prisma could be that...

  9. Summary

    1. Nice write up, exciting feature!

    2. Underlying GQL 2018 spec limitations hurt the DX somewhat; It would be nice to see investment in resolving those underlying issues

    3. Feels like there might be an opportunity to further simplify/streamline the new directives @inheritance @discriminantor

@jasonkuhrt
Copy link
Contributor

jasonkuhrt commented Dec 24, 2018

graphql/graphql-spec#488 (comment)

Oh @sorenbs from Prisma has already been contributing to the union discussion 😄

@stevefan1999-personal
Copy link

stevefan1999-personal commented Jan 18, 2019

Regarding Polymorphic self relations in SQL, I suggest we could further normalize it:

type User @inheritable(baseName: "User") {
  id: ID! @id
  nick: String! @unique
  friends: [User] @relation(name: "Friends")
}
type FacebookUser implements User @inherit(name: "Facebook") {
  facebookId: String!
}
type GoogleUser implements User @inherit(name: "Google") {
  googleId: String!
} 

Should generates:

UserBase id nick typeId targetId
0 Alice 2 0
1 Bob 1 0
2 Charlie 2 1
3 Daniel 0 3
UserCategory typeId identifier
0 (null)
1 Facebook
2 Google
UserFriends userId friendId
0 1
1 0
0 2
2 0
UserGoogle id googleId
0 alice
1 charlie
UserFacebook id facebookId
0 Bob

This should satisfy 3NF, and it is much more prototypical like we could do with NoSQL. And we could update the user category table easier in the future (e.g. expand more types). However right now this is only constructed on the basis of classical single inheritance, to implement multiple inheritance/union remains a hard question.

@nikolasburk nikolasburk changed the title [RFC] Datamodel v2 - Polymorphic Relations [RFC] Datamodel v1.1 - Polymorphic Relations Feb 13, 2019
@jnlsn
Copy link

jnlsn commented Mar 14, 2019

When using Mongo, would it be possible to combine this with @Embedded so the varying data is stored directly in the document rather than a related collection?

@diversit
Copy link

diversit commented Apr 8, 2019

Any idea when this can be added to the beta?

@omar-rubix
Copy link

Has this been scrapped? Are there any updates on this?

@nir-g
Copy link

nir-g commented Jul 30, 2019

Same as above. There were many discussions around the various aspects of inheritance and polymorphism - all the way to a formal proposal and RFC from the Prisma team. Then the whole thing wait very quiet. I'm now reading about Prisma2, but still no update on this important topic.

@amille14
Copy link

Also very interested in this as it would help a lot when implementing a graph schema with vertexes and edges. Having Vertex and Edge interfaces that I can extend would be very valuable. @mavilein any updates on this? Will it be included in Prisma 2 / Photon?

@williamluke4
Copy link

@nikolasburk @mavilein Could we get some information on the state of this?

@nikolasburk
Copy link
Member

@williamluke4 please note that we're not working on new features for Prisma 1 (more info here). However, polymorphic relations are certainly a feature we're thinking about in the context of Prisma 2, you can track the development in this GitHub issue: prisma/prisma#253

@williamluke4
Copy link

Thanks, @nikolasburk It's probably worth closing this then :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests