diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2018-10-23 19:38:46 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2018-10-26 16:34:33 -0400 |
| commit | 90574aabef36fd59841b7df7d8ac30e2030e9854 (patch) | |
| tree | ae94a1693474a4bbbdde26b9262fac14e8ebfbf1 /lib/sqlalchemy | |
| parent | ac358a04a7b077602ac668c19c3c40389d9e77e4 (diff) | |
| download | sqlalchemy-90574aabef36fd59841b7df7d8ac30e2030e9854.tar.gz | |
Create object- and column-oriented versions of AssociationProxyInstance
The :class:`.AssociationProxy` now has standard column comparison operations
such as :meth:`.ColumnOperators.like` and
:meth:`.ColumnOperators.startswith` available when the target attribute is a
plain column - the EXISTS expression that joins to the target table is
rendered as usual, but the column expression is then use within the WHERE
criteria of the EXISTS. Note that this alters the behavior of the
``.contains()`` method on the association proxy to make use of
:meth:`.ColumnOperators.contains` when used on a column-based attribute.
Fixes: #4351
Change-Id: I310941f4e8f778c200f8144a26a89e5364cd4dfb
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/ext/associationproxy.py | 100 |
1 files changed, 81 insertions, 19 deletions
diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py index 1c28b10a1..629b4ac64 100644 --- a/lib/sqlalchemy/ext/associationproxy.py +++ b/lib/sqlalchemy/ext/associationproxy.py @@ -17,6 +17,7 @@ import operator from .. import exc, orm, util from ..orm import collections, interfaces from ..sql import or_ +from ..sql.operators import ColumnOperators from .. import inspect @@ -217,7 +218,7 @@ class AssociationProxy(interfaces.InspectionAttrInfo): except KeyError: owner = self._calc_owner(class_) if owner is not None: - result = AssociationProxyInstance(self, owner) + result = AssociationProxyInstance.for_proxy(self, owner) setattr(class_, self.key + "_inst", result) return result else: @@ -283,13 +284,49 @@ class AssociationProxyInstance(object): """ - def __init__(self, parent, owning_class): + def __init__(self, parent, owning_class, target_class, value_attr): self.parent = parent self.key = parent.key self.owning_class = owning_class self.target_collection = parent.target_collection self.value_attr = parent.value_attr self.collection_class = None + self.target_class = target_class + self.value_attr = value_attr + + target_class = None + """The intermediary class handled by this + :class:`.AssociationProxyInstance`. + + Intercepted append/set/assignment events will result + in the generation of new instances of this class. + + """ + + @classmethod + def for_proxy(cls, parent, owning_class): + target_collection = parent.target_collection + value_attr = parent.value_attr + prop = orm.class_mapper(owning_class).\ + get_property(target_collection) + target_class = prop.mapper.class_ + + target_assoc = cls._cls_unwrap_target_assoc_proxy( + target_class, value_attr) + if target_assoc is not None: + return ObjectAssociationProxyInstance( + parent, owning_class, target_class, value_attr + ) + + is_object = getattr(target_class, value_attr).impl.uses_objects + if is_object: + return ObjectAssociationProxyInstance( + parent, owning_class, target_class, value_attr + ) + else: + return ColumnAssociationProxyInstance( + parent, owning_class, target_class, value_attr + ) def _get_property(self): return orm.class_mapper(self.owning_class).\ @@ -299,13 +336,18 @@ class AssociationProxyInstance(object): def _comparator(self): return self._get_property().comparator - @util.memoized_property - def _unwrap_target_assoc_proxy(self): - attr = getattr(self.target_class, self.value_attr) + @classmethod + def _cls_unwrap_target_assoc_proxy(cls, target_class, value_attr): + attr = getattr(target_class, value_attr) if isinstance(attr, (AssociationProxy, AssociationProxyInstance)): return attr return None + @util.memoized_property + def _unwrap_target_assoc_proxy(self): + return self._cls_unwrap_target_assoc_proxy( + self.target_class, self.value_attr) + @property def remote_attr(self): """The 'remote' :class:`.MapperProperty` referenced by this @@ -353,17 +395,6 @@ class AssociationProxyInstance(object): return (self.local_attr, self.remote_attr) @util.memoized_property - def target_class(self): - """The intermediary class handled by this - :class:`.AssociationProxyInstance`. - - Intercepted append/set/assignment events will result - in the generation of new instances of this class. - - """ - return self._get_property().mapper.class_ - - @util.memoized_property def scalar(self): """Return ``True`` if this :class:`.AssociationProxyInstance` proxies a scalar relationship on the local side.""" @@ -378,9 +409,9 @@ class AssociationProxyInstance(object): return not self._get_property().\ mapper.get_property(self.value_attr).uselist - @util.memoized_property + @property def _target_is_object(self): - return getattr(self.target_class, self.value_attr).impl.uses_objects + raise NotImplementedError() def _initialize_scalar_accessors(self): if self.parent.getset_factory: @@ -587,6 +618,12 @@ class AssociationProxyInstance(object): return self._criterion_exists( criterion=criterion, is_has=True, **kwargs) + +class ObjectAssociationProxyInstance(AssociationProxyInstance): + """an :class:`.AssociationProxyInstance` that has an object as a target. + """ + _target_is_object = True + def contains(self, obj): """Produce a proxied 'contains' expression using EXISTS. @@ -611,7 +648,7 @@ class AssociationProxyInstance(object): elif self._target_is_object and self.scalar and \ self._value_is_scalar: raise exc.InvalidRequestError( - "contains() doesn't apply to a scalar endpoint; use ==") + "contains() doesn't apply to a scalar object endpoint; use ==") else: return self._comparator._criterion_exists(**{self.value_attr: obj}) @@ -634,6 +671,31 @@ class AssociationProxyInstance(object): getattr(self.target_class, self.value_attr) != obj) +class ColumnAssociationProxyInstance( + ColumnOperators, AssociationProxyInstance): + """an :class:`.AssociationProxyInstance` that has a database column as a + target. + """ + _target_is_object = False + + def __eq__(self, other): + # special case "is None" to check for no related row as well + expr = self._criterion_exists( + self.remote_attr.operate(operator.eq, other) + ) + if other is None: + return or_( + expr, self._comparator == None + ) + else: + return expr + + def operate(self, op, *other, **kwargs): + return self._criterion_exists( + self.remote_attr.operate(op, *other, **kwargs) + ) + + class _lazy_collection(object): def __init__(self, obj, target): self.parent = obj |
