Skip to content

Commit 8f63199

Browse files
authored
Handle database transactions (#1039)
* Handle Django database atomic requests * Create and handle database atomic mutations * Make code compatible with Python 2.7 * Code style * Define set_rollback instead of using the one in rest_framework.views because of backward compatibility * Implement mock.patch.dict
1 parent a51c2bf commit 8f63199

File tree

12 files changed

+613
-70
lines changed

12 files changed

+613
-70
lines changed

docs/mutations.rst

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,121 @@ This argument is also sent back to the client with the mutation result
230230
(you do not have to do anything). For services that manage
231231
a pool of many GraphQL requests in bulk, the ``clientIDMutation``
232232
allows you to match up a specific mutation with the response.
233+
234+
235+
236+
Django Database Transactions
237+
----------------------------
238+
239+
Django gives you a few ways to control how database transactions are managed.
240+
241+
Tying transactions to HTTP requests
242+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
243+
244+
A common way to handle transactions in Django is to wrap each request in a transaction.
245+
Set ``ATOMIC_REQUESTS`` settings to ``True`` in the configuration of each database for
246+
which you want to enable this behavior.
247+
248+
It works like this. Before calling ``GraphQLView`` Django starts a transaction. If the
249+
response is produced without problems, Django commits the transaction. If the view, a
250+
``DjangoFormMutation`` or a ``DjangoModelFormMutation`` produces an exception, Django
251+
rolls back the transaction.
252+
253+
.. warning::
254+
255+
While the simplicity of this transaction model is appealing, it also makes it
256+
inefficient when traffic increases. Opening a transaction for every request has some
257+
overhead. The impact on performance depends on the query patterns of your application
258+
and on how well your database handles locking.
259+
260+
Check the next section for a better solution.
261+
262+
Tying transactions to mutations
263+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
264+
265+
A mutation can contain multiple fields, just like a query. There's one important
266+
distinction between queries and mutations, other than the name:
267+
268+
..
269+
270+
`While query fields are executed in parallel, mutation fields run in series, one
271+
after the other.`
272+
273+
This means that if we send two ``incrementCredits`` mutations in one request, the first
274+
is guaranteed to finish before the second begins, ensuring that we don't end up with a
275+
race condition with ourselves.
276+
277+
On the other hand, if the first ``incrementCredits`` runs successfully but the second
278+
one does not, the operation cannot be retried as it is. That's why is a good idea to
279+
run all mutation fields in a transaction, to guarantee all occur or nothing occurs.
280+
281+
To enable this behavior for all databases set the graphene ``ATOMIC_MUTATIONS`` settings
282+
to ``True`` in your settings file:
283+
284+
.. code:: python
285+
286+
GRAPHENE = {
287+
# ...
288+
"ATOMIC_MUTATIONS": True,
289+
}
290+
291+
On the contrary, if you want to enable this behavior for a specific database, set
292+
``ATOMIC_MUTATIONS`` to ``True`` in your database settings:
293+
294+
.. code:: python
295+
296+
DATABASES = {
297+
"default": {
298+
# ...
299+
"ATOMIC_MUTATIONS": True,
300+
},
301+
# ...
302+
}
303+
304+
Now, given the following example mutation:
305+
306+
.. code::
307+
308+
mutation IncreaseCreditsTwice {
309+
310+
increaseCredits1: increaseCredits(input: { amount: 10 }) {
311+
balance
312+
errors {
313+
field
314+
messages
315+
}
316+
}
317+
318+
increaseCredits2: increaseCredits(input: { amount: -1 }) {
319+
balance
320+
errors {
321+
field
322+
messages
323+
}
324+
}
325+
326+
}
327+
328+
The server is going to return something like:
329+
330+
.. code:: json
331+
332+
{
333+
"data": {
334+
"increaseCredits1": {
335+
"balance": 10.0,
336+
"errors": []
337+
},
338+
"increaseCredits2": {
339+
"balance": null,
340+
"errors": [
341+
{
342+
"field": "amount",
343+
"message": "Amount should be a positive number"
344+
}
345+
]
346+
},
347+
}
348+
}
349+
350+
But the balance will remain the same.

graphene_django/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
MUTATION_ERRORS_FLAG = "graphene_mutation_has_errors"

graphene_django/forms/mutation.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@
1111
# InputObjectType,
1212
# )
1313
from graphene.types.utils import yank_fields_from_attrs
14+
from graphene_django.constants import MUTATION_ERRORS_FLAG
1415
from graphene_django.registry import get_global_registry
1516

17+
18+
from django.core.exceptions import ValidationError
19+
from django.db import connection
20+
1621
from ..types import ErrorType
1722
from .converter import convert_form_field
1823

@@ -46,6 +51,7 @@ def mutate_and_get_payload(cls, root, info, **input):
4651
return cls.perform_mutate(form, info)
4752
else:
4853
errors = ErrorType.from_errors(form.errors)
54+
_set_errors_flag_to_context(info)
4955

5056
return cls(errors=errors, **form.data)
5157

@@ -170,6 +176,7 @@ def mutate_and_get_payload(cls, root, info, **input):
170176
return cls.perform_mutate(form, info)
171177
else:
172178
errors = ErrorType.from_errors(form.errors)
179+
_set_errors_flag_to_context(info)
173180

174181
return cls(errors=errors)
175182

@@ -178,3 +185,9 @@ def perform_mutate(cls, form, info):
178185
obj = form.save()
179186
kwargs = {cls._meta.return_field_name: obj}
180187
return cls(errors=[], **kwargs)
188+
189+
190+
def _set_errors_flag_to_context(info):
191+
# This is not ideal but necessary to keep the response errors empty
192+
if info and info.context:
193+
setattr(info.context, MUTATION_ERRORS_FLAG, True)

0 commit comments

Comments
 (0)