mirror of
https://github.com/Hopiu/django-cachalot.git
synced 2026-03-16 21:30:23 +00:00
155 lines
5.9 KiB
Python
155 lines
5.9 KiB
Python
from contextlib import contextmanager
|
|
from typing import Any, Optional, Tuple, Union
|
|
|
|
from django.apps import apps
|
|
from django.conf import settings
|
|
from django.db import connections
|
|
|
|
from .cache import cachalot_caches
|
|
from .settings import cachalot_settings
|
|
from .signals import post_invalidation
|
|
from .transaction import AtomicCache
|
|
from .utils import _invalidate_tables
|
|
|
|
|
|
try:
|
|
from asgiref.local import Local
|
|
LOCAL_STORAGE = Local()
|
|
except ImportError:
|
|
import threading
|
|
LOCAL_STORAGE = threading.local()
|
|
|
|
|
|
__all__ = ('invalidate', 'get_last_invalidation', 'cachalot_disabled')
|
|
|
|
|
|
def _cache_db_tables_iterator(tables, cache_alias, db_alias):
|
|
no_tables = not tables
|
|
cache_aliases = settings.CACHES if cache_alias is None else (cache_alias,)
|
|
db_aliases = settings.DATABASES if db_alias is None else (db_alias,)
|
|
for db_alias in db_aliases:
|
|
if no_tables:
|
|
tables = connections[db_alias].introspection.table_names()
|
|
if tables:
|
|
for cache_alias in cache_aliases:
|
|
yield cache_alias, db_alias, tables
|
|
|
|
|
|
def _get_tables(tables_or_models):
|
|
for table_or_model in tables_or_models:
|
|
if isinstance(table_or_model, str) and '.' in table_or_model:
|
|
try:
|
|
table_or_model = apps.get_model(table_or_model)
|
|
except LookupError:
|
|
pass
|
|
yield (table_or_model if isinstance(table_or_model, str)
|
|
else table_or_model._meta.db_table)
|
|
|
|
|
|
def invalidate(
|
|
*tables_or_models: Tuple[Union[str, Any], ...],
|
|
cache_alias: Optional[str] = None,
|
|
db_alias: Optional[str] = None,
|
|
) -> None:
|
|
"""
|
|
Clears what was cached by django-cachalot implying one or more SQL tables
|
|
or models from ``tables_or_models``.
|
|
If ``tables_or_models`` is not specified, all tables found in the database
|
|
(including those outside Django) are invalidated.
|
|
|
|
If ``cache_alias`` is specified, it only clears the SQL queries stored
|
|
on this cache, otherwise queries from all caches are cleared.
|
|
|
|
If ``db_alias`` is specified, it only clears the SQL queries executed
|
|
on this database, otherwise queries from all databases are cleared.
|
|
|
|
:arg tables_or_models: SQL tables names, models or models lookups
|
|
(or a combination)
|
|
:type tables_or_models: tuple of strings or models
|
|
:arg cache_alias: Alias from the Django ``CACHES`` setting
|
|
:arg db_alias: Alias from the Django ``DATABASES`` setting
|
|
:returns: Nothing
|
|
"""
|
|
send_signal = False
|
|
invalidated = set()
|
|
for cache_alias, db_alias, tables in _cache_db_tables_iterator(
|
|
list(_get_tables(tables_or_models)), cache_alias, db_alias):
|
|
cache = cachalot_caches.get_cache(cache_alias, db_alias)
|
|
if not isinstance(cache, AtomicCache):
|
|
send_signal = True
|
|
_invalidate_tables(cache, db_alias, tables)
|
|
invalidated.update(tables)
|
|
|
|
if send_signal:
|
|
for table in invalidated:
|
|
post_invalidation.send(table, db_alias=db_alias)
|
|
|
|
|
|
def get_last_invalidation(
|
|
*tables_or_models: Tuple[Union[str, Any], ...],
|
|
cache_alias: Optional[str] = None,
|
|
db_alias: Optional[str] = None,
|
|
) -> float:
|
|
"""
|
|
Returns the timestamp of the most recent invalidation of the given
|
|
``tables_or_models``. If ``tables_or_models`` is not specified,
|
|
all tables found in the database (including those outside Django) are used.
|
|
|
|
If ``cache_alias`` is specified, it only fetches invalidations
|
|
in this cache, otherwise invalidations in all caches are fetched.
|
|
|
|
If ``db_alias`` is specified, it only fetches invalidations
|
|
for this database, otherwise invalidations for all databases are fetched.
|
|
|
|
:arg tables_or_models: SQL tables names, models or models lookups
|
|
(or a combination)
|
|
:type tables_or_models: tuple of strings or models
|
|
:arg cache_alias: Alias from the Django ``CACHES`` setting
|
|
:arg db_alias: Alias from the Django ``DATABASES`` setting
|
|
:returns: The timestamp of the most recent invalidation
|
|
"""
|
|
last_invalidation = 0.0
|
|
for cache_alias, db_alias, tables in _cache_db_tables_iterator(
|
|
list(_get_tables(tables_or_models)), cache_alias, db_alias):
|
|
get_table_cache_key = cachalot_settings.CACHALOT_TABLE_KEYGEN
|
|
table_cache_keys = [get_table_cache_key(db_alias, t) for t in tables]
|
|
invalidations = cachalot_caches.get_cache(
|
|
cache_alias, db_alias).get_many(table_cache_keys).values()
|
|
if invalidations:
|
|
current_last_invalidation = max(invalidations)
|
|
if current_last_invalidation > last_invalidation:
|
|
last_invalidation = current_last_invalidation
|
|
return last_invalidation
|
|
|
|
|
|
@contextmanager
|
|
def cachalot_disabled(all_queries: bool = False):
|
|
"""
|
|
Context manager for temporarily disabling cachalot.
|
|
If you evaluate the same queryset a second time,
|
|
like normally for Django querysets, this will access
|
|
the variable that saved it in-memory. For example:
|
|
|
|
.. code-block:: python
|
|
|
|
with cachalot_disabled():
|
|
qs = Test.objects.filter(blah=blah)
|
|
# Does a single query to the db
|
|
list(qs) # Evaluates queryset
|
|
# Because the qs was evaluated, it's
|
|
# saved in memory:
|
|
list(qs) # this does 0 queries.
|
|
# This does 1 query to the db
|
|
list(Test.objects.filter(blah=blah))
|
|
|
|
If you evaluate the queryset outside the context manager, any duplicate
|
|
query will use the cached result unless an object creation happens in between
|
|
the original and duplicate query.
|
|
|
|
:arg all_queries: Any query, including already evaluated queries, are re-evaluated.
|
|
"""
|
|
was_enabled = getattr(LOCAL_STORAGE, "cachalot_enabled", cachalot_settings.CACHALOT_ENABLED)
|
|
LOCAL_STORAGE.cachalot_enabled = False
|
|
LOCAL_STORAGE.disable_on_all = all_queries
|
|
yield
|
|
LOCAL_STORAGE.cachalot_enabled = was_enabled
|