prisma / prisma1

💾 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL & MongoDB) [deprecated]
https://v1.prisma.io/docs/
Apache License 2.0
16.54k stars 863 forks source link

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

Closed mavilein closed 2 years ago

mavilein commented 5 years ago

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

The discriminator directive

On types

On interfaces and relation fields

A type union

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:

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:

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 commented 5 years ago

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 commented 5 years ago

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

Quadriphobs1 commented 5 years ago

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

tjpeden commented 5 years ago
  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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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

williamluke4 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago
  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 commented 5 years ago

https://github.com/facebook/graphql/issues/488#issuecomment-412644805

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

stevefan1999-personal commented 5 years ago

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.

jnlsn commented 5 years ago

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 commented 5 years ago

Any idea when this can be added to the beta?

omar-rubix commented 5 years ago

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

nir-g commented 5 years ago

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 commented 5 years ago

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 commented 4 years ago

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

nikolasburk commented 4 years ago

@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: https://github.com/prisma/prisma2/issues/253

williamluke4 commented 4 years ago

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