graphql-engine icon indicating copy to clipboard operation
graphql-engine copied to clipboard

feature: Polymorphic Relations

Open jorroll opened this issue 6 years ago • 15 comments

Feature Request: Polymorphic Relations

The ability to create polymorphic one-to-one, one-to-many, and many-to-many relationships in Hasura. This would necessitate Hasura generating appropriate union or interface graphql types for the polymorphic association (or some way for the user to create these types).

  • an example of a polymorphic association is a contact list which contains both person and organization subscribers.

Polymorphic associations have a number of advantages over regular associations which cannot be currently achieved:

  1. The ability to cascade deletion of a polymorphic association (or restrict updates, etc).
  2. The ability to provide the appropriate domain model in your graphql schema (using union or interface types).

Some examples which could benefit from polymorphic associations:

  1. A database has Person, Organization, and Event records which are each associated with an Address record. The address record has a standard shape, so it might make sense to model the data with people, organizations, events, and addresses tables.
    • Currently, you might add an address_id column to the people, organizations, and events tables which acts as a foreign key to an address record. However, this does not allow you to automatically delete the address associated with a Person, Organization, or Event.
      • To automatically delete the address associated with a person when the person is deleted, you can manually create a postgres trigger on the people table. It would be nice if Hasura supplied an option to create this trigger for you.
    • Alternatively, you could add person_addresses, organization_addresses, and event_addresses tables instead of a single addresses table. However this denormalizes your database schema and unnecessarily complicates the generated graphql schema.
  2. A database has ContactList records which have a many-to-many association with both Person and Organization records.
    • Currently, this must be modeled using organization_subscription and person_subscription join tables. If a graphql client wants to be able to pull the first 10 subscribers of a contact list in alphabetical order, you must create a custom sql view. Problematically, the graphql associated with this custom SQL view cannot return an interface/union type of organization and person records. This can be worked around by returning an artificial subscription object with person and organization properties which would contain either a person or organization. However jumping through these hoops is not ideal.

Possible implementations

As an example, Rails' Active Record supports polymorphic associations by utilizing an additional type column for associations. The type column contains the name of the table, while the standard foreign key column contains the id of the table row.

  • In our first example above, the addresses table might model its polymorphic association using owner_id and owner_type columns. The owner_type column would contain the table of the address owner (either people, organizations, or events) while the owner_id column would contain the row id of the address owner.

A major problem with the Active Record solution is that its logic is handled outside of the database in the ruby application. Active Record does not utilize a foreign key constraint for polymorphic associations because postgres can't handle that scenerio. i.e. the Hasura graphql-engine server would be handling the logic, checking for the existance of foreign records, etc. This strategy would likely run into major problems around concurrency and would be a major change for hasura, which seems to delegate as much logic as possible to the database.

Doing a bit of research, I've found a few helpful stackoverflow answers on how to handle polymorphic associations within the database:

  • https://stackoverflow.com/a/4970646/5490505
  • https://stackoverflow.com/a/7948179/5490505

The major takeaway is that postgres/sql does not formally support polymorphic associations. Given that polymorphic associations are a very common part of many data models though, and given that polymorphic associations are a widely used aspect of graphql, it seems worthwhile to find a way of supporting them within Hasura.

Since there doesn't appear to be an "official" way to support polymorphic associations, Hasura would need to take an opinionated route here (which, personally, I think is fine). The super-table / sub-table approach described in this Stackoverflow answer seems like a good solution: https://stackoverflow.com/a/4970646/5490505.

A super-table / sub-table approach would allow for describing both graphql interface types and union types (an interface type would be described by the super-table. A union type would also be described by the super table, but the super-table wouldn't contain any information other than id). Hasura would take care of joining the tables and presenting a single record to the graphql client. Hasura would also take care of handling mutations as if only a single table were being updated. Unfortunately, a super/sub table approach still wouldn't allow for cascade deletion of a polymorphic association (perhaps automatically generating postgres triggers could help with this).

Related: #1167

jorroll avatar Jun 03 '19 19:06 jorroll

Laravel eloquent also has very nice polymorphic relations implemented https://laravel.com/docs/eloquent-relationships

Youhan avatar Jun 20 '19 03:06 Youhan

@Youhan Looks like Laravel takes the same approach as ActiveRecord. As I pointed out in the "possible implementations" section above, while that strategy is pretty easy to implement at the graphql-engine level (i.e. not the database level), I think it's probably a non-starter for Hasura for the reasons outlined above.

jorroll avatar Jun 20 '19 05:06 jorroll

Any updates on that?

ragnorc avatar Sep 08 '19 21:09 ragnorc

Heyy, is it kosher to allow overriding of __typename, seems like we could easily get this to work, or is that too hacky?

TABLE Entity
id

TABLE entity_customer_types
entity_id
customer_id_for_type
__typename                    |  <type goes here in text> eg. "CustomerType1"

TABLE CustomerType1
id
name
type_1_field

TABLE CustomerType2
id
name
type_2_field

########
# Usage: 
########
Query {
  Entity {
    entity_customer_types {
    __typename
    ... on CustomerType1 {
      name
      type_1_field
    }
    ... on CustomerType2 {
      name
      type_2_field
    }
    }
  }
}

henghonglee avatar May 07 '20 18:05 henghonglee

I too come from a Laravel world, and really need polymorphic relations to make things like attaching images to models maintainable. Can we get some input on this becoming an official feature, or an option for the best temporary workaround? Thanks!

zlanich avatar Aug 26 '20 21:08 zlanich

@zlanich Have you seen these workarounds? https://github.com/hasura/graphql-engine/issues/1167#issuecomment-444761777

cllns avatar Sep 01 '20 16:09 cllns

@cllns Yes, and while that is "slightly" better than creating separate tables (from an organization perspective), it still requires you to create a relationship view for every object type. It's still quite clumsy, particularly when coming from a Laravel world where you don't need to do this. You just specify a morph-map that transforms the active record classes into slugs like "model-name", then it handles the rest for you. I'm really hoping for proper support for polymorphic relations at some point :(

zlanich avatar Sep 01 '20 16:09 zlanich

I really like to see this feature. The absence actually keeps us from adopting hasura.

sspies8684 avatar Dec 14 '20 10:12 sspies8684

After reading a lot about this, I think the best solution is the one adopted by the GitLab team: always use separate tables instead of polymorphic associations. It's the only solution that ensures you don't need external logic and keep things atomic and organized.

deloredo avatar Jan 02 '22 20:01 deloredo

@gloredo I initially interpreted your comment as, essentially, saying this issue should be closed without action. But on a second read, it's not clear to me that you are saying that. You might just be expressing the opinion that polymorphic relations (i.e. graphql union and interface types) should be built up within the Hasura server from multiple separate database tables.

Regardless, it should be noted that the article you linked to is largely specific to the Active Record (i.e. Rails) implementation of polymorphic associations and most of the criticisms discuss flaws specific to the Active Record approach. As I mentioned in the original post and also in a comment, I expect that the Active Record strategy is a no-go for Hasura, but that doesn't necessarily imply that Hasura couldn't achieve polymorphism via another method. Whatever Hasura does, I expect it will need to be opinionated since, so far as I'm aware, Postgres doesn't currently have a way to cleanly handle polymorphic associations (of course, I expect when Hasura supports MongoDB it will be easy to support polymorphic relations in that database via embedded documents).

But, in case other's interpret your comment like I originally did, I just wanted to point out that, even if we can't have polymorphic associations within the database, this doesn't mean that we can't have polymorphic relations within the GraphQL schema generated by Hasura. Perhaps I should make this clearer in the original posting above, but this is fundamentally a feature request for polymorphic relations within the generated graphql schema of Hasura. The possible implementations I discussed above focused on ways that this might be accomplished with the database schema, but if polymorphic graphql relations were achieved via another method this would also be fine (in my opinion, at least).

jorroll avatar Jan 03 '22 07:01 jorroll

@jorroll, you're right, I expressed myself incorrectly. I strongly believe that hasura should implement a solution to the problem, but it should be as db-level as possible, and keeping the tables separate is the way to go in my opinion.

As a way to "representing" it, graphql union and interface types seem to make a lot of sense. Initially, I thought of a "virtual table" that the other tables would "extend" as the biggest problem with the separate tables approach is the duplication of columns between tables, but it doesn't seem like a pretty or viable solution.

deloredo avatar Jan 03 '22 13:01 deloredo

RELATED:

  • graphQL spec RFC and @oneOf https://github.com/graphql/graphql-spec/pull/825 This would eventually trickle down to hasura to figure out a way to implement

  • General search on "polymorphism" in the graphql spec repo https://github.com/graphql/graphql-spec/issues?q=polymorphism+is%3Aopen

erikhSSI avatar Jan 12 '22 20:01 erikhSSI

Any updates on this?

Youhan avatar May 31 '23 07:05 Youhan

any updates?

Attaroce avatar Aug 09 '25 21:08 Attaroce

is it possible to augment schema with polymorphic data without spinning up additional service with remote scheme (that's what AI suggested me to do in order to achieve desired result, I'm not sure if it's going to work this way, just starting to learn gql)?

Attaroce avatar Aug 10 '25 09:08 Attaroce