mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-28 12:33:43 +03:00
be17278b49
* Add DjangoFormInputObjectType to forms/types InputObjectType derived class which gets fields from django form. Type of fields with choices (converted to enum) is set to custom scalar type (using Meta.object_type) to dynamically convert enum values back. * Correct Reporter model a_choice field type according to CHOICES tuple * Add tests for DjangoFormInputObjectType * Add pyenv files to .gitignore * Fix pyupgrade * Fix tests * Add docs * Fix docs example --------- Co-authored-by: Firas Kafri <3097061+firaskafri@users.noreply.github.com>
402 lines
12 KiB
ReStructuredText
402 lines
12 KiB
ReStructuredText
Mutations
|
|
=========
|
|
|
|
Introduction
|
|
------------
|
|
|
|
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 <https://docs.graphene-python.org/en/latest/types/mutations/>`__
|
|
features to add custom mutations to a Django project.
|
|
|
|
Simple example
|
|
--------------
|
|
|
|
.. code:: python
|
|
|
|
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()
|
|
|
|
|
|
Django Forms
|
|
------------
|
|
|
|
Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation.
|
|
|
|
DjangoFormMutation
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
.. code:: python
|
|
|
|
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
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
``DjangoModelFormMutation`` will pull the fields from a ``ModelForm``.
|
|
|
|
.. code:: python
|
|
|
|
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).
|
|
|
|
.. code:: python
|
|
|
|
class PetMutation(DjangoModelFormMutation):
|
|
class Meta:
|
|
form_class = PetForm
|
|
input_field_name = 'data'
|
|
return_field_name = 'my_pet'
|
|
|
|
Form validation
|
|
~~~~~~~~~~~~~~~
|
|
|
|
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
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
``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.
|
|
|
|
.. code:: python
|
|
|
|
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 type ``convert_choices_to_enum`` ``Meta`` class attribute is not set to ``False``. Any data field, which have choices in django, with value ``A_1`` (for example) from client will be automatically converted to ``1`` in mutation data.
|
|
* ``add_id_field_name`` is used to specify `id` field name (not required, by default equal to ``id``)
|
|
* ``add_id_field_type`` is used to specify `id` field type (not required, default is ``graphene.ID``)
|
|
|
|
Django REST Framework
|
|
---------------------
|
|
|
|
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:
|
|
|
|
.. code:: python
|
|
|
|
from graphene_django.rest_framework.mutation import SerializerMutation
|
|
|
|
class MyAwesomeMutation(SerializerMutation):
|
|
class Meta:
|
|
serializer_class = MySerializer
|
|
|
|
|
|
Create/Update Operations
|
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
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'
|
|
|
|
Overriding Update Queries
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Use the method ``get_serializer_kwargs`` to override how updates are applied.
|
|
|
|
.. code:: python
|
|
|
|
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}
|
|
|
|
|
|
|
|
Relay
|
|
-----
|
|
|
|
You can use relay with mutations. A Relay mutation must inherit from
|
|
``ClientIDMutation`` and implement the ``mutate_and_get_payload`` method:
|
|
|
|
.. code:: python
|
|
|
|
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 Database Transactions
|
|
----------------------------
|
|
|
|
Django gives you a few ways to control how database transactions are managed.
|
|
|
|
Tying transactions to HTTP requests
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
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.
|
|
|
|
Tying transactions to mutations
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
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:
|
|
|
|
.. code:: python
|
|
|
|
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:
|
|
|
|
.. code:: python
|
|
|
|
DATABASES = {
|
|
"default": {
|
|
# ...
|
|
"ATOMIC_MUTATIONS": True,
|
|
},
|
|
# ...
|
|
}
|
|
|
|
Now, given the following example mutation:
|
|
|
|
.. code::
|
|
|
|
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:
|
|
|
|
.. code:: json
|
|
|
|
{
|
|
"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.
|