]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Document caveat about backrefs and attribute_mapped_collection
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 26 Aug 2020 15:44:34 +0000 (11:44 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 26 Aug 2020 15:44:34 +0000 (11:44 -0400)
Fixes: #5538
Change-Id: I2bda6bed40d35560a71bf0ed09d141047ce59e82

doc/build/orm/collections.rst
lib/sqlalchemy/orm/collections.py

index 8be69963056055937d8f4178b2f1b48adcfb79ff..e37d85566c9a5447166ecb9d55e6cd1b0e29cf93 100644 (file)
@@ -306,6 +306,88 @@ Dictionary mappings are often combined with the "Association Proxy" extension to
 streamlined dictionary views.  See :ref:`proxying_dictionaries` and :ref:`composite_association_proxy`
 for examples.
 
+.. _key_collections_mutations:
+
+Dealing with Key Mutations and back-populating for Dictionary collections
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When using :func:`.attribute_mapped_collection`, the "key" for the dictionary
+is taken from an attribute on the target object.   **Changes to this key
+are not tracked**.  This means that the key must be assigned towards when
+it is first used, and if the key changes, the collection will not be mutated.
+A typical example where this might be an issue is when relying upon backrefs
+to populate an attribute mapped collection.  Given the following::
+
+    class A(Base):
+        __tablename__ = "a"
+
+        id = Column(Integer, primary_key=True)
+        bs = relationship(
+            "B",
+            collection_class=attribute_mapped_collection("data"),
+            back_populates="a",
+        )
+
+
+    class B(Base):
+        __tablename__ = "b"
+        id = Column(Integer, primary_key=True)
+        a_id = Column(ForeignKey("a.id"))
+        data = Column(String)
+
+        a = relationship("A", back_populates="bs")
+
+Above, if we create a ``B()`` that refers to a specific ``A()``, the back
+populates will then add the ``B()`` to the ``A.bs`` collection, however
+if the value of ``B.data`` is not set yet, the key will be ``None``::
+
+    >>> a1 = A()
+    >>> b1 = B(a=a1)
+    >>> a1.bs
+    {None: <test3.B object at 0x7f7b1023ef70>}
+
+
+Setting ``b1.data`` after the fact does not update the collection::
+
+    >>> b1.data = 'the key'
+    >>> a1.bs
+    {None: <test3.B object at 0x7f7b1023ef70>}
+
+
+This can also be seen if one attempts to set up ``B()`` in the constructor.
+The order of arguments changes the result::
+
+    >>> B(a=a1, data='the key')
+    <test3.B object at 0x7f7b10114280>
+    >>> a1.bs
+    {None: <test3.B object at 0x7f7b10114280>}
+
+vs::
+
+    >>> B(data='the key', a=a1)
+    <test3.B object at 0x7f7b10114340>
+    >>> a1.bs
+    {'the key': <test3.B object at 0x7f7b10114340>}
+
+If backrefs are being used in this way, ensure that attributes are populated
+in the correct order using an ``__init__`` method.
+
+An event handler such as the following may also be used to track changes in the
+collection as well::
+
+    from sqlalchemy import event
+
+    from sqlalchemy.orm import attributes
+
+    @event.listens_for(B.data, "set")
+    def set_item(obj, value, previous, initiator):
+        if obj.a is not None:
+            previous = None if previous == attributes.NO_VALUE else previous
+            obj.a.bs[value] = obj
+            obj.a.bs.pop(previous)
+
+
+
 .. autofunction:: attribute_mapped_collection
 
 .. autofunction:: column_mapped_collection
index 9d68179e5b0f460b720fe5fe62acd443204161de..262aeaf042555950ad3aba125d72d26df1faa333 100644 (file)
@@ -270,10 +270,13 @@ def attribute_mapped_collection(attr_name):
     'attr_name' attribute of entities in the collection, where ``attr_name``
     is the string name of the attribute.
 
-    The key value must be immutable for the lifetime of the object.  You
-    can not, for example, map on foreign key values if those key values will
-    change during the session, i.e. from None to a database-assigned integer
-    after a session flush.
+    .. warning:: the key value must be assigned to its final value
+       **before** it is accessed by the attribute mapped collection.
+       Additionally, changes to the key attribute are **not tracked**
+       automatically, which means the key in the dictionary is not
+       automatically synchronized with the key value on the target object
+       itself.  See the section :ref:`key_collections_mutations`
+       for an example.
 
     """
     getter = _SerializableAttrGetter(attr_name)