Ticket #34413: prefetching_the_earliest_latest_related_object_.patch
File prefetching_the_earliest_latest_related_object_.patch, 8.1 KB (added by , 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 23 23 from django.db.models import AutoField, DateField, DateTimeField, Field, sql 24 24 from django.db.models.constants import LOOKUP_SEP, OnConflict 25 25 from django.db.models.deletion import Collector 26 from django.db.models.expressions import Case, F, Value, When 26 from django.db.models.expressions import Case, F, Value, When, Subquery, OuterRef 27 27 from django.db.models.functions import Cast, Trunc 28 28 from django.db.models.query_utils import FilteredRelation, Q 29 29 from django.db.models.sql.constants import CURSOR, GET_ITERATOR_CHUNK_SIZE … … 1557 1557 if lookups == (None,): 1558 1558 clone._prefetch_related_lookups = () 1559 1559 else: 1560 to_annotate = {} 1560 1561 for lookup in lookups: 1561 1562 if isinstance(lookup, Prefetch): 1562 1563 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, '_') 1563 1567 lookup = lookup.split(LOOKUP_SEP, 1)[0] 1564 1568 if lookup in self.query._filtered_relations: 1565 1569 raise ValueError( 1566 1570 "prefetch_related() is not supported with FilteredRelation." 1567 1571 ) 1568 1572 clone._prefetch_related_lookups = clone._prefetch_related_lookups + lookups 1573 if to_annotate: 1574 clone = clone.annotate(**to_annotate) 1569 1575 return clone 1570 1576 1571 1577 def annotate(self, *args, **kwargs): … … 2131 2137 return model_fields 2132 2138 2133 2139 2134 class Prefetch: 2140 2141 class PrefetchBase: 2135 2142 def __init__(self, lookup, queryset=None, to_attr=None): 2136 2143 # `prefetch_through` is the path we traverse to perform the prefetch. 2137 self.prefetch_through = lookup2138 2144 # `prefetch_to` is the path to the attribute that stores the result. 2139 2145 self.prefetch_to = lookup 2140 2146 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)) 2146 2148 ): 2147 2149 raise ValueError( 2148 2150 "Prefetch querysets cannot use raw(), values(), and values_list()." … … 2183 2185 return self.queryset 2184 2186 return None 2185 2187 2188 def __hash__(self): 2189 return hash((self.__class__, self.prefetch_to)) 2190 2191 2192 class Prefetch(PrefetchBase): 2193 2186 2194 def __eq__(self, other): 2187 2195 if not isinstance(other, Prefetch): 2188 2196 return NotImplemented 2189 2197 return self.prefetch_to == other.prefetch_to 2190 2198 2191 def __hash__(self): 2192 return hash((self.__class__, self.prefetch_to)) 2199 2200 class 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 2281 class 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() 2193 2285 2194 2286 2195 2287 def normalize_prefetch_lookups(lookups, prefix=None): 2196 2288 """Normalize lookups into Prefetch objects.""" 2197 2289 ret = [] 2198 2290 for lookup in lookups: 2199 if not isinstance(lookup, Prefetch ):2291 if not isinstance(lookup, PrefetchBase): 2200 2292 lookup = Prefetch(lookup) 2201 2293 if prefix: 2202 2294 lookup.add_prefix(prefix) … … 2213 2305 return # nothing to do 2214 2306 2215 2307 # 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 2308 # lookups that we look up (see below). So we need some bookkeeping to 2217 2309 # ensure we don't do duplicate work. 2218 2310 done_queries = {} # dictionary of things like 'foo__bar': [results] 2219 2311 … … 2232 2324 ) 2233 2325 2234 2326 continue 2327 if isinstance(lookup, PrefetchEarliest): 2328 lookup.join_in_objects(type(model_instances[0]), model_instances) 2329 continue 2235 2330 2236 2331 # Top level, the list of objects to decorate is the result cache 2237 2332 # from the primary QuerySet. It won't be for deeper levels. … … 2269 2364 if not good_objects: 2270 2365 break 2271 2366 2272 # Descend down t ree2367 # Descend down the tree 2273 2368 2274 2369 # We assume that objects retrieved are homogeneous (which is the premise 2275 2370 # of prefetch_related), so what applies to first object applies to all.