]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The :mod:`sqlalchemy.ext.automap` extension will now set
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 23 Sep 2014 03:00:45 +0000 (23:00 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 23 Sep 2014 03:00:45 +0000 (23:00 -0400)
``cascade="all, delete-orphan"`` automatically on a one-to-many
relationship/backref where the foreign key is detected as containing
one or more non-nullable columns.  This argument is present in the
keywords passed to :func:`.automap.generate_relationship` in this
case and can still be overridden.  Additionally, if the
:class:`.ForeignKeyConstraint` specifies ``ondelete="CASCADE"``
for a non-nullable or ``ondelete="SET NULL"`` for a nullable set
of columns, the argument ``passive_deletes=True`` is also added to the
relationship.  Note that not all backends support reflection of
ondelete, but backends that do include Postgresql and MySQL.
fixes #3210

doc/build/changelog/changelog_10.rst
lib/sqlalchemy/ext/automap.py
test/ext/test_automap.py

index 7d7548e1173b8886359a51c990368f5d9d27fb13..88cae563f23e1189240706d5ee01442fe3c2a44b 100644 (file)
     series as well.  For changes that are specific to 1.0 with an emphasis
     on compatibility concerns, see :doc:`/changelog/migration_10`.
 
+    .. change::
+        :tags: feature, ext
+        :tickets: 3210
+
+        The :mod:`sqlalchemy.ext.automap` extension will now set
+        ``cascade="all, delete-orphan"`` automatically on a one-to-many
+        relationship/backref where the foreign key is detected as containing
+        one or more non-nullable columns.  This argument is present in the
+        keywords passed to :func:`.automap.generate_relationship` in this
+        case and can still be overridden.  Additionally, if the
+        :class:`.ForeignKeyConstraint` specifies ``ondelete="CASCADE"``
+        for a non-nullable or ``ondelete="SET NULL"`` for a nullable set
+        of columns, the argument ``passive_deletes=True`` is also added to the
+        relationship.  Note that not all backends support reflection of
+        ondelete, but backends that do include Postgresql and MySQL.
+
     .. change::
         :tags: feature, sql
         :tickets: 3206
index 121285ab3a8809d805cac807e709c7f3f934fff5..c11795d37267cb10152b3945c70898be66842ef3 100644 (file)
@@ -243,7 +243,26 @@ follows:
    one-to-many backref will be created on the referred class referring
    to this class.
 
-4. The names of the relationships are determined using the
+4. If any of the columns that are part of the :class:`.ForeignKeyConstraint`
+   are not nullable (e.g. ``nullable=False``), a
+   :paramref:`~.relationship.cascade` keyword argument
+   of ``all, delete-orphan`` will be added to the keyword arguments to
+   be passed to the relationship or backref.  If the
+   :class:`.ForeignKeyConstraint` reports that
+   :paramref:`.ForeignKeyConstraint.ondelete`
+   is set to ``CASCADE`` for a not null or ``SET NULL`` for a nullable
+   set of columns, the option :paramref:`~.relationship.passive_deletes`
+   flag is set to ``True`` in the set of relationship keyword arguments.
+   Note that not all backends support reflection of ON DELETE.
+
+   .. versionadded:: 1.0.0 - automap will detect non-nullable foreign key
+      constraints when producing a one-to-many relationship and establish
+      a default cascade of ``all, delete-orphan`` if so; additionally,
+      if the constraint specifies :paramref:`.ForeignKeyConstraint.ondelete`
+      of ``CASCADE`` for non-nullable or ``SET NULL`` for nullable columns,
+      the ``passive_deletes=True`` option is also added.
+
+5. The names of the relationships are determined using the
    :paramref:`.AutomapBase.prepare.name_for_scalar_relationship` and
    :paramref:`.AutomapBase.prepare.name_for_collection_relationship`
    callable functions.  It is important to note that the default relationship
@@ -252,18 +271,18 @@ follows:
    alternate class naming scheme, that's the name from which the relationship
    name will be derived.
 
-5. The classes are inspected for an existing mapped property matching these
+6. The classes are inspected for an existing mapped property matching these
    names.  If one is detected on one side, but none on the other side,
    :class:`.AutomapBase` attempts to create a relationship on the missing side,
    then uses the :paramref:`.relationship.back_populates` parameter in order to
    point the new relationship to the other side.
 
-6. In the usual case where no relationship is on either side,
+7. In the usual case where no relationship is on either side,
    :meth:`.AutomapBase.prepare` produces a :func:`.relationship` on the
    "many-to-one" side and matches it to the other using the
    :paramref:`.relationship.backref` parameter.
 
-7. Production of the :func:`.relationship` and optionally the :func:`.backref`
+8. Production of the :func:`.relationship` and optionally the :func:`.backref`
    is handed off to the :paramref:`.AutomapBase.prepare.generate_relationship`
    function, which can be supplied by the end-user in order to augment
    the arguments passed to :func:`.relationship` or :func:`.backref` or to
@@ -877,6 +896,19 @@ def _relationships_for_fks(automap_base, map_config, table_to_map_config,
                 constraint
             )
 
+            o2m_kws = {}
+            nullable = False not in set([fk.parent.nullable for fk in fks])
+            if not nullable:
+                o2m_kws['cascade'] = "all, delete-orphan"
+
+                if constraint.ondelete and \
+                        constraint.ondelete.lower() == "cascade":
+                    o2m_kws['passive_deletes'] = True
+            else:
+                if constraint.ondelete and \
+                        constraint.ondelete.lower() == "set null":
+                    o2m_kws['passive_deletes'] = True
+
             create_backref = backref_name not in referred_cfg.properties
 
             if relationship_name not in map_config.properties:
@@ -885,7 +917,8 @@ def _relationships_for_fks(automap_base, map_config, table_to_map_config,
                         automap_base,
                         interfaces.ONETOMANY, backref,
                         backref_name, referred_cls, local_cls,
-                        collection_class=collection_class)
+                        collection_class=collection_class,
+                        **o2m_kws)
                 else:
                     backref_obj = None
                 rel = generate_relationship(automap_base,
@@ -916,7 +949,8 @@ def _relationships_for_fks(automap_base, map_config, table_to_map_config,
                                                 fk.parent
                                                 for fk in constraint.elements],
                                             back_populates=relationship_name,
-                                            collection_class=collection_class)
+                                            collection_class=collection_class,
+                                            **o2m_kws)
                 if rel is not None:
                     referred_cfg.properties[backref_name] = rel
                     map_config.properties[
index 6cfd0fbcacb210700a0c260396ccec4c7dcc9f48..0a57b9caa542e1146c945f4d5b42e47545692f08 100644 (file)
@@ -1,7 +1,7 @@
 from sqlalchemy.testing import fixtures
 from ..orm._fixtures import FixtureTest
 from sqlalchemy.ext.automap import automap_base
-from sqlalchemy.orm import relationship, interfaces
+from sqlalchemy.orm import relationship, interfaces, configure_mappers
 from sqlalchemy.ext.automap import generate_relationship
 from sqlalchemy.testing.mock import Mock
 from sqlalchemy import String, Integer, ForeignKey
@@ -157,6 +157,72 @@ class AutomapTest(fixtures.MappedTest):
         ])
 
 
+class CascadeTest(fixtures.MappedTest):
+    @classmethod
+    def define_tables(cls, metadata):
+        Table(
+            "a", metadata,
+            Column('id', Integer, primary_key=True)
+        )
+        Table(
+            "b", metadata,
+            Column('id', Integer, primary_key=True),
+            Column('aid', ForeignKey('a.id'), nullable=True)
+        )
+        Table(
+            "c", metadata,
+            Column('id', Integer, primary_key=True),
+            Column('aid', ForeignKey('a.id'), nullable=False)
+        )
+        Table(
+            "d", metadata,
+            Column('id', Integer, primary_key=True),
+            Column(
+                'aid', ForeignKey('a.id', ondelete="cascade"), nullable=False)
+        )
+        Table(
+            "e", metadata,
+            Column('id', Integer, primary_key=True),
+            Column(
+                'aid', ForeignKey('a.id', ondelete="set null"),
+                nullable=True)
+        )
+
+    def test_o2m_relationship_cascade(self):
+        Base = automap_base(metadata=self.metadata)
+        Base.prepare()
+
+        configure_mappers()
+
+        b_rel = Base.classes.a.b_collection
+        assert not b_rel.property.cascade.delete
+        assert not b_rel.property.cascade.delete_orphan
+        assert not b_rel.property.passive_deletes
+
+        assert b_rel.property.cascade.save_update
+
+        c_rel = Base.classes.a.c_collection
+        assert c_rel.property.cascade.delete
+        assert c_rel.property.cascade.delete_orphan
+        assert not c_rel.property.passive_deletes
+
+        assert c_rel.property.cascade.save_update
+
+        d_rel = Base.classes.a.d_collection
+        assert d_rel.property.cascade.delete
+        assert d_rel.property.cascade.delete_orphan
+        assert d_rel.property.passive_deletes
+
+        assert d_rel.property.cascade.save_update
+
+        e_rel = Base.classes.a.e_collection
+        assert not e_rel.property.cascade.delete
+        assert not e_rel.property.cascade.delete_orphan
+        assert e_rel.property.passive_deletes
+
+        assert e_rel.property.cascade.save_update
+
+
 class AutomapInhTest(fixtures.MappedTest):
     @classmethod
     def define_tables(cls, metadata):