Graphene-django with ManyToMany & through-table
Asked Answered
K

1

10

My app has several many-to-many relationships with a through-model like so:

class Person(models.Model):
    name = models.CharField()

class Group(models.Model):
    name = models.CharField()
    members = models.ManyToManyField(Person, through='Membership')

class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    date_joined = models.DateField()  # Extra info on the relationship

It would seem intuitive to represent this data in graphql without an intermediate type for Membership (option A):

{
  "data": {
    "persons": [
      {
        "id": "1",
        "name": "Jack",
        "groups": [
          {
            "id": 3,                     # From Group-model
            "name": "Students",          # From Group-model
            "date_joined": "2019-01-01"  # From Membership-model
          },
          ...
        ]
      }
    ]
  }
}

vs. option B:

{
  "data": {
    "persons": [
      {
        "id": "1",
        "name": "Jack",
        "memberships": [
          {
            "id": 9,
            "date_joined": "2019-01-01"
            "group": {
              "id": 3, 
              "name": "Students"
            }
          },
          ...
        ]
      }
    ]
  }
}

I could not find any examples on how to implement option A with (django-)graphene. How could it be done and is this supported to work out of the box?

What are the pros and cons on both approaches? The data needs to be also mutated quite often, does it alter the verdict?

Kameko answered 27/4, 2019 at 13:11 Comment(2)
Have you been able to make this work in the meantime?Abisha
@R.Wenger No, I ended up writing a simple and no-hassle REST API.Kameko
R
4

You can achieve this by creating a type which expresses fields from both models. For example:

import graphene
from graphene_django.types import DjangoObjectType


# hybrid type, expresses some characteristics of Member and Group
class UserGroupType(DjangoObjectType):
    class Meta:
        model = Membership
    
        # date_joined is automatically derived from the membership
        # instance, name and id are declared below.
        fields = ('id', 'name', 'date_joined', )
    
    id = graphene.ID()
    name = graphene.String()
    
    def resolve_id(value_obj, info):
        return value_obj.group.pk

    def resolve_name(value_obj, info):
        return value_obj.group.name


class PersonType(DjangoObjectType):
    class Meta:
        model = Person
    
        # id and name are automatically derived from the person
        # instance, groups is declared below, overriding the 
        # normal model relationship.
        fields = ('id', 'name', 'groups', )
    
    groups = graphene.List(UserGroupType)
    
    def resolve_groups(value_obj, info):
        return value_obj.memberships

For any type built from Graphene's ObjectType (which DjangoObjectType descends from), to express a field in output you need two things:

  1. A declaration of the type of the field
  2. A resolver method which generates the result to be cast into that type

DjangoObjectType evaluates the model you provide it to generate these automatically and uses the fields attribute to let you customize what properties to reveal.

By customizing fields and then adding manual props/resolvers for what you want to add you can make the type return anything you want.

Note the resolvers don't receive self as the first argument, but instead get a value object. The value object is the return value of your query resolver and is generally an instance of your model or an array of models that matched a filter, etc.

Redness answered 16/11, 2020 at 19:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.