Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

magic-removal: Fixed #1136: Removed duplicated DB joins resulting fro…

…m kwarg evaluation. Thanks, Russ Magee

git-svn-id: http://code.djangoproject.com/svn/django/branches/magic-removal@1792 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit d382abc8757c4629c314e1ed8854fd06da68e821 1 parent dc5eb65
Adrian Holovaty authored December 29, 2005
37  django/db/models/manager.py
@@ -33,7 +33,7 @@ def __init__(self):
33 33
         self.creation_counter = Manager.creation_counter
34 34
         Manager.creation_counter += 1
35 35
         self.klass = None
36  
-    
  36
+
37 37
     def _prepare(self):
38 38
         if self.klass._meta.get_latest_by:
39 39
             self.get_latest = self.__get_latest
@@ -49,7 +49,7 @@ def contribute_to_class(self, klass, name):
49 49
         if not hasattr(klass, '_default_manager') or \
50 50
            self.creation_counter < klass._default_manager.creation_counter:
51 51
                 klass._default_manager = self
52  
-        
  52
+
53 53
     def _get_sql_clause(self, **kwargs):
54 54
         def quote_only_if_word(word):
55 55
             if ' ' in word:
@@ -61,15 +61,14 @@ def quote_only_if_word(word):
61 61
 
62 62
         # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z.
63 63
         select = ["%s.%s" % (backend.quote_name(opts.db_table), backend.quote_name(f.column)) for f in opts.fields]
64  
-        tables = [opts.db_table] + (kwargs.get('tables') and kwargs['tables'][:] or [])
65  
-        tables = [quote_only_if_word(t) for t in tables]
  64
+        tables = (kwargs.get('tables') and [quote_only_if_word(t) for t in kwargs['tables']] or [])
66 65
         where = kwargs.get('where') and kwargs['where'][:] or []
67 66
         params = kwargs.get('params') and kwargs['params'][:] or []
68 67
 
69 68
         # Convert the kwargs into SQL.
70  
-        tables2, join_where2, where2, params2, _ = parse_lookup(kwargs.items(), opts)
  69
+        tables2, joins, where2, params2 = parse_lookup(kwargs.items(), opts)
71 70
         tables.extend(tables2)
72  
-        where.extend(join_where2 + where2)
  71
+        where.extend(where2)
73 72
         params.extend(params2)
74 73
 
75 74
         # Add any additional constraints from the "where_constraints" parameter.
@@ -83,6 +82,22 @@ def quote_only_if_word(word):
83 82
         if kwargs.get('select'):
84 83
             select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), backend.quote_name(s[0])) for s in kwargs['select']])
85 84
 
  85
+        # Start composing the body of the SQL statement.
  86
+        sql = [" FROM", backend.quote_name(opts.db_table)]
  87
+
  88
+        # Compose the join dictionary into SQL describing the joins.
  89
+        if joins:
  90
+            sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition)
  91
+                                for (alias, (table, join_type, condition)) in joins.items()]))
  92
+
  93
+        # Compose the tables clause into SQL.
  94
+        if tables:
  95
+            sql.append(", " + ", ".join(tables))
  96
+
  97
+        # Compose the where clause into SQL.
  98
+        if where:
  99
+            sql.append(where and "WHERE " + " AND ".join(where))
  100
+
86 101
         # ORDER BY clause
87 102
         order_by = []
88 103
         for f in handle_legacy_orderlist(kwargs.get('order_by', opts.ordering)):
@@ -106,16 +121,16 @@ def quote_only_if_word(word):
106 121
                     else:
107 122
                         table_prefix = ''
108 123
                 order_by.append('%s%s %s' % (table_prefix, backend.quote_name(orderfield2column(col_name, opts)), order))
109  
-        order_by = ", ".join(order_by)
  124
+        if order_by:
  125
+            sql.append("ORDER BY " + ", ".join(order_by))
110 126
 
111 127
         # LIMIT and OFFSET clauses
112 128
         if kwargs.get('limit') is not None:
113  
-            limit_sql = " %s " % backend.get_limit_offset_sql(kwargs['limit'], kwargs.get('offset'))
  129
+            sql.append("%s " % backend.get_limit_offset_sql(kwargs['limit'], kwargs.get('offset')))
114 130
         else:
115 131
             assert kwargs.get('offset') is None, "'offset' is not allowed without 'limit'"
116  
-            limit_sql = ""
117 132
 
118  
-        return select, " FROM " + ",".join(tables) + (where and " WHERE " + " AND ".join(where) or "") + (order_by and " ORDER BY " + order_by or "") + limit_sql, params
  133
+        return select, " ".join(sql), params
119 134
 
120 135
     def get_iterator(self, **kwargs):
121 136
         # kwargs['select'] is a dictionary, and dictionaries' key order is
@@ -220,7 +235,7 @@ def __get_date_list(self, field, *args, **kwargs):
220 235
         # We have to manually run typecast_timestamp(str()) on the results, because
221 236
         # MySQL doesn't automatically cast the result of date functions as datetime
222 237
         # objects -- MySQL returns the values as strings, instead.
223  
-        return [typecast_timestamp(str(row[0])) for row in cursor.fetchall()] 
  238
+        return [typecast_timestamp(str(row[0])) for row in cursor.fetchall()]
224 239
 
225 240
 class ManagerDescriptor(object):
226 241
     def __init__(self, manager):
113  django/db/models/query.py
@@ -45,7 +45,7 @@ def orderlist2sql(order_list, opts, prefix=''):
45 45
             output.append('%s%s ASC' % (prefix, backend.quote_name(orderfield2column(f, opts))))
46 46
     return ', '.join(output)
47 47
 
48  
-class QBase:
  48
+class QOperator:
49 49
     "Base class for QAnd and QOr"
50 50
     def __init__(self, *args):
51 51
         self.args = args
@@ -53,17 +53,17 @@ def __init__(self, *args):
53 53
     def __repr__(self):
54 54
         return '(%s)' % self.operator.join([repr(el) for el in self.args])
55 55
 
56  
-    def get_sql(self, opts, table_count):
57  
-        tables, join_where, where, params = [], [], [], []
  56
+    def get_sql(self, opts):
  57
+        tables, joins, where, params = [], {}, [], []
58 58
         for val in self.args:
59  
-            tables2, join_where2, where2, params2, table_count = val.get_sql(opts, table_count)
  59
+            tables2, joins2, where2, params2 = val.get_sql(opts)
60 60
             tables.extend(tables2)
61  
-            join_where.extend(join_where2)
  61
+            joins.update(joins2)
62 62
             where.extend(where2)
63 63
             params.extend(params2)
64  
-        return tables, join_where, ['(%s)' % self.operator.join(where)], params, table_count
  64
+        return tables, joins, ['(%s)' % self.operator.join(where)], params
65 65
 
66  
-class QAnd(QBase):
  66
+class QAnd(QOperator):
67 67
     "Encapsulates a combined query that uses 'AND'."
68 68
     operator = ' AND '
69 69
     def __or__(self, other):
@@ -80,7 +80,7 @@ def __and__(self, other):
80 80
         else:
81 81
             raise TypeError, other
82 82
 
83  
-class QOr(QBase):
  83
+class QOr(QOperator):
84 84
     "Encapsulates a combined query that uses 'OR'."
85 85
     operator = ' OR '
86 86
     def __and__(self, other):
@@ -117,8 +117,8 @@ def __or__(self, other):
117 117
         else:
118 118
             raise TypeError, other
119 119
 
120  
-    def get_sql(self, opts, table_count):
121  
-        return parse_lookup(self.kwargs.items(), opts, table_count)
  120
+    def get_sql(self, opts):
  121
+        return parse_lookup(self.kwargs.items(), opts)
122 122
 
123 123
 
124 124
 def get_where_clause(lookup_type, table_prefix, field_name, value):
@@ -174,33 +174,40 @@ def throw_bad_kwarg_error(kwarg):
174 174
     # Helper function to remove redundancy.
175 175
     raise TypeError, "got unexpected keyword argument '%s'" % kwarg
176 176
 
177  
-def parse_lookup(kwarg_items, opts, table_count=0):
  177
+def parse_lookup(kwarg_items, opts):
178 178
     # Helper function that handles converting API kwargs (e.g.
179 179
     # "name__exact": "tom") to SQL.
180 180
 
181  
-    # Note that there is a distinction between where and join_where. The latter
182  
-    # is specifically a list of where clauses to use for JOINs. This
183  
-    # distinction is necessary because of support for "_or".
184  
-
185  
-    # table_count is used to ensure table aliases are unique.
186  
-    tables, join_where, where, params = [], [], [], []
  181
+    # 'joins' is a dictionary describing the tables that must be joined to complete the query.
  182
+    # Each key-value pair follows the form
  183
+    #   alias: (table, join_type, condition)
  184
+    # where
  185
+    #   alias is the AS alias for the joined table
  186
+    #   table is the actual table name to be joined
  187
+    #   join_type is the type of join (INNER JOIN, LEFT OUTER JOIN, etc)
  188
+    #   condition is the where-like statement over which narrows the join.
  189
+    #
  190
+    # alias will be derived from the lookup list name.
  191
+    # At present, this method only every returns INNER JOINs; the option is there for others
  192
+    # to implement custom Q()s, etc that return other join types.
  193
+    tables, joins, where, params = [], {}, [], []
187 194
     for kwarg, kwarg_value in kwarg_items:
188 195
         if kwarg in ('order_by', 'limit', 'offset', 'select_related', 'distinct', 'select', 'tables', 'where', 'params'):
189 196
             continue
190 197
         if kwarg_value is None:
191 198
             continue
192 199
         if kwarg == 'complex':
193  
-            tables2, join_where2, where2, params2, table_count = kwarg_value.get_sql(opts, table_count)
  200
+            tables2, joins2, where2, params2 = kwarg_value.get_sql(opts)
194 201
             tables.extend(tables2)
195  
-            join_where.extend(join_where2)
  202
+            joins.update(joins2)
196 203
             where.extend(where2)
197 204
             params.extend(params2)
198 205
             continue
199 206
         if kwarg == '_or':
200 207
             for val in kwarg_value:
201  
-                tables2, join_where2, where2, params2, table_count = parse_lookup(val, opts, table_count)
  208
+                tables2, joins2, where2, params2 = parse_lookup(val, opts)
202 209
                 tables.extend(tables2)
203  
-                join_where.extend(join_where2)
  210
+                joins.update(joins2)
204 211
                 where.append('(%s)' % ' OR '.join(where2))
205 212
                 params.extend(params2)
206 213
             continue
@@ -212,17 +219,16 @@ def parse_lookup(kwarg_items, opts, table_count=0):
212 219
             else:
213 220
                 lookup_list = lookup_list[:-1] + [opts.pk.name, 'exact']
214 221
         if len(lookup_list) == 1:
215  
-            _throw_bad_kwarg_error(kwarg)
  222
+            throw_bad_kwarg_error(kwarg)
216 223
         lookup_type = lookup_list.pop()
217 224
         current_opts = opts # We'll be overwriting this, so keep a reference to the original opts.
218 225
         current_table_alias = current_opts.db_table
219 226
         param_required = False
220 227
         while lookup_list or param_required:
221  
-            table_count += 1
222 228
             try:
223 229
                 # "current" is a piece of the lookup list. For example, in
224 230
                 # choices.get_list(poll__sites__id__exact=5), lookup_list is
225  
-                # ["polls", "sites", "id"], and the first current is "polls".
  231
+                # ["poll", "sites", "id"], and the first current is "poll".
226 232
                 try:
227 233
                     current = lookup_list.pop(0)
228 234
                 except IndexError:
@@ -233,15 +239,18 @@ def parse_lookup(kwarg_items, opts, table_count=0):
233 239
                 # Try many-to-many relationships first...
234 240
                 for f in current_opts.many_to_many:
235 241
                     if f.name == current:
236  
-                        rel_table_alias = backend.quote_name('t%s' % table_count)
237  
-                        table_count += 1
238  
-                        tables.append('%s %s' % \
239  
-                            (backend.quote_name(f.get_m2m_db_table(current_opts)), rel_table_alias))
240  
-                        join_where.append('%s.%s = %s.%s' % \
241  
-                            (backend.quote_name(current_table_alias),
242  
-                            backend.quote_name(current_opts.pk.column),
243  
-                            rel_table_alias,
244  
-                            backend.quote_name(current_opts.object_name.lower() + '_id')))
  242
+                        rel_table_alias = backend.quote_name(current_table_alias + LOOKUP_SEPARATOR + current)
  243
+
  244
+                        joins[rel_table_alias] = (
  245
+                            backend.quote_name(f.get_m2m_db_table(current_opts)),
  246
+                            "INNER JOIN",
  247
+                            '%s.%s = %s.%s' %
  248
+                                (backend.quote_name(current_table_alias),
  249
+                                backend.quote_name(current_opts.pk.column),
  250
+                                rel_table_alias,
  251
+                                backend.quote_name(current_opts.object_name.lower() + '_id'))
  252
+                        )
  253
+
245 254
                         # Optimization: In the case of primary-key lookups, we
246 255
                         # don't have to do an extra join.
247 256
                         if lookup_list and lookup_list[0] == f.rel.to._meta.pk.name and lookup_type == 'exact':
@@ -251,14 +260,17 @@ def parse_lookup(kwarg_items, opts, table_count=0):
251 260
                             lookup_list.pop()
252 261
                             param_required = False
253 262
                         else:
254  
-                            new_table_alias = 't%s' % table_count
255  
-                            tables.append('%s %s' % (backend.quote_name(f.rel.to._meta.db_table),
256  
-                                backend.quote_name(new_table_alias)))
257  
-                            join_where.append('%s.%s = %s.%s' % \
258  
-                                (backend.quote_name(rel_table_alias),
259  
-                                backend.quote_name(f.rel.to._meta.object_name.lower() + '_id'),
260  
-                                backend.quote_name(new_table_alias),
261  
-                                backend.quote_name(f.rel.to._meta.pk.column)))
  263
+                            new_table_alias = current_table_alias + LOOKUP_SEPARATOR + current
  264
+
  265
+                            joins[backend.quote_name(new_table_alias)] = (
  266
+                                backend.quote_name(f.rel.to._meta.db_table),
  267
+                                "INNER JOIN",
  268
+                                '%s.%s = %s.%s' %
  269
+                                    (rel_table_alias,
  270
+                                    backend.quote_name(f.rel.to._meta.object_name.lower() + '_id'),
  271
+                                    backend.quote_name(new_table_alias),
  272
+                                    backend.quote_name(f.rel.to._meta.pk.column))
  273
+                            )
262 274
                             current_table_alias = new_table_alias
263 275
                             param_required = True
264 276
                         current_opts = f.rel.to._meta
@@ -280,12 +292,17 @@ def parse_lookup(kwarg_items, opts, table_count=0):
280 292
                             where.append(get_where_clause(lookup_type, current_table_alias+'.', f.column, kwarg_value))
281 293
                             params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value))
282 294
                         else:
283  
-                            new_table_alias = 't%s' % table_count
284  
-                            tables.append('%s %s' % \
285  
-                                (backend.quote_name(f.rel.to._meta.db_table), backend.quote_name(new_table_alias)))
286  
-                            join_where.append('%s.%s = %s.%s' % \
287  
-                                (backend.quote_name(current_table_alias), backend.quote_name(f.column),
288  
-                                backend.quote_name(new_table_alias), backend.quote_name(f.rel.to._meta.pk.column)))
  295
+                            new_table_alias = current_table_alias + LOOKUP_SEPARATOR + current
  296
+
  297
+                            joins[backend.quote_name(new_table_alias)] = (
  298
+                                backend.quote_name(f.rel.to._meta.db_table),
  299
+                                "INNER JOIN",
  300
+                                '%s.%s = %s.%s' %
  301
+                                    (backend.quote_name(current_table_alias),
  302
+                                    backend.quote_name(f.column),
  303
+                                    backend.quote_name(new_table_alias),
  304
+                                    backend.quote_name(f.rel.to._meta.pk.column))
  305
+                            )
289 306
                             current_table_alias = new_table_alias
290 307
                             param_required = True
291 308
                         current_opts = f.rel.to._meta
@@ -301,4 +318,4 @@ def parse_lookup(kwarg_items, opts, table_count=0):
301 318
                 throw_bad_kwarg_error(kwarg)
302 319
             except StopIteration:
303 320
                 continue
304  
-    return tables, join_where, where, params, table_count
  321
+    return tables, joins, where, params
14  tests/modeltests/many_to_one/models.py
@@ -67,6 +67,19 @@ def __repr__(self):
67 67
 >>> Article.objects.get_list(reporter__first_name__exact='John', order_by=['pub_date'])
68 68
 [This is a test, John's second story]
69 69
 
  70
+# Query twice over the related field.
  71
+>>> Article.objects.get_list(reporter__first_name__exact='John', reporter__last_name__exact='Smith')
  72
+[This is a test, John's second story]
  73
+
  74
+# The underlying query only makes one join when a related table is referenced twice.
  75
+>>> null, sql, null = Article.objects._get_sql_clause(reporter__first_name__exact='John', reporter__last_name__exact='Smith')
  76
+>>> sql.count('INNER JOIN')
  77
+1
  78
+
  79
+# The automatically joined table has a predictable name.
  80
+>>> Article.objects.get_list(reporter__first_name__exact='John', where=["many_to_one_articles__reporter.last_name='Smith'"])
  81
+[This is a test, John's second story]
  82
+
70 83
 # Find all Articles for the Reporter whose ID is 1.
71 84
 >>> Article.objects.get_list(reporter__id__exact=1, order_by=['pub_date'])
72 85
 [This is a test, John's second story]
@@ -95,4 +108,5 @@ def __repr__(self):
95 108
 >>> a4.save()
96 109
 >>> a4.get_reporter()
97 110
 John Smith
  111
+
98 112
 """

0 notes on commit d382abc

Please sign in to comment.
Something went wrong with that request. Please try again.