def cached_as(*samples, **kwargs): """ Caches results of a function and invalidates them same way as given queryset(s). NOTE: Ignores queryset cached ops settings, always caches. """ timeout = kwargs.pop('timeout', None) extra = kwargs.pop('extra', None) key_func = kwargs.pop('key_func', func_cache_key) lock = kwargs.pop('lock', None) if not samples: raise TypeError('Pass a queryset, a model or an object to cache like') if kwargs: raise TypeError('Unexpected keyword arguments %s' % ', '.join(kwargs)) # If we unexpectedly get list instead of queryset return identity decorator. # Paginator could do this when page.object_list is empty. if len(samples) == 1 and isinstance(samples[0], list): return lambda func: func def _get_queryset(sample): if isinstance(sample, Model): queryset = sample.__class__.objects.filter(pk=sample.pk) elif isinstance(sample, type) and issubclass(sample, Model): queryset = sample.objects.all() else: queryset = sample queryset._require_cacheprofile() return queryset querysets = map(_get_queryset, samples) dbs = list({qs.db for qs in querysets}) cond_dnfs = join_with(cat, imap(dnfs, querysets)) key_extra = [qs._cache_key(prefix=False) for qs in querysets] key_extra.append(extra) if not timeout: # TODO: switch to is None on major release timeout = min(qs._cacheprofile['timeout'] for qs in querysets) if lock is None: lock = any(qs._cacheprofile['lock'] for qs in querysets) def decorator(func): @wraps(func) def wrapper(*args, **kwargs): if not settings.CACHEOPS_ENABLED or transaction_states.is_dirty( dbs): return func(*args, **kwargs) prefix = get_prefix(func=func, _cond_dnfs=cond_dnfs, dbs=dbs) cache_key = prefix + 'as:' + key_func(func, args, kwargs, key_extra) with redis_client.getting(cache_key, lock=lock) as cache_data: cache_read.send(sender=None, func=func, hit=cache_data is not None) if cache_data is not None: return pickle.loads(cache_data) else: result = func(*args, **kwargs) cache_thing(prefix, cache_key, result, cond_dnfs, timeout, dbs=dbs) return result return wrapper return decorator
def dnfs(qs): """ Converts query condition tree into a DNF of eq conds. Separately for each alias. Any negations, conditions with lookups other than __exact or __in, conditions on joined models and subrequests are ignored. __in is converted into = or = or = ... """ SOME = object() SOME_TREE = [[(None, None, SOME, True)]] def negate(term): return (term[0], term[1], term[2], not term[3]) def _dnf(where): """ Constructs DNF of where tree consisting of terms in form: (alias, attribute, value, negation) meaning `alias.attribute = value` or `not alias.attribute = value` if negation is False Any conditions other then eq are dropped. """ if isinstance(where, Lookup): # If where.lhs don't refer to a field then don't bother if not hasattr(where.lhs, 'target'): return SOME_TREE # Don't bother with complex right hand side either if isinstance(where.rhs, (QuerySet, Query)): return SOME_TREE # Skip conditions on non-serialized fields if isinstance(where.lhs.target, NOT_SERIALIZED_FIELDS): return SOME_TREE # 1.10: django.db.models.fields.related_lookups.RelatedExact attname = where.lhs.target.attname if isinstance(where, Exact): return [[(where.lhs.alias, attname, where.rhs, True)]] elif isinstance(where, IsNull): return [[(where.lhs.alias, attname, None, where.rhs)]] elif isinstance(where, In) and len(where.rhs) < LONG_DISJUNCTION: return [[(where.lhs.alias, attname, v, True)] for v in where.rhs] else: return SOME_TREE elif isinstance(where, EverythingNode): return [[]] elif isinstance(where, NothingNode): return [] elif isinstance(where, (ExtraWhere, SubqueryConstraint)): return SOME_TREE elif len(where) == 0: return [[]] else: chilren_dnfs = map(_dnf, where.children) if len(chilren_dnfs) == 0: return [[]] elif len(chilren_dnfs) == 1: result = chilren_dnfs[0] else: # Just unite children joined with OR if where.connector == OR: result = cat(chilren_dnfs) # Use Cartesian product to AND children else: result = map(cat, product(*chilren_dnfs)) # Negating and expanding brackets if where.negated: result = [map(negate, p) for p in product(*result)] return result def clean_conj(conj, for_alias): conds = {} for alias, attname, value, negation in conj: # "SOME" conds, negated conds and conds for other aliases should be stripped if value is not SOME and negation and alias == for_alias: # Conjs with fields eq 2 different values will never cause invalidation if attname in conds and conds[attname] != value: return None conds[attname] = value return conds def clean_dnf(tree, aliases): cleaned = [ clean_conj(conj, alias) for conj in tree for alias in aliases ] # Remove deleted conjunctions cleaned = [conj for conj in cleaned if conj is not None] # Any empty conjunction eats up the rest # NOTE: a more elaborate DNF reduction is not really needed, # just keep your querysets sane. if not all(cleaned): return [[]] # To keep all schemes the same we sort conjunctions return cleaned def query_dnf(query): def table_for(alias): if alias in main_alias: return alias return query.alias_map[alias].table_name dnf = _dnf(query.where) # NOTE: we exclude content_type as it never changes and will hold dead invalidation info main_alias = query.model._meta.db_table aliases = {alias for alias, cnt in query.alias_refcount.items() if cnt} \ | {main_alias} - {'django_content_type'} tables = group_by(table_for, aliases) return { table: clean_dnf(dnf, table_aliases) for table, table_aliases in tables.items() } if django.VERSION >= (1, 11) and qs.query.combined_queries: return join_with(cat, (query_dnf(q) for q in qs.query.combined_queries)) else: return query_dnf(qs.query)