mirror of
				https://github.com/graphql-python/graphene-django.git
				synced 2025-10-31 07:57:31 +03:00 
			
		
		
		
	* 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.
 |