Ticket #7270: django_select_related_onetoone_r10454.patch

File django_select_related_onetoone_r10454.patch, 22.4 KB (added by Ben Davis, 15 years ago)

Updated to always use LEFT OUTER JOINs, added back tests

  • django/db/models/sql/query.py

     
    13911391            self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1,
    13921392                    used, next, restricted, new_nullable, dupe_set, avoid)
    13931393
     1394        # Do reverse columns, but only the ones in the requested list.
     1395        if restricted:
     1396            related_fields = [(x.field, x.model) for x in opts.get_all_related_objects() if x.field.unique]
     1397            for f, model in related_fields:
     1398                if f.rel.parent_link or f.related_query_name() not in requested:
     1399                    continue
     1400                table = model._meta.db_table
     1401                alias = self.join((root_alias, table, f.rel.get_related_field().column,
     1402                        f.column), exclusions=used,
     1403                        promote=True, reuse=used)
     1404                used.add(alias)
     1405 
     1406                self.related_select_cols.extend([(alias, f2.column)
     1407                        for f2 in model._meta.fields])
     1408                self.related_select_fields.extend(model._meta.fields)
     1409 
     1410                next = requested.get(f.related_query_name(), {})
     1411                #if nullable is not None:
     1412                #    new_nullable = nullable
     1413                #else:
     1414                #    new_nullable = f.null
     1415                if f.null is not None:
     1416                    new_nullable = f.null
     1417                else:
     1418                    new_nullable = None
     1419                self.fill_related_selections(model._meta, table, cur_depth + 1,
     1420                        used, next, restricted, new_nullable)
     1421
     1422        # Do reverse columns, but only the ones in the requested list.
     1423        if restricted:
     1424            related_fields = [(x.field, x.model) for x in opts.get_all_related_objects() if x.field.unique]
     1425            for f, model in related_fields:
     1426                if f.rel.parent_link or f.related_query_name() not in requested:
     1427                    continue
     1428                table = model._meta.db_table
     1429                alias = self.join((root_alias, table, f.rel.get_related_field().column,
     1430                        f.column), exclusions=used,
     1431                        promote=True, reuse=used)
     1432                used.add(alias)
     1433
     1434                self.related_select_cols.extend([(alias, f2.column)
     1435                        for f2 in model._meta.fields])
     1436                self.related_select_fields.extend(model._meta.fields)
     1437
     1438                next = requested.get(f.related_query_name(), {})
     1439                #if nullable is not None:
     1440                #    new_nullable = nullable
     1441                #else:
     1442                #    new_nullable = f.null
     1443                if f.null is not None:
     1444                    new_nullable = f.null
     1445                else:
     1446                    new_nullable = None
     1447                self.fill_related_selections(model._meta, table, cur_depth + 1,
     1448                        used, next, restricted, new_nullable)
     1449
     1450
     1451
    13941452    def add_aggregate(self, aggregate, model, alias, is_summary):
    13951453        """
    13961454        Adds a single aggregate expression to the Query
  • django/db/models/options.py

     
    4747        self.proxy_for_model = None
    4848        self.parents = SortedDict()
    4949        self.duplicate_targets = {}
     50        self.reverse_field_cache = {}
    5051
    5152        # To handle various inheritance situations, we need to track where
    5253        # managers came from (concrete or abstract base classes).
  • django/db/models/fields/related.py

     
     1import types
    12from django.db import connection, transaction
    23from django.db.backends import util
    34from django.db.models import signals, get_model
     
    178179    # SingleRelatedObjectDescriptor instance.
    179180    def __init__(self, related):
    180181        self.related = related
    181         self.cache_name = '_%s_cache' % related.get_accessor_name()
     182        #self.cache_name = '_%s_cache' % related.get_accessor_name()
     183        cache_name = '_%s_cache' % related.field.related_query_name()
     184        # Contribute to the parent model for later lookup.
     185        related.parent_model._meta.reverse_field_cache[related.field.related_query_name()] = cache_name
     186        self.cache_name = cache_name
    182187
    183188    def __get__(self, instance, instance_type=None):
    184189        if instance is None:
     
    291296    # attribute is a ForeignRelatedObjectsDescriptor instance.
    292297    def __init__(self, related):
    293298        self.related = related   # RelatedObject instance
     299        if related.field.unique:
     300            cache_name = '_%s_cache' % related.field.related_query_name()
     301            # Contribute to the parent model for later lookup.
     302            related.parent_model._meta.reverse_field_cache[related.field.related_query_name()] = cache_name
     303            self.cache_name = cache_name
    294304
    295305    def __get__(self, instance, instance_type=None):
    296306        if instance is None:
     
    309319        if self.related.field.null:
    310320            manager.clear()
    311321        manager.add(*value)
     322        # Cache the value specially if from a unique set.
     323        if self.related.field.unique:
     324            self.cache_name = value
    312325
    313326    def delete_manager(self, instance):
    314327        """
     
    325338        """
    326339        rel_field = self.related.field
    327340        rel_model = self.related.model
     341        if rel_field.unique:
     342            cache_name = self.cache_name
    328343
    329344        class RelatedManager(superclass):
    330345            def get_query_set(self):
     
    369384                        obj.save()
    370385                clear.alters_data = True
    371386
     387            if rel_field.unique:
     388                def all(self):
     389                    try:
     390                        result = getattr(instance, cache_name)
     391                        if isinstance(result, (types.TupleType, types.ListType)):
     392                            return result
     393                        else:
     394                            return [result]
     395                    except AttributeError, ae:
     396                        return superclass.get_query_set(self)
     397
    372398        manager = RelatedManager()
    373399        attname = rel_field.rel.get_related_field().name
    374400        manager.core_filters = {'%s__%s' % (rel_field.name, attname):
  • django/db/models/query.py

     
    937937            obj = klass(*fields)
    938938
    939939    index_end = index_start + field_count + offset
     940
    940941    for f in klass._meta.fields:
    941942        if not select_related_descend(f, restricted, requested):
    942943            continue
     
    950951            rel_obj, index_end = cached_row
    951952            if obj is not None:
    952953                setattr(obj, f.get_cache_name(), rel_obj)
     954
     955            # Do the reverse cache if the field is a unique relation.
     956            if f.unique:
     957                cache_var_name = rel_obj._meta.reverse_field_cache[f.related_query_name()]
     958                setattr(rel_obj, cache_var_name, obj)
     959
     960    if restricted:
     961        related_fields = [(x.field, x.model) for x in klass._meta.get_all_related_objects() if x.field.unique]
     962        for f, model in related_fields:
     963            if f.related_query_name() not in requested:
     964                continue
     965
     966            next = requested.get(f.related_query_name(), {})
     967            cached_row = get_cached_row(model, row, index_end, max_depth,
     968                    cur_depth+1, next)
     969            if cached_row:
     970                rel_obj, index_end = cached_row
     971                if rel_obj is not None:
     972                    setattr(rel_obj, f.get_cache_name(), obj)
     973
     974                # Now do the reverse cache.
     975                cache_var_name = obj._meta.reverse_field_cache[f.related_query_name()]
     976                setattr(obj, cache_var_name, rel_obj)
     977
    953978    return obj, index_end
    954979
    955980def delete_objects(seen_objs):
  • tests/modeltests/select_related_reverse/models.py

     
     1"""
     2``select_related()`` follows all forward relationships, but should also follow
     3reverse relationships where it is appropriate (currently for OneToOneFields
     4only).
     5"""
     6
     7from django.db import models
     8
     9# OneToOneField tests only.
     10
     11class User(models.Model):
     12    alias = models.CharField(max_length=20)
     13   
     14    def __unicode__(self):
     15        return 'User, alias = %s' % (self.alias,)
     16
     17
     18class UserInfo(models.Model):
     19    user = models.OneToOneField(User, primary_key=True)
     20    name = models.CharField(max_length=32)
     21
     22    def __unicode__(self):
     23        return 'UserInfo, name = %s' % (self.name,)
     24   
     25
     26class UserStatResults(models.Model):
     27    results = models.CharField(max_length=50)
     28
     29    def __unicode__(self):
     30        return 'UserStatResults, results = %s' % (self.results,)
     31   
     32
     33class UserStat(models.Model):
     34    user = models.OneToOneField(User, primary_key=True)
     35    posts = models.IntegerField()
     36    results = models.ForeignKey(UserStatResults)
     37
     38    def __unicode__(self):
     39        return 'UserStat, posts = %s' % (self.posts,)
     40
     41
     42class StatDetails(models.Model):
     43    base_stats = models.OneToOneField(UserStat, primary_key=True)
     44    comments = models.IntegerField()
     45
     46    def __unicode__(self):
     47        return 'StatDetails, comments = %s' % (self.comments,)
     48
     49
     50__test__ = {'API_TESTS':"""
     51
     52# Set up.
     53# The test runner sets settings.DEBUG to False, but we want to gather queries
     54# so we'll set it to True here and reset it at the end of the test suite.
     55>>> from django.conf import settings
     56>>> settings.DEBUG = True
     57>>> from django import db
     58
     59>>> usr = UserStatResults.objects.create(results='first results')
     60>>> u = User.objects.create(alias='tom')
     61>>> ui = UserInfo.objects.create(user=u, name='Tom Jones')
     62>>> stat = UserStat.objects.create(user=u, posts=150, results=usr)
     63>>> sd = StatDetails.objects.create(base_stats=stat, comments=259)
     64>>> u = User.objects.create(alias='john')
     65>>> ui = UserInfo.objects.create(user=u, name='John Smith')
     66>>> stat = UserStat.objects.create(user=u, posts=33, results=usr)
     67>>> sd = StatDetails = StatDetails.objects.create(base_stats=stat, comments=104)
     68
     69# Specifying models in the select_related(...) works as expected.
     70>>> db.reset_queries()
     71>>> u = User.objects.select_related('userinfo', 'userstat').get(alias='tom')
     72>>> u.userstat
     73<UserStat: UserStat, posts = 150>
     74>>> len(db.connection.queries)
     751
     76>>> u.userstat.statdetails
     77<StatDetails: StatDetails, comments = 259>
     78>>> len(db.connection.queries)
     792
     80>>> u.userinfo
     81<UserInfo: UserInfo, name = Tom Jones>
     82>>> len(db.connection.queries)
     832
     84>>> db.reset_queries()
     85>>> u = User.objects.select_related('userstat', 'userstat__statdetails', 'userstat__results').get(alias='tom')
     86>>> u.userstat
     87<UserStat: UserStat, posts = 150>
     88>>> len(db.connection.queries)
     891
     90>>> u.userstat.statdetails
     91<StatDetails: StatDetails, comments = 259>
     92>>> len(db.connection.queries)
     931
     94>>> u.userstat.results
     95<UserStatResults: UserStatResults, results = first results>
     96>>> len(db.connection.queries)
     971
     98>>> u.userinfo
     99<UserInfo: UserInfo, name = Tom Jones>
     100>>> len(db.connection.queries)
     1012
     102
     103>>> db.reset_queries()
     104>>> us = UserStat.objects.select_related('user__userinfo').get(user__alias='john')
     105>>> us
     106<UserStat: UserStat, posts = 33>
     107>>> len(db.connection.queries)
     1081
     109>>> us.user
     110<User: User, alias = john>
     111>>> len(db.connection.queries)
     1121
     113>>> us.user.userinfo
     114<UserInfo: UserInfo, name = John Smith>
     115>>> len(db.connection.queries)
     1161
     117>>> us.user.userstat
     118<UserStat: UserStat, posts = 33>
     119>>> len(db.connection.queries)
     1201
     121
     122"""}
     123
  • tests/modeltests/select_related_reverse2/models.py

     
     1"""
     2``select_related()`` follows all forward relationships, but should also follow
     3reverse relationships where it is appropriate (currently for OneToOneFields
     4only).
     5"""
     6
     7from django.db import models
     8
     9# OneToOneField tests only.
     10
     11class User(models.Model):
     12    alias = models.CharField(max_length=20)
     13   
     14    def __unicode__(self):
     15        return 'User, alias = %s' % (self.alias,)
     16
     17
     18class UserInfo(models.Model):
     19    user = models.OneToOneField(User, primary_key=True)
     20    name = models.CharField(max_length=32)
     21
     22    def __unicode__(self):
     23        return 'UserInfo, name = %s' % (self.name,)
     24   
     25
     26class UserStatResults(models.Model):
     27    results = models.CharField(max_length=50)
     28
     29    def __unicode__(self):
     30        return 'UserStatResults, results = %s' % (self.results,)
     31   
     32
     33class UserStat(models.Model):
     34    user = models.ForeignKey(User, primary_key=True, unique=True, related_name='userstat')
     35    posts = models.IntegerField()
     36    results = models.ForeignKey(UserStatResults)
     37
     38    def __unicode__(self):
     39        return 'UserStat, posts = %s' % (self.posts,)
     40
     41
     42class StatDetails(models.Model):
     43    base_stats = models.OneToOneField(UserStat, primary_key=True)
     44    comments = models.IntegerField()
     45
     46    def __unicode__(self):
     47        return 'StatDetails, comments = %s' % (self.comments,)
     48
     49
     50__test__ = {'API_TESTS':"""
     51
     52# Set up.
     53# The test runner sets settings.DEBUG to False, but we want to gather queries
     54# so we'll set it to True here and reset it at the end of the test suite.
     55>>> from django.conf import settings
     56>>> settings.DEBUG = True
     57>>> from django import db
     58
     59>>> usr = UserStatResults.objects.create(results='first results')
     60>>> u = User.objects.create(alias='tom')
     61>>> ui = UserInfo.objects.create(user=u, name='Tom Jones')
     62>>> stat = UserStat.objects.create(user=u, posts=150, results=usr)
     63>>> sd = StatDetails.objects.create(base_stats=stat, comments=259)
     64>>> u = User.objects.create(alias='john')
     65>>> ui = UserInfo.objects.create(user=u, name='John Smith')
     66>>> stat = UserStat.objects.create(user=u, posts=33, results=usr)
     67>>> sd = StatDetails = StatDetails.objects.create(base_stats=stat, comments=104)
     68
     69# Specifying models in the select_related(...) works as expected.
     70>>> db.reset_queries()
     71>>> u = User.objects.select_related('userinfo', 'userstat').get(alias='tom')
     72>>> u.userstat.all()
     73[<UserStat: UserStat, posts = 150>]
     74>>> len(db.connection.queries)
     751
     76>>> u.userstat.all()[0].statdetails
     77<StatDetails: StatDetails, comments = 259>
     78>>> len(db.connection.queries)
     792
     80>>> u.userinfo
     81<UserInfo: UserInfo, name = Tom Jones>
     82>>> len(db.connection.queries)
     832
     84>>> db.reset_queries()
     85>>> u = User.objects.select_related('userstat', 'userstat__statdetails', 'userstat__results').get(alias='tom')
     86>>> u.userstat.all()
     87[<UserStat: UserStat, posts = 150>]
     88>>> len(db.connection.queries)
     891
     90>>> u.userstat.all()[0].statdetails
     91<StatDetails: StatDetails, comments = 259>
     92>>> len(db.connection.queries)
     931
     94>>> u.userstat.all()[0].results
     95<UserStatResults: UserStatResults, results = first results>
     96>>> len(db.connection.queries)
     971
     98>>> u.userinfo
     99<UserInfo: UserInfo, name = Tom Jones>
     100>>> len(db.connection.queries)
     1012
     102
     103>>> db.reset_queries()
     104>>> us = UserStat.objects.select_related('user__userinfo').get(user__alias='john')
     105>>> us
     106<UserStat: UserStat, posts = 33>
     107>>> len(db.connection.queries)
     1081
     109>>> us.user
     110<User: User, alias = john>
     111>>> len(db.connection.queries)
     1121
     113>>> us.user.userinfo
     114<UserInfo: UserInfo, name = John Smith>
     115>>> len(db.connection.queries)
     1161
     117>>> us.user.userstat.all()
     118[<UserStat: UserStat, posts = 33>]
     119>>> len(db.connection.queries)
     1201
     121
     122"""}
     123
  • tests/modeltests/select_related_reverse3/models.py

     
     1"""
     2``select_related()`` follows all forward relationships, but should also follow
     3reverse relationships where it is appropriate (currently for OneToOneFields
     4only).
     5"""
     6
     7from django.db import models
     8
     9# OneToOneField tests only.
     10
     11class User(models.Model):
     12    alias = models.CharField(max_length=20)
     13   
     14    def __unicode__(self):
     15        return 'User, alias = %s' % (self.alias,)
     16
     17
     18class UserInfo(models.Model):
     19    user = models.OneToOneField(User, primary_key=True)
     20    name = models.CharField(max_length=32)
     21
     22    def __unicode__(self):
     23        return 'UserInfo, name = %s' % (self.name,)
     24   
     25
     26class UserStatResults(models.Model):
     27    results = models.CharField(max_length=50)
     28
     29    def __unicode__(self):
     30        return 'UserStatResults, results = %s' % (self.results,)
     31   
     32
     33class UserStat(models.Model):
     34    user = models.OneToOneField(User, primary_key=True)
     35    posts = models.IntegerField()
     36    results = models.ForeignKey(UserStatResults)
     37    watcher = models.OneToOneField(User, related_name='stat_watcher')
     38
     39    def __unicode__(self):
     40        return 'UserStat, posts = %s' % (self.posts,)
     41
     42
     43class StatDetails(models.Model):
     44    base_stats = models.OneToOneField(UserStat, primary_key=True)
     45    comments = models.IntegerField()
     46
     47    def __unicode__(self):
     48        return 'StatDetails, comments = %s' % (self.comments,)
     49
     50
     51__test__ = {'API_TESTS':"""
     52
     53# Set up.
     54# The test runner sets settings.DEBUG to False, but we want to gather queries
     55# so we'll set it to True here and reset it at the end of the test suite.
     56>>> from django.conf import settings
     57>>> settings.DEBUG = True
     58>>> from django import db
     59
     60>>> usr = UserStatResults.objects.create(results='first results')
     61>>> u1 = User.objects.create(alias='tom')
     62>>> ui = UserInfo.objects.create(user=u1, name='Tom Jones')
     63>>> u2 = User.objects.create(alias='john')
     64>>> ui = UserInfo.objects.create(user=u2, name='John Smith')
     65>>> u3 = User.objects.create(alias='bob')
     66>>> ui = UserInfo.objects.create(user=u3, name='Bob Rogers')
     67>>> stat = UserStat.objects.create(user=u1, posts=150, results=usr, watcher=u3)
     68>>> sd = StatDetails.objects.create(base_stats=stat, comments=259)
     69>>> stat = UserStat.objects.create(user=u2, posts=33, results=usr, watcher=u2)
     70>>> sd = StatDetails = StatDetails.objects.create(base_stats=stat, comments=104)
     71
     72# Specifying models in the select_related(...) works as expected.
     73>>> db.reset_queries()
     74>>> u = User.objects.select_related('userinfo', 'userstat').get(alias='tom')
     75>>> u.userstat
     76<UserStat: UserStat, posts = 150>
     77>>> len(db.connection.queries)
     781
     79>>> u.userstat.statdetails
     80<StatDetails: StatDetails, comments = 259>
     81>>> len(db.connection.queries)
     822
     83>>> u.userinfo
     84<UserInfo: UserInfo, name = Tom Jones>
     85>>> len(db.connection.queries)
     862
     87>>> db.reset_queries()
     88>>> u = User.objects.select_related('userstat', 'userstat__statdetails', 'userstat__results').get(alias='tom')
     89>>> u.userstat
     90<UserStat: UserStat, posts = 150>
     91>>> len(db.connection.queries)
     921
     93>>> u.userstat.statdetails
     94<StatDetails: StatDetails, comments = 259>
     95>>> len(db.connection.queries)
     961
     97>>> u.userstat.results
     98<UserStatResults: UserStatResults, results = first results>
     99>>> len(db.connection.queries)
     1001
     101>>> u.userinfo
     102<UserInfo: UserInfo, name = Tom Jones>
     103>>> len(db.connection.queries)
     1042
     105
     106>>> db.reset_queries()
     107>>> us = UserStat.objects.select_related('user__userinfo').get(user__alias='john')
     108>>> us
     109<UserStat: UserStat, posts = 33>
     110>>> len(db.connection.queries)
     1111
     112>>> us.user
     113<User: User, alias = john>
     114>>> len(db.connection.queries)
     1151
     116>>> us.user.userinfo
     117<UserInfo: UserInfo, name = John Smith>
     118>>> len(db.connection.queries)
     1191
     120>>> us.user.userstat
     121<UserStat: UserStat, posts = 33>
     122>>> len(db.connection.queries)
     1231
     124
     125# !!!!!!!!!!!!!!
     126# Everything past this point is testing multiple fields pointing to the same model.
     127# !!!!!!!!!!!!!!
     128>>> us.user.userstat.watcher
     129<User: User, alias = john>
     130>>> len(db.connection.queries)
     1312
     132
     133>>> db.reset_queries()
     134>>> us = UserStat.objects.select_related('user__userinfo', 'watcher').get(user__alias='john')
     135>>> us
     136<UserStat: UserStat, posts = 33>
     137>>> len(db.connection.queries)
     1381
     139>>> us.user
     140<User: User, alias = john>
     141>>> len(db.connection.queries)
     1421
     143>>> us.user.userinfo
     144<UserInfo: UserInfo, name = John Smith>
     145>>> len(db.connection.queries)
     1461
     147>>> us.user.userstat
     148<UserStat: UserStat, posts = 33>
     149>>> len(db.connection.queries)
     1501
     151
     152>>> us.user.userstat.watcher
     153<User: User, alias = john>
     154>>> len(db.connection.queries)
     1551
     156
     157# Show that the # of queries increases as expected when used reverse.
     158>>> db.reset_queries()
     159>>> u = User.objects.select_related('userinfo', 'userstat').get(alias='tom')
     160>>> u.userstat
     161<UserStat: UserStat, posts = 150>
     162>>> len(db.connection.queries)
     1631
     164>>> u.userstat.statdetails
     165<StatDetails: StatDetails, comments = 259>
     166>>> len(db.connection.queries)
     1672
     168>>> u.userstat.watcher
     169<User: User, alias = bob>
     170>>> len(db.connection.queries)
     1713
     172>>> u.userinfo
     173<UserInfo: UserInfo, name = Tom Jones>
     174>>> len(db.connection.queries)
     1753
     176
     177# Show that we can follow the watcher too.
     178>>> db.reset_queries()
     179>>> u = User.objects.select_related('userinfo', 'userstat').get(alias='john')
     180>>> u.userstat
     181<UserStat: UserStat, posts = 33>
     182>>> len(db.connection.queries)
     1831
     184>>> u.userstat.watcher
     185<User: User, alias = john>
     186>>> len(db.connection.queries)
     1872
     188>>> u.stat_watcher
     189<UserStat: UserStat, posts = 33>
     190>>> len(db.connection.queries)
     1913
     192
     193# Show that we can follow the watcher via the select_related(...) bits.
     194>>> db.reset_queries()
     195>>> u = User.objects.select_related('userstat', 'stat_watcher').get(alias='john')
     196>>> u.userstat
     197<UserStat: UserStat, posts = 33>
     198>>> len(db.connection.queries)
     1991
     200>>> u.userstat.watcher
     201<User: User, alias = john>
     202>>> len(db.connection.queries)
     2032
     204>>> u.stat_watcher
     205<UserStat: UserStat, posts = 33>
     206>>> len(db.connection.queries)
     2072
     208
     209# Show that we can follow the watcher via the select_related(...) bits that are nested.
     210>>> db.reset_queries()
     211>>> u = User.objects.select_related('userstat', 'userstat__watcher').get(alias='tom')
     212>>> u.userstat
     213<UserStat: UserStat, posts = 150>
     214>>> len(db.connection.queries)
     2151
     216>>> u.userstat.watcher
     217<User: User, alias = bob>
     218>>> len(db.connection.queries)
     2191
     220
     221"""}
     222
Back to Top