chained any() / has() with association proxy

Issue #3769 resolved
Mike Bayer repo owner created an issue
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy

Base = declarative_base()


class A(Base):
    __tablename__ = 'a'
    id = Column(Integer, primary_key=True)

    b_values = association_proxy("atob", "bvalue")


class B(Base):
    __tablename__ = 'b'
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey('a.id'))
    value = Column(String)


class AtoB(Base):
    __tablename__ = 'atob'

    a_id = Column(ForeignKey('a.id'), primary_key=True)
    b_id = Column(ForeignKey('b.id'), primary_key=True)

    a = relationship("A", backref="atob")
    b = relationship("B", backref="atob")

    bvalue = association_proxy("b", "value")

s = Session()

print s.query(A).filter(A.b_values.any(value='hi'))

not supported right now. Needs any() and has() to detect an assocaition proxy and chain:

diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py
index fdc44f3..cfb6a3a 100644
--- a/lib/sqlalchemy/ext/associationproxy.py
+++ b/lib/sqlalchemy/ext/associationproxy.py
@@ -363,6 +363,15 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
     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)
+        if isinstance(attr, AssociationProxy):
+            return attr, getattr(
+                self.target_class, attr.target_collection)
+
+        return None, None
+
     def any(self, criterion=None, **kwargs):
         """Produce a proxied 'any' expression using EXISTS.

@@ -372,6 +381,15 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
         operators of the underlying proxied attributes.

         """
+
+        target_assoc, inner = self._unwrap_target_assoc_proxy
+        if target_assoc is not None:
+            if target_assoc._target_is_object and target_assoc._uselist:
+                inner = inner.any(criterion=criterion, **kwargs)
+            else:
+                inner = inner.has(criterion=criterion, **kwargs)
+            return self._comparator.any(inner)
+
         if self._target_is_object:
             if self._value_is_scalar:
                 value_expr = getattr(
@@ -406,6 +424,14 @@ class AssociationProxy(interfaces.InspectionAttrInfo):

         """

+        target_assoc, inner = self._unwrap_target_assoc_proxy
+        if target_assoc is not None:
+            if target_assoc._target_is_object and target_assoc._uselist:
+                inner = inner.any(criterion=criterion, **kwargs)
+            else:
+                inner = inner.has(criterion=criterion, **kwargs)
+            return self._comparator.has(inner)
+
         if self._target_is_object:
             return self._comparator.has(
                 getattr(self.target_class, self.value_attr).

Comments (3)

  1. Mike Bayer reporter

    Support AssociationProxy any() / has() / contains() to another AssociationProxy

    The :meth:.AssociationProxy.any, :meth:.AssociationProxy.has and :meth:.AssociationProxy.contains comparison methods now support linkage to an attribute that is itself also an :class:.AssociationProxy, recursively.

    After some initial attempts it's clear that the any() / has() of AssociationProxy needed to be reworked into a generic _criterion_exists() to allow this to work recursively without excess complexity. For the case of the multi-linked associationproxy, the usual checks of "any()" / "has()" correctness simply don't take place; for a single-link association proxy the error checking logic that takes place in relationship() has been ported to the local any() / has() methods.

    Change-Id: Ic5aed2a4e910b8138a737d215430113c31cce856 Fixes: #3769

    → <<cset 27a0bdcae0eb>>

  2. Log in to comment