Django Graphene writing mutations with multiple layers of nested foreign keys

4.3k views Asked by At

How do you write the schema and query for nested foreign keys? I checked the docs and found no examples of how to do this. So here was my attempt based on github and stackoverflow answers lets say I have these models:

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

class Person(models.Model):
    name = models.CharField()
    address = models.ForeignKey('Address', on_delete=models.CASCADE, blank=False, null=False)

class Blog(models.Model):
    person = models.ForeignKey('Person', on_delete=models.CASCADE, blank=False, null=False)
    text = models.TextField()

I tried writing a schema like this:

class AddressInput(graphene.InputObjectType):

    name = graphene.String(required=True)


class PersonInput(graphene.InputObjectType):

    name = graphene.String(required=True)
    address =graphene.Field(AddressInput)

class CreateNewBlog(graphene.Mutation):

    blog=graphene.Field(BlogType)

    class Arguments:
        address_data = AddressInput()
        person_data = PersonInput()
        text = graphene.String()

    @staticmethod
    def mutate(root, info, person_data=None, address_data=None, **input):

        address = Address.objects.create(name=address_data.name)
        person = Person.objects.create(address=address, name=person_data.name)
        blog = Blog.objects.create(person =person, text=input['text'])
        blog.save()

        return CreateNewBlog(blog=blog)

and I used a query like this:

mutation {
        CreateNewBlog(person: { address: {name: "aaa"}, 
            name: "First Last" }, text: "hi hi") {
            Blog {
              person{
                name
                address{
                  name
                }
              },
              text
                
            }
        }
}

I got this error message:

{
  "errors": [
    {
      "message": "'NoneType' object has no attribute 'name'",
      "locations": [
        {
          "line": 32,
          "column": 9
        }
      ],
      "path": [
        "CreateNewBlog"
      ]
    }
  ],
  "data": {
    "CreateNewBlog": null
  }
}

I think the issue is in the way I wrote the schema.py file. Where it does not work to nest InputFields inside another InputField. Is there any other ways to write a single mutation?

1

There are 1 answers

0
Andrew Ingram On BEST ANSWER

Okay, a few things here. Firstly, you should generate your schema.graphql file, because that'll show you the actual final shape of the schema being built by Graphene, which would've made your debugging easier. Or you could use GraphiQL to test out your queries and lets its documentation and autocomplete do the heavy lifting for you.

But on to the specifics, your Graphene mutation definition is going to be generating a mutation that looks like this:

input AddressInput {
  name: String!
}

input PersonInput {
  name: String!
  address: AddressInput
}

type CreateNewBlogOutput {
  blog: Blog
}

type Mutation {
  CreateNewBlog(addressData: AddressInput, personData: PersonInput, text: String): CreateNewBlogOutput!
}

Worth noting that there are two ways for you to supply an AddressInput here, one at root, and one inside PersonInput. This probably isn't what you're intending to do. Secondly, none of the root arguments are required, which is contributing to your error message being fairly unhelpful, because the problem is you're calling the mutation incorrect parameters but the query validator is letting it through because your types are very permissive.

I believe that if you were to run the mutation like the following, it'd actually work:

mutation {
  CreateNewBlog(
    personData: {
      address: {
        name: "aaa"
      }, 
      name: "First Last"
    },
    text: "hi hi"
  ) {
    blog {
      person {
        name
        address {
          name
        }
      }
      text
    }
  }
}

I only made two changes here, person was changed to personData (to match your mutation definition, Graphene does the conversation from snake case to camel case automatically), and Blog to blog in the field selection.

But lets go a little further, here's how I would have made the mutation.

class AddressInput(graphene.InputObjectType):
    name = graphene.String(required=True)


class PersonInput(graphene.InputObjectType):
    name = graphene.String(required=True)
    address = AddressInput(required=True)


class CreateNewBlogInput(graphene.InputObjectType):
    person = PersonInput(required=True)
    text = graphene.String(required=True)


class CreateNewBlogPayload(graphene.ObjectType):
    blog = graphene.Field(BlogType, required=True)


class CreateNewBlog(graphene.Mutation):
    class Arguments:
        input_data = CreateNewBlogInput(required=True, name="input")

    Output = CreateNewBlogPayload


    @staticmethod
    def mutate(root, info, input_data):
        address = Address.objects.create(name=input_data.person.address.name)
        person = Person.objects.create(address=address, name=input_data.person.name)
        blog = Blog.objects.create(person=person, text=input_data.text)
        blog.save()

        return CreateNewBlogPayload(blog=blog)

I'd also change CreateNewBlog to createNewBlog when constructing Graphene's mutation object, because the GraphQL convention is to use lower camel case for mutations.

Then you'd run it like this:

mutation {
  createNewBlog(
    input: {
      person: {
        address: {
          name: "aaa"
        }, 
        name: "First Last"
      }
      text: "hi hi"
    }
  ) {
    blog {
      person {
        name
        address {
          name
        }
      }
      text
    }
  }
}

Why wrap the entire input in a single input field? Mainly because it makes calling the mutation easier in the client when using variables, you can just provide single input arg of the correct shape rather than multiple.

// So instead of this
mutation OldCreateNewBlog($person: PersonInput, $text: String) {
  createNewBlog(
    personData: $person
    text: $text
  ) {
    blog {
      person {
        name
        address {
          name
        }
      }
      text
    }
  }
}

// You have this
mutation NewCreateNewBlog($input: CreateNewBlogInput!) {
  createNewBlog(
    input: $input
  ) {
    blog {
      person {
        name
        address {
          name
        }
      }
      text
    }
  }
}

The latter makes it easier to change the input shape over time and only have to make the change in one place in client code.