chained any() / has() with association proxy
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)
-
-
reporter this became a lot more involved. ultimately the fix is simple but had to cover more cases:
-
reporter - changed status to resolved
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>>
- Log in to comment
I can confirm the patch worked. I have actually monkey patched SQLAlchemy 1.0.14 from within my application like so:
https://github.com/Natureshadow/OSMAlchemy/blob/master/osmalchemy/util/patch.py