Django ORM — замечательная вещь. Это делает доступ к базе данных настолько простым, что иногда вы забываете, что это даже происходит. То есть до тех пор, пока вы не откроете django-debug-toolbar и не увидите, что внезапно запускаете сотни запросов! Мало того, но, глядя на реальные запросы, многие из них являются дубликатами! Вы думаете: «Откуда взялись все эти запросы? Глупые коллеги, не пишущие эффективный код! » Тогда вы неизбежно понимаете, что половина дополнительных запросов была написана вами. Как это произошло?
Это слишком просто. Возможно, у вас есть User
объект с вспомогательным методом, который выполняет соединение, чтобы получить их недавнюю активность. Вы передаете user
экземпляры во многих своих вызовах методов. Чтобы не заключать с вызывающей стороной более широкий контракт, чем необходимо, служебные методы повсюду вызывают этот вспомогательный метод. Ваш код хороший и жесткий; вы никуда не повторяетесь, но некоторые запросы к страницам вызывают эту функцию из разных мест в стеке полдюжины раз!
Почему это так важно? После первого запроса база данных, вероятно, будет иметь хорошую теплую версию в своем кэше. На панели инструментов отладки вы, вероятно, увидите, что многие из ваших повторяющихся запросов будут возвращены менее чем за 2 миллисекунды. Однако любая задержка на сервере базы данных все равно может вас убить . Кроме того, даже крошечные запросы все еще вызывают некоторую конкуренцию и нагрузку на базу данных.
Существуют различные существующие решения для кэширования запросов в Django. Как правило, все они требуют ручного истечения срока действия кеша, если у вас есть крайние случаи, такие как запись данных в вашу базу данных. Другими словами, они могут вносить ошибки.
Я пришел к какому-то патчу для некоторых внутренних компонентов Django для кеширования результатов отдельных операторов SQL, но только внутри жизненного цикла отдельного запроса. Это приведет к нулевой загрузке вашей базы данных, если у вас идеальный код. Для простых смертных это может значительно снизить количество вызовов вашей базы данных.
Вы начинаете с добавления части промежуточного программного обеспечения:
from myapp.utils import query_cache class QueryCacheMiddleware: def process_request(self, request): query_cache.patch()
Затем вы должны включить это промежуточное ПО в settings.py
:
MIDDLEWARE_CLASSES = ( ... 'myapp.middleware.QueryCacheMiddleware',
Наконец, вот сам query_cache
патч.
''' Hack to cache SELECT statements inside a single Django request. The patch() method replaces the Django internal execute_sql method with a stand-in called execute_sql_cache. That method looks at the sql to be run, and if it's a select statement, it checks a thread-local cache first. Only if it's not found in the cache does it proceed to execute the SQL. On any other type of sql statement, it blows away the cache. There is some logic to not cache large result sets, meaning anything over 100 records. This is to preserve Django's lazy query set evaluation. ''' from threading import local import itertools from django.db.models.sql.constants import MULTI from django.db.models.sql.compiler import SQLCompiler from django.db.models.sql.datastructures import EmptyResultSet from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE _thread_locals = local() def get_sql(compiler): ''' get a tuple of the SQL query and the arguments ''' try: return compiler.as_sql() except EmptyResultSet: pass return ('', []) def execute_sql_cache(self, result_type=MULTI): if hasattr(_thread_locals, 'query_cache'): sql = get_sql(self) # ('SELECT * FROM ...', (50)) <= sql string, args tuple if sql[0][:6].upper() == 'SELECT': # uses the tuple of sql + args as the cache key if sql in _thread_locals.query_cache: return _thread_locals.query_cache[sql] result = self._execute_sql(result_type) if hasattr(result, 'next'): # only cache if this is not a full first page of a chunked set peek = result.next() result = list(itertools.chain([peek], result)) if len(peek) == GET_ITERATOR_CHUNK_SIZE: return result _thread_locals.query_cache[sql] = result return result else: # the database has been updated; throw away the cache _thread_locals.query_cache = {} return self._execute_sql(result_type) def patch(): ''' patch the django query runner to use our own method to execute sql ''' _thread_locals.query_cache = {} if not hasattr(SQLCompiler, '_execute_sql'): SQLCompiler._execute_sql = SQLCompiler.execute_sql SQLCompiler.execute_sql = execute_sql_cache
Здесь происходит то, что я заменяю внутренний execute_sql
метод Django оболочкой, которая кэширует результаты в локальном словаре потока. Он кэширует только небольшие наборы результатов. При любом результате, превышающем 100 строк, Django запустит курсор базы данных и генератор. Кэшировать их без активных запросов ко всему набору данных было бы довольно сложно , поэтому в этом случае я помогу. Я заметил, что в моей кодовой базе большинство повторных вызовов относятся к одной записи или небольшому набору записей.
Чтобы не иметь дело с какими-либо хитрыми случаями аннулирования, я просто удаляю кеш, если выполняется какой-либо оператор UPDATE, INSERT или DELETE.
Конечно, это не будет работать, если у вас есть длительные запросы страниц, которые намеренно делают один и тот же запрос снова и снова, ожидая определенного результата.