Graphene-Django makes it easy to perform mutations.
With Graphene-Django we can take advantage of pre-existing Django features to quickly build CRUD functionality, while still using the core graphene mutation features to add custom mutations to a Django project.
import graphene
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType):
class Meta:
model = Question
fields = '__all__'
class QuestionMutation(graphene.Mutation):
class Arguments:
# The input arguments for this mutation
text = graphene.String(required=True)
id = graphene.ID()
# The class attributes define the response of the mutation
question = graphene.Field(QuestionType)
@classmethod
def mutate(cls, root, info, text, id):
question = Question.objects.get(pk=id)
question.text = text
question.save()
# Notice we return an instance of this mutation
return QuestionMutation(question=question)
class Mutation(graphene.ObjectType):
update_question = QuestionMutation.Field()
Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation.
from graphene_django.forms.mutation import DjangoFormMutation
class MyForm(forms.Form):
name = forms.CharField()
class MyMutation(DjangoFormMutation):
class Meta:
form_class = MyForm
MyMutation
will automatically receive an input
argument. This argument should be a dict
where the key is name
and the value is a string.
DjangoModelFormMutation
will pull the fields from a ModelForm
.
from graphene_django.forms.mutation import DjangoModelFormMutation
class Pet(models.Model):
name = models.CharField()
class PetForm(forms.ModelForm):
class Meta:
model = Pet
fields = ('name',)
# This will get returned when the mutation completes successfully
class PetType(DjangoObjectType):
class Meta:
model = Pet
fields = '__all__'
class PetMutation(DjangoModelFormMutation):
pet = Field(PetType)
class Meta:
form_class = PetForm
PetMutation
will grab the fields from PetForm
and turn them into inputs. If the form is valid then the mutation
will lookup the DjangoObjectType
for the Pet
model and return that under the key pet
. Otherwise it will
return a list of errors.
You can change the input name (default is input
) and the return field name (default is the model name lowercase).
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
input_field_name = 'data'
return_field_name = 'my_pet'
Form mutations will call is_valid()
on your forms.
If the form is valid then the class method perform_mutate(form, info)
is called on the mutation. Override this method
to change how the form is saved or to return a different Graphene object type.
If the form is not valid then a list of errors will be returned. These errors have two fields: field
, a string
containing the name of the invalid form field, and messages
, a list of strings with the validation messages.
DjangoFormInputObjectType
is used in mutations to create input fields by using django form to retrieve input data structure from it. This can be helpful in situations where you need to pass data to several django forms in one mutation.
from graphene_django.forms.types import DjangoFormInputObjectType
class PetFormInput(DjangoFormInputObjectType):
# any other fields can be placed here as well as
# other djangoforminputobjects and intputobjects
class Meta:
form_class = PetForm
object_type = PetType
class QuestionFormInput(DjangoFormInputObjectType)
class Meta:
form_class = QuestionForm
object_type = QuestionType
class SeveralFormsInputData(graphene.InputObjectType):
pet = PetFormInput(required=True)
question = QuestionFormInput(required=True)
class SomeSophisticatedMutation(graphene.Mutation):
class Arguments:
data = SeveralFormsInputData(required=True)
@staticmethod
def mutate(_root, _info, data):
pet_form_inst = PetForm(data=data.pet)
question_form_inst = QuestionForm(data=data.question)
if pet_form_inst.is_valid():
pet_model_instance = pet_form_inst.save(commit=False)
if question_form_inst.is_valid():
question_model_instance = question_form_inst.save(commit=False)
# ...
Additional to InputObjectType Meta
class attributes:
form_class
is required and should be equal to django form class.object_type
is not required and used to enable convertion of enum values back to original if model object typeconvert_choices_to_enum
Meta
class attribute is not set toFalse
. Any data field, which have choices in django, with valueA_1
(for example) from client will be automatically converted to1
in mutation data.add_id_field_name
is used to specify id field name (not required, by default equal toid
)add_id_field_type
is used to specify id field type (not required, default isgraphene.ID
)
You can re-use your Django Rest Framework serializer with Graphene Django mutations.
You can create a Mutation based on a serializer by using the SerializerMutation base class:
from graphene_django.rest_framework.mutation import SerializerMutation
class MyAwesomeMutation(SerializerMutation):
class Meta:
serializer_class = MySerializer
By default ModelSerializers accept create and update operations. To
customize this use the model_operations attribute on the SerializerMutation
class.
The update operation looks up models by the primary key by default. You can
customize the look up with the lookup_field
attribute on the SerializerMutation
class.
from graphene_django.rest_framework.mutation import SerializerMutation
from .serializers import MyModelSerializer
class AwesomeModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
model_operations = ['create', 'update']
lookup_field = 'id'
Use the method get_serializer_kwargs
to override how updates are applied.
from graphene_django.rest_framework.mutation import SerializerMutation
from .serializers import MyModelSerializer
class AwesomeModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
@classmethod
def get_serializer_kwargs(cls, root, info, **input):
if 'id' in input:
instance = Post.objects.filter(
id=input['id'], owner=info.context.user
).first()
if instance:
return {'instance': instance, 'data': input, 'partial': True}
else:
raise http.Http404
return {'data': input, 'partial': True}
You can use relay with mutations. A Relay mutation must inherit from
ClientIDMutation
and implement the mutate_and_get_payload
method:
import graphene
from graphene import relay
from graphene_django import DjangoObjectType
from graphql_relay import from_global_id
from .queries import QuestionType
class QuestionMutation(relay.ClientIDMutation):
class Input:
text = graphene.String(required=True)
id = graphene.ID()
question = graphene.Field(QuestionType)
@classmethod
def mutate_and_get_payload(cls, root, info, text, id):
question = Question.objects.get(pk=from_global_id(id)[1])
question.text = text
question.save()
return QuestionMutation(question=question)
Notice that the class Arguments
is renamed to class Input
with relay.
This is due to a deprecation of class Arguments
in graphene 2.0.
Relay ClientIDMutation accept a clientIDMutation
argument.
This argument is also sent back to the client with the mutation result
(you do not have to do anything). For services that manage
a pool of many GraphQL requests in bulk, the clientIDMutation
allows you to match up a specific mutation with the response.
Django gives you a few ways to control how database transactions are managed.
A common way to handle transactions in Django is to wrap each request in a transaction.
Set ATOMIC_REQUESTS
settings to True
in the configuration of each database for
which you want to enable this behavior.
It works like this. Before calling GraphQLView
Django starts a transaction. If the
response is produced without problems, Django commits the transaction. If the view, a
DjangoFormMutation
or a DjangoModelFormMutation
produces an exception, Django
rolls back the transaction.
Warning
While the simplicity of this transaction model is appealing, it also makes it inefficient when traffic increases. Opening a transaction for every request has some overhead. The impact on performance depends on the query patterns of your application and on how well your database handles locking.
Check the next section for a better solution.
A mutation can contain multiple fields, just like a query. There's one important distinction between queries and mutations, other than the name:
While query fields are executed in parallel, mutation fields run in series, one after the other.
This means that if we send two incrementCredits
mutations in one request, the first
is guaranteed to finish before the second begins, ensuring that we don't end up with a
race condition with ourselves.
On the other hand, if the first incrementCredits
runs successfully but the second
one does not, the operation cannot be retried as it is. That's why is a good idea to
run all mutation fields in a transaction, to guarantee all occur or nothing occurs.
To enable this behavior for all databases set the graphene ATOMIC_MUTATIONS
settings
to True
in your settings file:
GRAPHENE = {
# ...
"ATOMIC_MUTATIONS": True,
}
On the contrary, if you want to enable this behavior for a specific database, set
ATOMIC_MUTATIONS
to True
in your database settings:
DATABASES = {
"default": {
# ...
"ATOMIC_MUTATIONS": True,
},
# ...
}
Now, given the following example mutation:
mutation IncreaseCreditsTwice { increaseCredits1: increaseCredits(input: { amount: 10 }) { balance errors { field messages } } increaseCredits2: increaseCredits(input: { amount: -1 }) { balance errors { field messages } } }
The server is going to return something like:
{
"data": {
"increaseCredits1": {
"balance": 10.0,
"errors": []
},
"increaseCredits2": {
"balance": null,
"errors": [
{
"field": "amount",
"message": "Amount should be a positive number"
}
]
},
}
}
But the balance will remain the same.