Ticket #34413: prefetching_the_earliest_latest_related_object_.patch

File prefetching_the_earliest_latest_related_object_.patch, 8.1 KB (added by Willem Van Onsem, 20 months ago)
  • django/db/models/query.py

    Subject: [PATCH] prefetching the earliest/latest related object.
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/django/db/models/query.py b/django/db/models/query.py
    a b  
    2323from django.db.models import AutoField, DateField, DateTimeField, Field, sql
    2424from django.db.models.constants import LOOKUP_SEP, OnConflict
    2525from django.db.models.deletion import Collector
    26 from django.db.models.expressions import Case, F, Value, When
     26from django.db.models.expressions import Case, F, Value, When, Subquery, OuterRef
    2727from django.db.models.functions import Cast, Trunc
    2828from django.db.models.query_utils import FilteredRelation, Q
    2929from django.db.models.sql.constants import CURSOR, GET_ITERATOR_CHUNK_SIZE
     
    15571557        if lookups == (None,):
    15581558            clone._prefetch_related_lookups = ()
    15591559        else:
     1560            to_annotate = {}
    15601561            for lookup in lookups:
    15611562                if isinstance(lookup, Prefetch):
    15621563                    lookup = lookup.prefetch_to
     1564                elif isinstance(lookup, PrefetchEarliest):
     1565                    to_annotate[lookup.annotate_name()] = lookup.field_subquery(self.model)
     1566                    lookup = lookup.to_attr or lookup.prefetch_through.replace(LOOKUP_SEP, '_')
    15631567                lookup = lookup.split(LOOKUP_SEP, 1)[0]
    15641568                if lookup in self.query._filtered_relations:
    15651569                    raise ValueError(
    15661570                        "prefetch_related() is not supported with FilteredRelation."
    15671571                    )
    15681572            clone._prefetch_related_lookups = clone._prefetch_related_lookups + lookups
     1573            if to_annotate:
     1574                clone = clone.annotate(**to_annotate)
    15691575        return clone
    15701576
    15711577    def annotate(self, *args, **kwargs):
     
    21312137        return model_fields
    21322138
    21332139
    2134 class Prefetch:
     2140
     2141class PrefetchBase:
    21352142    def __init__(self, lookup, queryset=None, to_attr=None):
    21362143        # `prefetch_through` is the path we traverse to perform the prefetch.
    2137         self.prefetch_through = lookup
    21382144        # `prefetch_to` is the path to the attribute that stores the result.
    21392145        self.prefetch_to = lookup
    21402146        if queryset is not None and (
    2141             isinstance(queryset, RawQuerySet)
    2142             or (
    2143                 hasattr(queryset, "_iterable_class")
    2144                 and not issubclass(queryset._iterable_class, ModelIterable)
    2145             )
     2147            isinstance(queryset, RawQuerySet) or (not issubclass(getattr(queryset, '_iterable_class', None), ModelIterable))
    21462148        ):
    21472149            raise ValueError(
    21482150                "Prefetch querysets cannot use raw(), values(), and values_list()."
     
    21832185            return self.queryset
    21842186        return None
    21852187
     2188    def __hash__(self):
     2189        return hash((self.__class__, self.prefetch_to))
     2190
     2191
     2192class Prefetch(PrefetchBase):
     2193
    21862194    def __eq__(self, other):
    21872195        if not isinstance(other, Prefetch):
    21882196            return NotImplemented
    21892197        return self.prefetch_to == other.prefetch_to
    21902198
    2191     def __hash__(self):
    2192         return hash((self.__class__, self.prefetch_to))
     2199
     2200class PrefetchEarliest(PrefetchBase):
     2201    def __init__(self, lookup, to_attr, queryset=None,
     2202                 ordering=None, join_field='pk'):
     2203        super().__init__(lookup, queryset, to_attr)
     2204        if not isinstance(ordering, (type(None), list, tuple)):
     2205            raise TypeError(
     2206                'If the ordering is specified, it must be a list or tuple of fields'
     2207            )
     2208        self.ordering = ordering
     2209        if not isinstance(join_field, str):
     2210            raise TypeError(
     2211                'The join_field must be string.'
     2212            )
     2213        self.join_field = join_field
     2214
     2215    def annotate_name(self):
     2216        return f'_subquery_{id(self)}'
     2217
     2218    def get_fallback_queryset(self, model):
     2219        for item in self.prefetch_through.split(LOOKUP_SEP):
     2220            if item == 'pk':
     2221                item = model._meta.pk.name
     2222            model = model._meta.get_field(item).related_model
     2223        return model._base_manager.all()
     2224
     2225    def reverse_lookup(self, source_model):
     2226        opts = source_model._meta
     2227        reverse = []
     2228        target_field = None
     2229        for item in self.prefetch_through.split(LOOKUP_SEP):
     2230            if item == 'pk':
     2231                item = opts.pk.name
     2232            relation = opts.get_field(item)
     2233            if target_field is None:
     2234                target_field = relation.remote_field.target_field.name
     2235            opts = relation.related_model._meta
     2236            from django.db.models import ForeignObjectRel
     2237            if isinstance(relation, ForeignObjectRel):
     2238                revrel = relation.field.name
     2239            else:
     2240                revrel = relation.related_query_name()
     2241            reverse.append(revrel)
     2242
     2243        return Q((LOOKUP_SEP.join(reversed(reverse)), OuterRef(target_field)))
     2244
     2245    def prepare_queryset(self, source_model):
     2246        queryset = self.queryset or self.get_fallback_queryset(source_model)
     2247        queryset = queryset.filter(self.reverse_lookup(source_model))
     2248        if self.ordering is not None:
     2249            return queryset.order_by(*self.ordering)
     2250        # if not, it will be ordered by the queryset itself, or the model (hopefully)
     2251        return queryset
     2252
     2253    def sliced_values(self, qs):
     2254        return qs.values(self.join_field)[:1]
     2255
     2256
     2257    def field_subquery(self, source_model):
     2258        return Subquery(
     2259            self.sliced_values(self.prepare_queryset(source_model))
     2260        )
     2261
     2262    def join_in_objects(self, source_model, items):
     2263        queryset = self.queryset or self.get_fallback_queryset(source_model)
     2264        join_field = self.join_field
     2265        # N.B.: the same value might be used by multiple items
     2266        attribute_name = self.annotate_name()
     2267        item_pks = {getattr(item, attribute_name) for item in items}
     2268        result = {
     2269            getattr(element, join_field): element
     2270            for element in queryset.filter(Q((
     2271                f'{self.join_field}__in', item_pks
     2272            )))
     2273        }
     2274        target = self.to_attr or self.prefetch_through
     2275        for item in items:
     2276            setattr(item, target, result.get(getattr(item, attribute_name)))
     2277
     2278
     2279
     2280
     2281class PrefetchLatest(PrefetchEarliest):
     2282    def prepare_queryset(self, *args, **kwargs):
     2283        # reverse the ordering, such that we take the latest instead of the earliest
     2284        return super().prepare_queryset(*args, **kwargs).reverse()
    21932285
    21942286
    21952287def normalize_prefetch_lookups(lookups, prefix=None):
    21962288    """Normalize lookups into Prefetch objects."""
    21972289    ret = []
    21982290    for lookup in lookups:
    2199         if not isinstance(lookup, Prefetch):
     2291        if not isinstance(lookup, PrefetchBase):
    22002292            lookup = Prefetch(lookup)
    22012293        if prefix:
    22022294            lookup.add_prefix(prefix)
     
    22132305        return  # nothing to do
    22142306
    22152307    # We need to be able to dynamically add to the list of prefetch_related
    2216     # lookups that we look up (see below).  So we need some book keeping to
     2308    # lookups that we look up (see below).  So we need some bookkeeping to
    22172309    # ensure we don't do duplicate work.
    22182310    done_queries = {}  # dictionary of things like 'foo__bar': [results]
    22192311
     
    22322324                )
    22332325
    22342326            continue
     2327        if isinstance(lookup, PrefetchEarliest):
     2328            lookup.join_in_objects(type(model_instances[0]), model_instances)
     2329            continue
    22352330
    22362331        # Top level, the list of objects to decorate is the result cache
    22372332        # from the primary QuerySet. It won't be for deeper levels.
     
    22692364            if not good_objects:
    22702365                break
    22712366
    2272             # Descend down tree
     2367            # Descend down the tree
    22732368
    22742369            # We assume that objects retrieved are homogeneous (which is the premise
    22752370            # of prefetch_related), so what applies to first object applies to all.
Back to Top