summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/changelog_10.rst9
-rw-r--r--lib/sqlalchemy/orm/strategies.py18
-rw-r--r--test/orm/test_mapper.py41
3 files changed, 65 insertions, 3 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst
index a44b4d62b..c51040dd5 100644
--- a/doc/build/changelog/changelog_10.rst
+++ b/doc/build/changelog/changelog_10.rst
@@ -20,6 +20,15 @@
.. change::
:tags: bug, orm
+ :tickets: 3710
+
+ Fixed bug whereby the event listeners used for backrefs could
+ be inadvertently applied multiple times, when using a deep class
+ inheritance hierarchy in conjunction with mutiple mapper configuration
+ steps.
+
+ .. change::
+ :tags: bug, orm
:tickets: 3706
Fixed bug whereby passing a :func:`.text` construct to the
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 3c03a681d..37cecb079 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -71,8 +71,20 @@ def _register_attribute(
)
)
+ # a single MapperProperty is shared down a class inheritance
+ # hierarchy, so we set up attribute instrumentation and backref event
+ # for each mapper down the hierarchy.
+
+ # typically, "mapper" is the same as prop.parent, due to the way
+ # the configure_mappers() process runs, however this is not strongly
+ # enforced, and in the case of a second configure_mappers() run the
+ # mapper here might not be prop.parent; also, a subclass mapper may
+ # be called here before a superclass mapper. That is, can't depend
+ # on mappers not already being set up so we have to check each one.
+
for m in mapper.self_and_descendants:
- if prop is m._props.get(prop.key):
+ if prop is m._props.get(prop.key) and \
+ not m.class_manager._attr_has_impl(prop.key):
desc = attributes.register_attribute_impl(
m.class_,
@@ -83,8 +95,8 @@ def _register_attribute(
useobject=useobject,
extension=attribute_ext,
trackparent=useobject and (
- prop.single_parent
- or prop.direction is interfaces.ONETOMANY),
+ prop.single_parent or
+ prop.direction is interfaces.ONETOMANY),
typecallable=typecallable,
callable_=callable_,
active_history=active_history,
diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py
index 69a039681..e357a7e25 100644
--- a/test/orm/test_mapper.py
+++ b/test/orm/test_mapper.py
@@ -373,6 +373,47 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
})
assert getattr(Foo().__class__, 'name').impl is not None
+ def test_class_hier_only_instrument_once_multiple_configure(self):
+ users, addresses = (self.tables.users, self.tables.addresses)
+
+ class A(object):
+ pass
+
+ class ASub(A):
+ pass
+
+ class ASubSub(ASub):
+ pass
+
+ class B(object):
+ pass
+
+ from sqlalchemy.testing import mock
+ from sqlalchemy.orm.attributes import register_attribute_impl
+
+ with mock.patch(
+ "sqlalchemy.orm.attributes.register_attribute_impl",
+ side_effect=register_attribute_impl
+ ) as some_mock:
+
+ mapper(A, users, properties={
+ 'bs': relationship(B)
+ })
+ mapper(B, addresses)
+
+ configure_mappers()
+
+ mapper(ASub, inherits=A)
+ mapper(ASubSub, inherits=ASub)
+
+ configure_mappers()
+
+ b_calls = [
+ c for c in some_mock.mock_calls if c[1][1] == 'bs'
+ ]
+ eq_(len(b_calls), 3)
+
+
def test_check_descriptor_as_method(self):
User, users = self.classes.User, self.tables.users