Search code examples
pythondjangographqlgraphene-django

GraphQL Mutation in Graphene for Object with Foreign Key Relation


I'm building a simple CRUD interface with Python, GraphQL (graphene-django) and Django. The CREATE mutation for an Object (Ingredient) that includes Foreign Key relations to another Object (Category) won't work. I want to give GraphQL the id of the CategoryObject and not a whole category instance. Then in the backend it should draw the relation to the Category object.

In the Django model the Ingredient Object contains an instance of the Foreign key Category Object (see code below). Is the whole Category Object needed here to draw the relation and to use Ingredient.objects.select_related('category').all()?

The create mutation expects IngredientInput that includes all properties and an integer field for the foreign key relation. So the graphQL mutation itself currently works as I want it to.

My question is similar if not the same as this one but these answers don't help me.

models.py:

class Category(models.Model):
    name = models.CharField(max_length=50, unique=True)
    notes = models.TextField()

    class Meta:
        verbose_name = u"Category"
        verbose_name_plural = u"Categories"
        ordering = ("id",)

    def __str__(self):
        return self.name


class Ingredient(models.Model):
    name = models.CharField(max_length=100)
    notes = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

    class Meta:
        verbose_name = u"Ingredient"
        verbose_name_plural = u"Ingredients"
        ordering = ("id",)

    def __str__(self):
        return self.name

schema.py:

class CategoryType(DjangoObjectType):
    class Meta:
        model = Category


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


class IngredientType(DjangoObjectType):
    class Meta:
        model = Ingredient


class IngredientInput(graphene.InputObjectType):
    name = graphene.String(required=True)
    notes = graphene.String()
    category = graphene.Int()


class CreateIngredient(graphene.Mutation):
    class Arguments:
        ingredientData = IngredientInput(required=True)

    ingredient = graphene.Field(IngredientType)

    @staticmethod
    def mutate(root, info, ingredientData):
        _ingredient = Ingredient.objects.create(**ingredientData)
        return CreateIngredient(ingredient=_ingredient)


class Mutation(graphene.ObjectType):
    create_category = CreateCategory.Field()
    create_ingredient = CreateIngredient.Field()

graphql_query:

mutation createIngredient($ingredientData: IngredientInput!) {
  createIngredient(ingredientData: $ingredientData) {
    ingredient {
      id
      name
      notes
      category{name}
    }

graphql-variables:

{
  "ingredientData": {
    "name": "milk",
    "notes": "from cow",
    "category": 8  # here I ant to insert the id of an existing category object
  }
}

error-message after executoin the query:

{
  "errors": [
    {
      "message": "Cannot assign \"8\": \"Ingredient.category\" must be a \"Category\" instance.",
      "locations": [
        {
          "line": 38,
          "column": 3
        }
      ],
      "path": [
        "createIngredient"
      ]
    }
  ],
  "data": {
    "createIngredient": null
  }
}

Solution

  • I had this same problem today.

    The Cannot assign \"8\": \"Ingredient.category\" must be a \"Category\" instance. error is a Django error that happens when you try to create an object using the foreign key integer directly instead of an object. If you want to use the foreign key id directly you have to use the _id suffix.

    For example, instead of using:

    _ingredient = Ingredient.objects.create(name="milk", notes="from_cow", category=8)
    

    You have to use either

    category_obj = Category.objects.get(id=8)
    _ingredient = Ingredient.objects.create(name="milk", notes="from_cow", category=category_obj)
    

    or

    _ingredient = Ingredient.objects.create(name="milk", notes="from_cow", category_id=8)
    

    In the case of using GraphQL, you would have to set your InputObjectType field to <name>_id. In your case:

    class IngredientInput(graphene.InputObjectType):
        name = graphene.String(required=True)
        notes = graphene.String()
        category_id = graphene.Int()
    

    This, however will make your field in the schema show up as categoryId. If you wish to keep the category name, you must change to:

    category_id = graphene.Int(name="category")
    

    Cheers!