Skip to content
Permalink
Browse files

Fixed #19385 -- Introduced multicolumn joins to the ORM

This patch consist of two main parts. The first part deals with adding
RelatedField and ForeignObject fields and basing current relation
fields on those. The second part deals with altering the ORM to be able
to handle multicolumn joins.

There is still a lot of work to be done for full multicolumn ForeignKey
support. The ForeignObject based fields do not integrate to other parts
of Django (admin, ModelForms, ...). There is no support for multicolumn
primary keys or any public way to use multicolumn ForeignKeys. Even
with these limitations the ForeignObject based fields allows
implementing some interesting constructs, for examples see the added
tests in tests/foreign_object/tests.py).

The downside of using ForeignObject is that it is private API and should
remain so until full virtual field support is implemented. Changes are
expected.

The patch was written by Jeremy Tillman with some final polish by
committer. A very big thank you for the work done!
  • Loading branch information...
Jeremy Tillman authored and akaariai committed Jan 2, 2013
1 parent 2279066 commit 4810cefd0a99f6963ad57415dd8771e8b2b4a279
@@ -10,7 +10,7 @@
from django.db import connection
from django.db.models import signals
from django.db import models, router, DEFAULT_DB_ALIAS
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
from django.db.models.fields.related import ForeignObject, ManyToManyRel
from django.db.models.related import PathInfo
from django.forms import ModelForm
from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
@@ -149,7 +149,7 @@ def __set__(self, instance, value):
setattr(instance, self.fk_field, fk)
setattr(instance, self.cache_attr, value)

class GenericRelation(RelatedField, Field):
class GenericRelation(ForeignObject):
"""Provides an accessor to generic related objects (e.g. comments)"""

def __init__(self, to, **kwargs):
@@ -167,25 +167,35 @@ def __init__(self, to, **kwargs):
kwargs['blank'] = True
kwargs['editable'] = False
kwargs['serialize'] = False
Field.__init__(self, **kwargs)
super(GenericRelation, self).__init__(
to,
to_fields=[],
from_fields=[self.object_id_field_name],
**kwargs)

def resolve_relation_fields(self):
self.to_fields = [self.model._meta.pk.name]
return [(self.rel.to._meta.get_field_by_name(self.object_id_field_name)[0],
self.model._meta.pk)]

def get_path_info(self):
from_field = self.model._meta.pk
opts = self.rel.to._meta
target = opts.get_field_by_name(self.object_id_field_name)[0]
# Note that we are using different field for the join_field
# than from_field or to_field. This is a hack, but we need the
# GenericRelation to generate the extra SQL.
return ([PathInfo(from_field, target, self.model._meta, opts, self, True, False)],
opts, target, self)
# Still need the path from here to parent.
return [PathInfo(self.model._meta, opts, (target,), self, True, False)]

def get_choices_default(self):
return Field.get_choices(self, include_blank=False)
return super(GenericRelation, self).get_choices(include_blank=False)

def value_to_string(self, obj):
qs = getattr(obj, self.name).all()
return smart_text([instance._get_pk_val() for instance in qs])

def get_joining_columns(self, reverse_join=False):
if not reverse_join:
raise ValueError('GenericRelation only supports reverse joins.')
return super(GenericRelation, self).get_joining_columns(reverse_join)

def m2m_db_table(self):
return self.rel.to._meta.db_table

@@ -153,8 +153,16 @@ def get_validation_errors(outfile, app=None):
continue

# Make sure the related field specified by a ForeignKey is unique
if not f.rel.to._meta.get_field(f.rel.field_name).unique:
e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name, f.rel.to.__name__))
if f.requires_unique_target:
if len(f.foreign_related_fields) > 1:
has_unique_field = False
for rel_field in f.foreign_related_fields:
has_unique_field = has_unique_field or rel_field.unique
if not has_unique_field:
e.add(opts, "Field combination '%s' under model '%s' must have a unique=True constraint" % (','.join([rel_field.name for rel_field in f.foreign_related_fields]), f.rel.to.__name__))
else:
if not f.foreign_related_fields[0].unique:
e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.foreign_related_fields[0].name, f.rel.to.__name__))

rel_opts = f.rel.to._meta
rel_name = f.related.get_accessor_name()
@@ -17,6 +17,12 @@ def resolve_columns(self, row, fields=()):
values.append(value)
return row[:index_extra_select] + tuple(values)

def as_subquery_condition(self, alias, columns):
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
sql, params = self.as_sql()
return '(%s) IN (%s)' % (', '.join(['%s.%s' % (qn(alias), qn2(column)) for column in columns]), sql), params

class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler):
pass

@@ -8,7 +8,7 @@
from django.db.models.fields import *
from django.db.models.fields.subclassing import SubfieldBase
from django.db.models.fields.files import FileField, ImageField
from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
from django.db.models.fields.related import ForeignKey, ForeignObject, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
from django.db.models.deletion import CASCADE, PROTECT, SET, SET_NULL, SET_DEFAULT, DO_NOTHING, ProtectedError
from django.db.models import signals
from django.utils.decorators import wraps
@@ -10,7 +10,7 @@
from django.core.exceptions import (ObjectDoesNotExist,
MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS)
from django.db.models.fields import AutoField, FieldDoesNotExist
from django.db.models.fields.related import (ManyToOneRel,
from django.db.models.fields.related import (ForeignObjectRel, ManyToOneRel,
OneToOneField, add_lazy_relation)
from django.db import (router, transaction, DatabaseError,
DEFAULT_DB_ALIAS)
@@ -333,12 +333,12 @@ def __init__(self, *args, **kwargs):
# The reason for the kwargs check is that standard iterator passes in by
# args, and instantiation for iteration is 33% faster.
args_len = len(args)
if args_len > len(self._meta.fields):
if args_len > len(self._meta.concrete_fields):
# Daft, but matches old exception sans the err msg.
raise IndexError("Number of args exceeds number of fields")

fields_iter = iter(self._meta.fields)
if not kwargs:
fields_iter = iter(self._meta.concrete_fields)
# The ordering of the zip calls matter - zip throws StopIteration
# when an iter throws it. So if the first iter throws it, the second
# is *not* consumed. We rely on this, so don't change the order
@@ -347,6 +347,7 @@ def __init__(self, *args, **kwargs):
setattr(self, field.attname, val)
else:
# Slower, kwargs-ready version.
fields_iter = iter(self._meta.fields)
for val, field in zip(args, fields_iter):
setattr(self, field.attname, val)
kwargs.pop(field.name, None)
@@ -363,11 +364,12 @@ def __init__(self, *args, **kwargs):
# data-descriptor object (DeferredAttribute) without triggering its
# __get__ method.
if (field.attname not in kwargs and
isinstance(self.__class__.__dict__.get(field.attname), DeferredAttribute)):
(isinstance(self.__class__.__dict__.get(field.attname), DeferredAttribute)
or field.column is None)):
# This field will be populated on request.
continue
if kwargs:
if isinstance(field.rel, ManyToOneRel):
if isinstance(field.rel, ForeignObjectRel):
try:
# Assume object instance was passed in.
rel_obj = kwargs.pop(field.name)
@@ -394,6 +396,7 @@ def __init__(self, *args, **kwargs):
val = field.get_default()
else:
val = field.get_default()

if is_related_object:
# If we are passed a related instance, set it using the
# field.name instead of field.attname (e.g. "user" instead of
@@ -528,7 +531,7 @@ def save(self, force_insert=False, force_update=False, using=None,
# automatically do a "update_fields" save on the loaded fields.
elif not force_insert and self._deferred and using == self._state.db:
field_names = set()
for field in self._meta.fields:
for field in self._meta.concrete_fields:
if not field.primary_key and not hasattr(field, 'through'):
field_names.add(field.attname)
deferred_fields = [
@@ -614,7 +617,7 @@ def _save_table(self, raw=False, cls=None, force_insert=False,
for a single table.
"""
meta = cls._meta
non_pks = [f for f in meta.local_fields if not f.primary_key]
non_pks = [f for f in meta.local_concrete_fields if not f.primary_key]

if update_fields:
non_pks = [f for f in non_pks
@@ -652,7 +655,7 @@ def _save_table(self, raw=False, cls=None, force_insert=False,
**{field.name: getattr(self, field.attname)}).count()
self._order = order_value

fields = meta.local_fields
fields = meta.local_concrete_fields
if not pk_set:
fields = [f for f in fields if not isinstance(f, AutoField)]

Oops, something went wrong.

0 comments on commit 4810cef

Please sign in to comment.
You can’t perform that action at this time.