since ``a1.user`` implies ``u1.addresses.append(a1)``, ``a1`` would get
cascaded into the :class:`_orm.Session`. This remains the default behavior
throughout 1.4. At some point, a new flag :paramref:`_orm.relationship.cascade_backrefs`
-was added to disable to above behavior, as it can be surprising and also gets in
-the way of some operations where the object would be placed in the :class:`_orm.Session`
-too early and get prematurely flushed.
+was added to disable to above behavior, along with :paramref:`_orm.backref.cascade_backrefs`
+to set this when the relationship is specified by ``relationship.backref``, as it can be
+surprising and also gets in the way of some operations where the object would be placed in
+the :class:`_orm.Session` too early and get prematurely flushed.
In 2.0, the default behavior will be that "cascade_backrefs" is False, and
additionally there will be no "True" behavior as this is not generally a desirable
behavior. When 2.0 deprecation warnings are enabled, a warning will be emitted
when a "backref cascade" actually takes place. To get the new behavior, either
-set :paramref:`_orm.relationship.cascade_backrefs` to ``False`` on the target
-relationship, as is already supported in 1.3 and earlier, or alternatively make
+set :paramref:`_orm.relationship.cascade_backrefs` and
+:paramref:`_orm.backref.cascade_backrefs` to ``False`` on any target
+relationships, as is already supported in 1.3 and earlier, or alternatively make
use of the :paramref:`_orm.Session.future` flag to :term:`2.0-style` mode::
Session = sessionmaker(engine, future=True)
:paramref:`_orm.Session.future` flag is set on :class:`_orm.sessionmaker`
or :class:`_orm.Session`.
+.. _error_s9r1:
+
+Object is being merged into a Session along the backref cascade
+---------------------------------------------------------------
+
+This message refers to the "backref cascade" behavior of SQLAlchemy,
+which is described at :ref:`backref_cascade`. This refers to the action of
+an object being added into a :class:`_orm.Session` as a result of another
+object that's already present in that session being associated with it.
+As this behavior has been shown to be more confusing than helpful,
+the :paramref:`_orm.relationship.cascade_backrefs` and
+:paramref:`_orm.backref.cascade_backrefs` parameters were added, which can
+be set to ``False`` to disable it, and in SQLAlchemy 2.0 the "cascade backrefs"
+behavior will be disabled completely.
+
+To set :paramref:`_orm.relationship.cascade_backrefs` to ``False`` on a
+backref that is currently configured using the
+:paramref:`_orm.relationship.backref` string parameter, the backref must
+be declared using the :func:`_orm.backref` function first so that the
+:paramref:`_orm.backref.cascade_backrefs` parameter may be passed.
+
+Alternatively, the entire "cascade backrefs" behavior can be turned off
+across the board by using the :class:`_orm.Session` in "future" mode,
+by passing ``True`` for the :paramref:`_orm.Session.future` parameter.
+
+.. seealso::
+
+ :ref:`backref_cascade` - complete description of the cascade backrefs
+ behavior
+
+ :ref:`change_5150` - background on the change for SQLAlchemy 2.0.
+
Connections and Transactions
============================
session until it's construction is completed, but still needs to be given
associations to objects which are already persistent in the target session.
+When relationships are created by the :paramref:`_orm.relationship.backref`
+parameter on :func:`_orm.relationship`, the :paramref:`_orm.cascade_backrefs`
+parameter may be set to ``False`` on the backref side by using the
+:func:`_orm.backref` function instead of a string. For example, the above relationship
+could be declared::
+ mapper_registry.map_imperatively(Order, order_table, properties={
+ 'items' : relationship(
+ Item, backref=backref('order', cascade_backrefs=False), cascade_backrefs=False
+ )
+ })
+
+This sets the ``cascade_backrefs=False`` behavior on both relationships.
.. _session_deleting_from_collections:
_version_token = None
-class SQLAlchemyError(Exception):
- """Generic error class."""
+class HasDescriptionCode(object):
+ """helper which adds 'code' as an attribute and '_code_str' as a method"""
code = None
code = kw.pop("code", None)
if code is not None:
self.code = code
- super(SQLAlchemyError, self).__init__(*arg, **kw)
+ super(HasDescriptionCode, self).__init__(*arg, **kw)
def _code_str(self):
if not self.code:
)
)
+
+class SQLAlchemyError(HasDescriptionCode, Exception):
+ """Generic error class."""
+
def _message(self, as_unicode=compat.py3k):
# rules:
#
# Warnings
-class SADeprecationWarning(DeprecationWarning):
+class SADeprecationWarning(HasDescriptionCode, DeprecationWarning):
"""Issued for usage of deprecated APIs."""
deprecated_since = None
"Indicates the version that started raising this deprecation warning"
+ def __str__(self):
+ message = super(SADeprecationWarning, self).__str__()
+ if self.code:
+ message = "%s %s" % (message, self._code_str())
+ return message
+
class RemovedIn20Warning(SADeprecationWarning):
"""Issued for usage of APIs specifically deprecated in SQLAlchemy 2.0.
deprecated_since = "1.4"
"Indicates the version that started raising this deprecation warning"
+ def __str__(self):
+ return (
+ super(RemovedIn20Warning, self).__str__()
+ + " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)"
+ )
+
class MovedIn20Warning(RemovedIn20Warning):
"""Subtype of RemovedIn20Warning to indicate an API that moved only."""
"Indicates the version that started raising this deprecation warning"
-class SAWarning(RuntimeWarning):
+class SAWarning(HasDescriptionCode, RuntimeWarning):
"""Issued at runtime."""
'"%s" object is being merged into a Session along the backref '
'cascade path for relationship "%s"; in SQLAlchemy 2.0, this '
"reverse cascade will not take place. Set cascade_backrefs to "
- "False for the 2.0 behavior; or to set globally for the whole "
- "Session, set the future=True flag" % (state.class_.__name__, prop)
+ "False in either the relationship() or backref() function for "
+ "the 2.0 behavior; or to set globally for the whole "
+ "Session, set the future=True flag" % (state.class_.__name__, prop),
+ code="s9r1",
)
SQLALCHEMY_WARN_20 = True
-def _warn_with_version(msg, version, type_, stacklevel):
- is_20 = issubclass(type_, exc.RemovedIn20Warning)
-
- if is_20 and not SQLALCHEMY_WARN_20:
+def _warn_with_version(msg, version, type_, stacklevel, code=None):
+ if issubclass(type_, exc.RemovedIn20Warning) and not SQLALCHEMY_WARN_20:
return
- if is_20:
- msg += " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)"
-
- warn = type_(msg)
+ warn = type_(msg, code=code)
warn.deprecated_since = version
_warnings_warn(warn, stacklevel=stacklevel + 1)
-def warn_deprecated(msg, version, stacklevel=3):
- _warn_with_version(msg, version, exc.SADeprecationWarning, stacklevel)
+def warn_deprecated(msg, version, stacklevel=3, code=None):
+ _warn_with_version(
+ msg, version, exc.SADeprecationWarning, stacklevel, code=code
+ )
-def warn_deprecated_limited(msg, args, version, stacklevel=3):
+def warn_deprecated_limited(msg, args, version, stacklevel=3, code=None):
"""Issue a deprecation warning with a parameterized string,
limiting the number of registrations.
"""
if args:
msg = _hash_limit_string(msg, 10, args)
- _warn_with_version(msg, version, exc.SADeprecationWarning, stacklevel)
+ _warn_with_version(
+ msg, version, exc.SADeprecationWarning, stacklevel, code=code
+ )
-def warn_deprecated_20(msg, stacklevel=3):
+def warn_deprecated_20(msg, stacklevel=3, code=None):
_warn_with_version(
msg,
exc.RemovedIn20Warning.deprecated_since,
exc.RemovedIn20Warning,
stacklevel,
+ code=code,
)
"""
if code:
- msg = "%s %s" % (msg, exc.SQLAlchemyError(msg, code=code)._code_str())
-
- _warnings_warn(msg, exc.SAWarning)
+ _warnings_warn(exc.SAWarning(msg, code=code))
+ else:
+ _warnings_warn(msg, exc.SAWarning)
def warn_limited(msg, args):
with testing.expect_deprecated(
'"Address" object is being merged into a Session along '
'the backref cascade path for relationship "User.addresses"'
+ # link added to this specific warning
+ r".*Background on this error at: http://sqlalche.me/e/14/s9r1"
+ # link added to all RemovedIn20Warnings
+ r".*Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9"
):
a1.user = u1
sess.add(a1)