summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorShai Berger <shai@platonix.com>2017-09-21 19:13:09 +0300
committerTim Graham <timograham@gmail.com>2017-09-21 12:13:09 -0400
commitd612026c37f85769322a44c0284ca2d2be6b6e5b (patch)
tree3b8e13157e72dba25878837d055706f135181ba1 /docs
parent347551c2a1bc78b3a5886be6adde0c8151001acc (diff)
Refs #28595 -- Added a hook to add execute wrappers for database queries.
Thanks Adam Johnson, Carl Meyer, Anssi Kääriäinen, Mariusz Felisiak, Michael Manfre, and Tim Graham for discussion and review.
Diffstat (limited to 'docs')
-rw-r--r--docs/releases/2.0.txt5
-rw-r--r--docs/topics/db/index.txt1
-rw-r--r--docs/topics/db/instrumentation.txt114
3 files changed, 120 insertions, 0 deletions
diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt
index 71d41b664c..77eee0c206 100644
--- a/docs/releases/2.0.txt
+++ b/docs/releases/2.0.txt
@@ -339,6 +339,11 @@ Models
parameters, if the backend supports this feature. Of Django's built-in
backends, only Oracle supports it.
+* The new :meth:`connection.execute_wrapper()
+ <django.db.backends.base.DatabaseWrapper.execute_wrapper>` method allows
+ :doc:`installing wrappers around execution of database queries
+ </topics/db/instrumentation>`.
+
* The new ``filter`` argument for built-in aggregates allows :ref:`adding
different conditionals <conditional-aggregation>` to multiple aggregations
over the same fields or relations.
diff --git a/docs/topics/db/index.txt b/docs/topics/db/index.txt
index 51f60a65d7..032ec30bb5 100644
--- a/docs/topics/db/index.txt
+++ b/docs/topics/db/index.txt
@@ -21,4 +21,5 @@ model maps to a single database table.
multi-db
tablespaces
optimization
+ instrumentation
examples/index
diff --git a/docs/topics/db/instrumentation.txt b/docs/topics/db/instrumentation.txt
new file mode 100644
index 0000000000..9578c1224b
--- /dev/null
+++ b/docs/topics/db/instrumentation.txt
@@ -0,0 +1,114 @@
+========================
+Database instrumentation
+========================
+
+.. versionadded:: 2.0
+
+To help you understand and control the queries issued by your code, Django
+provides a hook for installing wrapper functions around the execution of
+database queries. For example, wrappers can count queries, measure query
+duration, log queries, or even prevent query execution (e.g. to make sure that
+no queries are issued while rendering a template with prefetched data).
+
+The wrappers are modeled after :doc:`middleware </topics/http/middleware>` --
+they are callables which take another callable as one of their arguments. They
+call that callable to invoke the (possibly wrapped) database query, and they
+can do what they want around that call. They are, however, created and
+installed by user code, and so don't need a separate factory like middleware do.
+
+Installing a wrapper is done in a context manager -- so the wrappers are
+temporary and specific to some flow in your code.
+
+As mentioned above, an example of a wrapper is a query execution blocker. It
+could look like this::
+
+ def blocker(*args):
+ raise Exception('No database access allowed here.')
+
+And it would be used in a view to block queries from the template like so::
+
+ from django.db import connection
+ from django.shortcuts import render
+
+ def my_view(request):
+ context = {...} # Code to generate context with all data.
+ template_name = ...
+ with connection.execute_wrapper(blocker):
+ return render(request, template_name, context)
+
+The parameters sent to the wrappers are:
+
+* ``execute`` -- a callable, which should be invoked with the rest of the
+ parameters in order to execute the query.
+
+* ``sql`` -- a ``str``, the SQL query to be sent to the database.
+
+* ``params`` -- a list/tuple of parameter values for the SQL command, or a
+ list/tuple of lists/tuples if the wrapped call is ``executemany()``.
+
+* ``many`` -- a ``bool`` indicating whether the ultimately invoked call is
+ ``execute()`` or ``executemany()`` (and whether ``params`` is expected to be
+ a sequence of values, or a sequence of sequences of values).
+
+* ``context`` -- a dictionary with further data about the context of
+ invocation. This includes the connection and cursor.
+
+Using the parameters, a slightly more complex version of the blocker could
+include the connection name in the error message::
+
+ def blocker(execute, sql, params, many, context):
+ alias = context['connection'].alias
+ raise Exception("Access to database '{}' blocked here".format(alias))
+
+For a more complete example, a query logger could look like this::
+
+ import time
+
+ class QueryLogger:
+
+ def __init__(self):
+ self.queries = []
+
+ def __call__(self, execute, sql, params, many, context):
+ current_query = {'sql': sql, 'params': params, 'many': many}
+ start = time.time()
+ try:
+ result = execute(sql, params, many, context)
+ except Exception as e:
+ current_query['status'] = 'error'
+ current_query['exception'] = e
+ raise
+ else:
+ current_query['status'] = 'ok'
+ return result
+ finally:
+ duration = time.time() - start
+ current_query['duration'] = duration
+ self.queries.append(current_query)
+
+To use this, you would create a logger object and install it as a wrapper::
+
+ from django.db import connection
+
+ ql = QueryLogger()
+ with connection.execute_wrapper(ql):
+ do_queries()
+ # Now we can print the log.
+ print(ql.queries)
+
+.. currentmodule:: django.db.backends.base.DatabaseWrapper
+
+``connection.execute_wrapper()``
+--------------------------------
+
+.. method:: execute_wrapper(wrapper)
+
+Returns a context manager which, when entered, installs a wrapper around
+database query executions, and when exited, removes the wrapper. The wrapper is
+installed on the thread-local connection object.
+
+``wrapper`` is a callable taking five arguments. It is called for every query
+execution in the scope of the context manager, with arguments ``execute``,
+``sql``, ``params``, ``many``, and ``context`` as described above. It's
+expected to call ``execute(sql, params, many, context)`` and return the return
+value of that call.