Mutations¶
GraphQL mutations allow you to modify data on your server. django-graphex provides powerful tools to create mutations from Django models, making CRUD operations simple and consistent.
DjangoModelMutation¶
The DjangoModelMutation is the cornerstone of mutations in django-graphex. It automatically generates Create, Read, Update, and Delete (CRUD) operations directly from a Django model.
Features¶
- :material-auto-fix: Automatic CRUD Operations: Generates create, update, and delete mutations
- :material-check-circle: Built-in Validation: Validates all writable model fields, FK existence, uniqueness, and
unique_togetherconstraints - :material-file-upload: File Upload Support: Handles multipart/form-data requests
- :material-link-variant: Nested Relationships: Supports nested field creation and updates
- :material-alert-circle: Error Handling: Returns structured error responses
Basic Usage¶
Configuration Options¶
The DjangoModelMutation supports several configuration options:
Meta Configuration¶
from django.contrib.auth.models import User
from .models import Address, Profile
class UserMutation(DjangoModelMutation):
class Meta:
model = User
only_fields = ('username', 'email', 'first_name', 'last_name')
exclude_fields = ('password',)
input_field_name = 'user_data' # Default: 'new_{model_name}'
output_field_name = 'user' # Default: '{model_name}'
description = "Custom description for the mutation"
# map each nested field to its related Django model
nested_fields = {'profile': Profile, 'addresses': Address}
Field Filtering¶
Field Control
Use only_fields to include specific fields, or exclude_fields to exclude certain fields from mutations.
Custom Arguments¶
You can add custom arguments to your mutations:
import graphene
from django.contrib.auth.models import User
from django_graphex import DjangoModelMutation
class UserMutation(DjangoModelMutation):
class Meta:
model = User
class Arguments:
send_email = graphene.Boolean(
default_value=False,
description="Send welcome email after user creation"
)
@classmethod
def create(cls, root, info, **kwargs):
send_email = kwargs.pop('send_email', False)
response = super().create(root, info, **kwargs)
if response.ok and send_email:
send_welcome_email(getattr(response, cls._meta.output_field_name).email)
return response
Nested Fields Support¶
Handle related models with nested fields:
from django.contrib.auth.models import User
from .models import Address, Profile
class UserMutation(DjangoModelMutation):
class Meta:
model = User
# each nested field maps to its related Django model
nested_fields = {'profile': Profile, 'addresses': Address}
Nested Fields Behavior
- For single objects: The created object's ID is assigned to the field
- For lists: Objects are added to the many-to-many relationship
File Upload Support¶
The mutation automatically handles file uploads when the request content type is multipart/form-data:
from .models import Profile
# The mutation will automatically handle avatar uploads (ImageField on the model)
class ProfileMutation(DjangoModelMutation):
class Meta:
model = Profile
Error Handling¶
All mutations return a consistent response structure:
{
"ok": Boolean, # True if successful, False if errors
"errors": [ErrorType], # List of validation errors
"{model_name}": Object # The created/updated/deleted object (null if errors)
}
Example error response:
{
"data": {
"createUser": {
"ok": false,
"errors": [
{
"field": "email",
"messages": ["This field is required."]
},
{
"field": "username",
"messages": ["A user with that username already exists."]
}
],
"user": null
}
}
}
Custom Mutation Logic¶
Override methods to add custom logic:
Override create / update and call super() to run logic around the save
(there is no separate save hook — validation and persistence happen inside
create/update):
from django.contrib.auth.models import User
from django_graphex import DjangoModelMutation
class UserMutation(DjangoModelMutation):
class Meta:
model = User
@classmethod
def create(cls, root, info, **kwargs):
response = super().create(root, info, **kwargs)
# Custom logic after a successful save
if response.ok:
send_welcome_email(getattr(response, cls._meta.output_field_name).email)
return response
Complete Example¶
Here's a complete example showing all features:
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
birth_date = models.DateField(null=True, blank=True)
class Address(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
street = models.CharField(max_length=255)
city = models.CharField(max_length=100)
country = models.CharField(max_length=100)
import graphene
from django.contrib.auth.models import User
from django_graphex import DjangoModelMutation
from .models import Address, Profile
class UserMutation(DjangoModelMutation):
class Meta:
model = User
exclude_fields = ('is_staff', 'is_superuser')
nested_fields = {'profile': Profile, 'addresses': Address}
class Arguments:
send_welcome_email = graphene.Boolean(default_value=True)
How nested writes work¶
nested_fields = {field_name: RelatedModel} lets a single create/update write
related objects alongside the parent. The same engine backs both
DjangoModelMutation and DjangoModelType.
- Atomic — the whole operation runs in a
transaction.atomic()block. If the parent or any child fails validation, everything rolls back (no orphan rows) and the response isok: falsewith the errors. -
Relation-aware — the relation is introspected from the model:
Relation Order Behavior Forward ForeignKey/OneToOneFieldchild saved first its pk is set on the parent Reverse FK / reverse OneToOneparent first each child is linked to the parent ManyToManyField(either side)parent first children are saved and .add()-ed -
Upsert — a child payload that carries its
idupdates that row; without anidit creates a new one. (The nested input only exposesidon the parent's update input, so nested creates stay create-only.) - Additive & safe — M2M/reverse children are only added, never removed; an
empty
[]/{}payload is a no-op (the relation is left untouched). - Errors are prefixed — a child error is reported as
field.subfield(e.g.addresses.zip_code).
Reverse-FK nested fields
For a reverse relation (the child holds the FK to the parent), the child's
FK back to the parent is injected automatically at save time — you do not need
to supply it in the mutation input. Use exclude_fields on the child's
DjangoModelMutation (or only_fields) to keep it out of the input:
Traditional GraphQL Mutations¶
While DjangoModelMutation covers most use cases, you can still create traditional GraphQL mutations for custom logic:
import graphene
from django_graphex import DjangoObjectType
from django.contrib.auth.models import User
class UserType(DjangoObjectType):
class Meta:
model = User
class CreateUser(graphene.Mutation):
class Arguments:
username = graphene.String(required=True)
email = graphene.String(required=True)
password = graphene.String(required=True)
ok = graphene.Boolean()
user = graphene.Field(UserType)
def mutate(self, info, username, email, password):
user = User.objects.create_user(
username=username,
email=email,
password=password
)
return CreateUser(ok=True, user=user)
from django_graphex._compat import ErrorType
class CreateUser(graphene.Mutation):
class Arguments:
username = graphene.String(required=True)
email = graphene.String(required=True)
password = graphene.String(required=True)
ok = graphene.Boolean()
user = graphene.Field(UserType)
errors = graphene.List(ErrorType)
def mutate(self, info, username, email, password):
# Validation
errors = []
if User.objects.filter(username=username).exists():
errors.append(ErrorType(
field="username",
messages=["Username already exists"]
))
if User.objects.filter(email=email).exists():
errors.append(ErrorType(
field="email",
messages=["Email already registered"]
))
if errors:
return CreateUser(ok=False, errors=errors, user=None)
# Create user
user = User.objects.create_user(
username=username,
email=email,
password=password
)
return CreateUser(ok=True, user=user, errors=None)
Best Practices¶
Mutation Best Practices
- Use DjangoModelMutation: Drive mutations from
Meta.modelfor consistency - Validate Input: Always validate input data before processing
- Handle Errors Gracefully: Provide clear, actionable error messages
- Test Thoroughly: Write tests for all mutation scenarios
- Document Fields: Use descriptions for all mutation fields and arguments
- Security First: Implement proper authentication and authorization
Authentication & Permissions¶
from graphql import GraphQLError
from django.contrib.auth.models import User
from django_graphex import DjangoModelMutation
class UserMutation(DjangoModelMutation):
class Meta:
model = User
@classmethod
def create(cls, root, info, **kwargs):
if not info.context.user.is_authenticated:
raise GraphQLError("Authentication required")
if not info.context.user.has_perm('auth.add_user'):
raise GraphQLError("Permission denied")
return super().create(root, info, **kwargs)
Custom Validation¶
For field-level validation beyond automatic DB checks, supply a Pydantic model
via Meta.pydantic_model:
from pydantic import BaseModel, field_validator
from django.contrib.auth.models import User
from django_graphex import DjangoModelMutation
class UserValidation(BaseModel):
username: str
email: str
@field_validator("email", check_fields=False)
@classmethod
def email_must_be_corporate(cls, v):
if not v.endswith("@example.com"):
raise ValueError("Only corporate email addresses are accepted.")
return v
class UserMutation(DjangoModelMutation):
class Meta:
model = User
pydantic_model = UserValidation
Testing Mutations¶
import pytest
from graphene.test import Client
from .schema import schema
@pytest.mark.django_db
def test_create_user_mutation():
client = Client(schema)
mutation = """
mutation CreateUser($userData: UserInput!) {
createUser(newUser: $userData) {
ok
user {
id
username
email
}
errors {
field
messages
}
}
}
"""
variables = {
"userData": {
"username": "testuser",
"email": "test@example.com",
"password": "secretpass123"
}
}
result = client.execute(mutation, variables=variables)
assert result['data']['createUser']['ok'] is True
assert result['data']['createUser']['user']['username'] == 'testuser'
@pytest.mark.django_db
def test_create_user_validation_error():
client = Client(schema)
mutation = """
mutation CreateUser($userData: UserInput!) {
createUser(newUser: $userData) {
ok
errors {
field
messages
}
}
}
"""
# Missing required email
variables = {
"userData": {
"username": "testuser",
"password": "secretpass123"
}
}
result = client.execute(mutation, variables=variables)
assert result['data']['createUser']['ok'] is False
assert len(result['data']['createUser']['errors']) > 0
The mutation system in django-graphex provides a robust foundation for handling data modifications in your GraphQL API, with built-in validation, error handling, and support for complex operations.