mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-06 21:33:29 +03:00
Create and handle database atomic mutations
This commit is contained in:
parent
09c5215abe
commit
7bd29acde6
|
@ -255,3 +255,95 @@ rolls back the transaction.
|
||||||
inefficient when traffic increases. Opening a transaction for every request has some
|
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
|
overhead. The impact on performance depends on the query patterns of your application
|
||||||
and on how well your database handles locking.
|
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.
|
||||||
|
|
|
@ -45,6 +45,7 @@ DEFAULTS = {
|
||||||
# This sets headerEditorEnabled GraphiQL option, for details go to
|
# This sets headerEditorEnabled GraphiQL option, for details go to
|
||||||
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
|
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
|
||||||
"GRAPHIQL_HEADER_EDITOR_ENABLED": True,
|
"GRAPHIQL_HEADER_EDITOR_ENABLED": True,
|
||||||
|
"ATOMIC_MUTATIONS": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
|
@ -6,6 +6,8 @@ from mock import patch
|
||||||
|
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
|
from graphene_django.settings import graphene_settings
|
||||||
|
|
||||||
from .models import Pet
|
from .models import Pet
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -567,9 +569,13 @@ def test_passes_request_into_context_request(client):
|
||||||
|
|
||||||
|
|
||||||
def test_form_mutation_multiple_creation_invalid_atomic_request(client):
|
def test_form_mutation_multiple_creation_invalid_atomic_request(client):
|
||||||
|
old_atomic_mutations = connection.settings_dict.get("ATOMIC_MUTATIONS", False)
|
||||||
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
||||||
|
old_graphene_atomic_mutations = graphene_settings.ATOMIC_MUTATIONS
|
||||||
try:
|
try:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = False
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = True
|
connection.settings_dict["ATOMIC_REQUESTS"] = True
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = False
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
mutation PetMutations {
|
mutation PetMutations {
|
||||||
|
@ -602,13 +608,109 @@ def test_form_mutation_multiple_creation_invalid_atomic_request(client):
|
||||||
assert Pet.objects.count() == 0
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = old_atomic_mutations
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = old_graphene_atomic_mutations
|
||||||
|
|
||||||
|
|
||||||
def test_form_mutation_multiple_creation_invalid_non_atomic_request(client):
|
def test_form_mutation_multiple_creation_invalid_atomic_mutation_1(client):
|
||||||
|
old_atomic_mutations = connection.settings_dict.get("ATOMIC_MUTATIONS", False)
|
||||||
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
||||||
|
old_graphene_atomic_mutations = graphene_settings.ATOMIC_MUTATIONS
|
||||||
try:
|
try:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = True
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = False
|
connection.settings_dict["ATOMIC_REQUESTS"] = False
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = False
|
||||||
|
|
||||||
|
query = """
|
||||||
|
mutation PetMutations {
|
||||||
|
petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = client.post(url_string(query=query))
|
||||||
|
content = response_json(response)
|
||||||
|
|
||||||
|
assert "errors" not in content
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation1"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation2"]["errors"] == []
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
|
finally:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = old_atomic_mutations
|
||||||
|
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = old_graphene_atomic_mutations
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_mutation_multiple_creation_invalid_atomic_mutation_2(client):
|
||||||
|
old_atomic_mutations = connection.settings_dict.get("ATOMIC_MUTATIONS", False)
|
||||||
|
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
||||||
|
old_graphene_atomic_mutations = graphene_settings.ATOMIC_MUTATIONS
|
||||||
|
try:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = False
|
||||||
|
connection.settings_dict["ATOMIC_REQUESTS"] = False
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = True
|
||||||
|
|
||||||
|
query = """
|
||||||
|
mutation PetMutations {
|
||||||
|
petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = client.post(url_string(query=query))
|
||||||
|
content = response_json(response)
|
||||||
|
|
||||||
|
assert "errors" not in content
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation1"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation2"]["errors"] == []
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
|
finally:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = old_atomic_mutations
|
||||||
|
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = old_graphene_atomic_mutations
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_mutation_multiple_creation_invalid_non_atomic(client):
|
||||||
|
old_atomic_mutations = connection.settings_dict.get("ATOMIC_MUTATIONS", False)
|
||||||
|
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
||||||
|
old_graphene_atomic_mutations = graphene_settings.ATOMIC_MUTATIONS
|
||||||
|
try:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = False
|
||||||
|
connection.settings_dict["ATOMIC_REQUESTS"] = False
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = False
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
mutation PetMutations {
|
mutation PetMutations {
|
||||||
|
@ -645,13 +747,19 @@ def test_form_mutation_multiple_creation_invalid_non_atomic_request(client):
|
||||||
assert pet.age == 0
|
assert pet.age == 0
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = old_atomic_mutations
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = old_graphene_atomic_mutations
|
||||||
|
|
||||||
|
|
||||||
def test_model_form_mutation_multiple_creation_invalid_atomic_request(client):
|
def test_model_form_mutation_multiple_creation_invalid_atomic_request(client):
|
||||||
|
old_atomic_mutations = connection.settings_dict.get("ATOMIC_MUTATIONS", False)
|
||||||
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
||||||
|
old_graphene_atomic_mutations = graphene_settings.ATOMIC_MUTATIONS
|
||||||
try:
|
try:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = False
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = True
|
connection.settings_dict["ATOMIC_REQUESTS"] = True
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = False
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
mutation PetMutations {
|
mutation PetMutations {
|
||||||
|
@ -693,13 +801,19 @@ def test_model_form_mutation_multiple_creation_invalid_atomic_request(client):
|
||||||
assert Pet.objects.count() == 0
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = old_atomic_mutations
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = old_graphene_atomic_mutations
|
||||||
|
|
||||||
|
|
||||||
def test_model_form_mutation_multiple_creation_invalid_non_atomic_request(client):
|
def test_model_form_mutation_multiple_creation_invalid_non_atomic(client):
|
||||||
|
old_atomic_mutations = connection.settings_dict.get("ATOMIC_MUTATIONS", False)
|
||||||
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
||||||
|
old_graphene_atomic_mutations = graphene_settings.ATOMIC_MUTATIONS
|
||||||
try:
|
try:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = False
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = False
|
connection.settings_dict["ATOMIC_REQUESTS"] = False
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = False
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
mutation PetMutations {
|
mutation PetMutations {
|
||||||
|
@ -745,30 +859,44 @@ def test_model_form_mutation_multiple_creation_invalid_non_atomic_request(client
|
||||||
assert pet.age == 0
|
assert pet.age == 0
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = old_atomic_mutations
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = old_graphene_atomic_mutations
|
||||||
|
|
||||||
|
|
||||||
@patch("rest_framework.views.transaction.set_rollback")
|
@patch("rest_framework.views.transaction.set_rollback")
|
||||||
def test_query_errors_atomic_request(set_rollback_mock, client):
|
def test_query_errors_atomic_request(set_rollback_mock, client):
|
||||||
|
old_atomic_mutations = connection.settings_dict.get("ATOMIC_MUTATIONS", False)
|
||||||
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
||||||
|
old_graphene_atomic_mutations = graphene_settings.ATOMIC_MUTATIONS
|
||||||
try:
|
try:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = False
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = True
|
connection.settings_dict["ATOMIC_REQUESTS"] = True
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = False
|
||||||
|
|
||||||
client.get(url_string(query="force error"))
|
client.get(url_string(query="force error"))
|
||||||
set_rollback_mock.assert_called_once_with(True)
|
set_rollback_mock.assert_called_once_with(True)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = old_atomic_mutations
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = old_graphene_atomic_mutations
|
||||||
|
|
||||||
|
|
||||||
@patch("rest_framework.views.transaction.set_rollback")
|
@patch("rest_framework.views.transaction.set_rollback")
|
||||||
def test_query_errors_non_atomic_request(set_rollback_mock, client):
|
def test_query_errors_non_atomic(set_rollback_mock, client):
|
||||||
|
old_atomic_mutations = connection.settings_dict.get("ATOMIC_MUTATIONS", False)
|
||||||
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
old_atomic_requests = connection.settings_dict["ATOMIC_REQUESTS"]
|
||||||
|
old_graphene_atomic_mutations = graphene_settings.ATOMIC_MUTATIONS
|
||||||
try:
|
try:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = False
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = False
|
connection.settings_dict["ATOMIC_REQUESTS"] = False
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = False
|
||||||
|
|
||||||
client.get(url_string(query="force error"))
|
client.get(url_string(query="force error"))
|
||||||
set_rollback_mock.assert_not_called()
|
set_rollback_mock.assert_not_called()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
connection.settings_dict["ATOMIC_MUTATIONS"] = old_atomic_mutations
|
||||||
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
connection.settings_dict["ATOMIC_REQUESTS"] = old_atomic_requests
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS = old_graphene_atomic_mutations
|
||||||
|
|
|
@ -3,6 +3,7 @@ import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
from django.db import connection, transaction
|
||||||
from django.http import HttpResponse, HttpResponseNotAllowed
|
from django.http import HttpResponse, HttpResponseNotAllowed
|
||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
@ -320,14 +321,27 @@ class GraphQLView(View):
|
||||||
# executor is not a valid argument in all backends
|
# executor is not a valid argument in all backends
|
||||||
extra_options["executor"] = self.executor
|
extra_options["executor"] = self.executor
|
||||||
|
|
||||||
return document.execute(
|
operation_type = document.get_operation_type(operation_name)
|
||||||
root_value=self.get_root_value(request),
|
options = {
|
||||||
variable_values=variables,
|
"root_value": self.get_root_value(request),
|
||||||
operation_name=operation_name,
|
"variable_values": variables,
|
||||||
context_value=self.get_context(request),
|
"operation_name": operation_name,
|
||||||
middleware=self.get_middleware(request),
|
"context_value": self.get_context(request),
|
||||||
**extra_options
|
"middleware": self.get_middleware(request),
|
||||||
)
|
**extra_options,
|
||||||
|
}
|
||||||
|
|
||||||
|
if operation_type == "mutation" and (
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS is True
|
||||||
|
or connection.settings_dict.get("ATOMIC_MUTATIONS", False) is True
|
||||||
|
):
|
||||||
|
with transaction.atomic():
|
||||||
|
result = document.execute(**options)
|
||||||
|
if getattr(request, MUTATION_ERRORS_FLAG, False) is True:
|
||||||
|
transaction.set_rollback(True)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return document.execute(**options)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ExecutionResult(errors=[e], invalid=True)
|
return ExecutionResult(errors=[e], invalid=True)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user