id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
- addresses = relationship("Address", backref="user", cascade_backrefs=True)
+ addresses = relationship("Address", backref="user")
class Address(Base):
__tablename__ = 'address'
>>> a1 is existing_a1
False
-Above, our ``a1`` is already pending in the session. The subsequent
-:meth:`~.Session.merge` operation essentially does nothing. To resolve this
-issue, the ``cascade_backrefs`` flag should be set to its default of
-``False``. Further detail on cascade operation is at
-:ref:`unitofwork_cascades`.
-
+Above, our ``a1`` is already pending in the session. The
+subsequent :meth:`~.Session.merge` operation essentially
+does nothing. Cascade can be configured via the ``cascade``
+option on :func:`.relationship`, although in this case it
+would mean removing the ``save-update`` cascade from the
+``User.addresses`` relationship - and usually, that behavior
+is extremely convenient. The solution here would usually be to not assign
+``a1.user`` to an object already persistent in the target
+session.
-.. note:: Up until version 0.7 of SQLAlchemy, the "cascade backrefs" flag
- defaulted to ``True`` and prior to 0.6.5 there was no way to disable it,
- without turning off "save-update" cascade entirely.
+Note that a new :func:`.relationship` option introduced in 0.6.5,
+``cascade_backrefs=False``, will also prevent the ``Address`` from
+being added to the session via the ``a1.user = u1`` assignment.
+Further detail on cascade operation is at :ref:`unitofwork_cascades`.
Another example of unexpected state::
The default value for ``cascade`` on :func:`~sqlalchemy.orm.relationship` is
``save-update, merge``.
-``save-update`` cascade, by default, does not take place on backrefs (new in 0.7).
-This means that, given a mapping such as this::
+``save-update`` cascade also takes place on backrefs by default. This means
+that, given a mapping such as this::
mapper(Order, order_table, properties={
'items' : relationship(Item, items_table, backref='order')
If an ``Order`` is already in the session, and is assigned to the ``order``
attribute of an ``Item``, the backref appends the ``Item`` to the ``orders``
-collection of that ``Order``, however, the ``Item`` will not yet be present
-in the session::
+collection of that ``Order``, resulting in the ``save-update`` cascade taking
+place::
>>> o1 = Order()
>>> session.add(o1)
>>> i1 in o1.orders
True
>>> i1 in session
- False
-
-You can of course :func:`~.Session.add` ``i1`` to the session at a later
-point. SQLAlchemy defaults to this behavior as of 0.7 as it is helpful for
-situations where an object needs to be kept out of a session until it's
-construction is completed, but still needs to be given associations to objects
-which are already persistent in the target session. It's more intuitive
-that "cascades" into a :class:`.Session` work from left to right only, and also
-allows session membership behavior to be more compatible with relationships that
-don't have backrefs configured.
-
-The save-update cascade can be made bi-directional using ``cascade_backrefs`` flag::
+ True
+
+This behavior can be disabled as of 0.6.5 using the ``cascade_backrefs`` flag::
mapper(Order, order_table, properties={
'items' : relationship(Item, items_table, backref='order',
- cascade_backrefs=True)
+ cascade_backrefs=False)
})
-Using the above mapping with the previous usage example,
-the assignment of ``i1.order = o1`` will append ``i1`` to the ``orders``
-collection of ``o1``, and will add ``i1`` to the session. ``cascade_backrefs``
-is specific to just one direction, so the above configuration still would not
-cascade an ``Order`` into the session if it were associated with an ``Item``
-already in the session, unless ``cascade_backrefs`` were also configured on the
-"order" side of the relationship.
+So above, the assignment of ``i1.order = o1`` will append ``i1`` to the ``orders``
+collection of ``o1``, but will not add ``i1`` to the session. You can of
+course :func:`~.Session.add` ``i1`` to the session at a later point. This option
+may be helpful for situations where an object needs to be kept out of a
+session until it's construction is completed, but still needs to be given
+associations to objects which are already persistent in the target session.
-Setting ``cascade_backrefs=True`` on a relationship and its backref makes the
-operation compatible with the default behavior of SQLAlchemy 0.6 and earlier.
.. _unitofwork_transaction:
* ``all`` - shorthand for "save-update,merge, refresh-expire,
expunge, delete"
- :param cascade_backrefs=False:
+ :param cascade_backrefs=True:
a boolean value indicating if the ``save-update`` cascade should
- operate along a backref event. When set to ``True`` on a
+ operate along a backref event. When set to ``False`` on a
one-to-many relationship that has a many-to-one backref, assigning
a persistent object to the many-to-one attribute on a transient object
- will add the transient to the session. Similarly, when
- set to ``True`` on a many-to-one relationship that has a one-to-many
+ will not add the transient to the session. Similarly, when
+ set to ``False`` on a many-to-one relationship that has a one-to-many
backref, appending a persistent object to the one-to-many collection
- on a transient object will add the transient to the session.
+ on a transient object will not add the transient to the session.
- ``cascade_backrefs`` is new in 0.6.5. The default value changes
- from ``True`` to ``False`` as of the 0.7 series.
+ ``cascade_backrefs`` is new in 0.6.5.
:param collection_class:
a class or callable that returns a new list-holding object. will
single_parent=False, innerjoin=False,
doc=None,
active_history=False,
- cascade_backrefs=False,
+ cascade_backrefs=True,
load_on_pending=False,
strategy_class=None, _local_remote_pairs=None,
query_class=None):
c = session.query(Company).first()
daboss.company = c
- session.add(daboss)
manager_list = [e for e in c.employees if isinstance(e, Manager)]
session.flush()
session.expunge_all()
Derived from mailing list-reported problems and trac tickets.
-These are generally very old 0.1-era tests and at some point should
-be cleaned up and modernized.
-
"""
import datetime
Column('company_id', Integer, ForeignKey("companies.company_id")),
Column('date', sa.DateTime))
+ Table('items', metadata,
+ Column('item_id', Integer, primary_key=True, test_needs_autoincrement=True),
+ Column('invoice_id', Integer, ForeignKey('invoices.invoice_id')),
+ Column('code', String(20)),
+ Column('qty', Integer))
+
@classmethod
def setup_classes(cls):
class Company(_base.ComparableEntity):
class Phone(_base.ComparableEntity):
pass
+ class Item(_base.ComparableEntity):
+ pass
+
class Invoice(_base.ComparableEntity):
pass
@testing.resolve_artifact_names
- def test_load_m2o_attached_to_o2m(self):
+ def testone(self):
"""
Tests eager load of a many-to-one attached to a one-to-many. this
testcase illustrated the bug, which is that when the single Company is
session.expunge_all()
i = session.query(Invoice).get(invoice_id)
-
- def go():
- eq_(c, i.company)
- eq_(c.addresses, i.company.addresses)
- self.assert_sql_count(testing.db, go, 0)
+ eq_(c, i.company)
+
+ @testing.resolve_artifact_names
+ def testtwo(self):
+ """The original testcase that includes various complicating factors"""
+
+ mapper(Phone, phone_numbers)
+
+ mapper(Address, addresses, properties={
+ 'phones': relationship(Phone, lazy='joined', backref='address',
+ order_by=phone_numbers.c.phone_id)})
+
+ mapper(Company, companies, properties={
+ 'addresses': relationship(Address, lazy='joined', backref='company',
+ order_by=addresses.c.address_id)})
+
+ mapper(Item, items)
+
+ mapper(Invoice, invoices, properties={
+ 'items': relationship(Item, lazy='joined', backref='invoice',
+ order_by=items.c.item_id),
+ 'company': relationship(Company, lazy='joined', backref='invoices')})
+
+ c1 = Company(company_name='company 1', addresses=[
+ Address(address='a1 address',
+ phones=[Phone(type='home', number='1111'),
+ Phone(type='work', number='22222')]),
+ Address(address='a2 address',
+ phones=[Phone(type='home', number='3333'),
+ Phone(type='work', number='44444')])
+ ])
+
+ session = create_session()
+ session.add(c1)
+ session.flush()
+
+ company_id = c1.company_id
+
+ session.expunge_all()
+
+ a = session.query(Company).get(company_id)
+
+ # set up an invoice
+ i1 = Invoice(date=datetime.datetime.now(), company=a)
+
+ item1 = Item(code='aaaa', qty=1, invoice=i1)
+ item2 = Item(code='bbbb', qty=2, invoice=i1)
+ item3 = Item(code='cccc', qty=3, invoice=i1)
+
+ session.flush()
+ invoice_id = i1.invoice_id
+
+ session.expunge_all()
+ c = session.query(Company).get(company_id)
+
+ session.expunge_all()
+ i = session.query(Invoice).get(invoice_id)
+
+ eq_(c, i.company)
class EagerTest8(_base.MappedTest):
# the _SingleParent extension sets the backref get to "active" !
# u1 gets loaded and deleted
u2.address = a1
- sess.add(u2)
sess.commit()
assert sess.query(User).count() == 1
def setup_mappers(cls):
mapper(Address, addresses)
mapper(User, users, properties={
- 'addresses':relationship(Address,
- backref=backref('user', cascade_backrefs=True),
- )
+ 'addresses':relationship(Address, backref='user',
+ cascade_backrefs=False)
})
mapper(Dingaling, dingalings, properties={
- 'address' : relationship(Address,
- backref=backref('dingalings', cascade_backrefs=True),
- )
+ 'address' : relationship(Address, backref='dingalings',
+ cascade_backrefs=False)
})
@testing.resolve_artifact_names
u1 = User(name='u1')
a1.user = u1
-
sess.flush()
# expire 'addresses'. backrefs
# in user.addresses
a2 = Address(email_address='a2')
a2.user = u1
- sess.add(a2)
-
+
# expire u1.addresses again. this expires
# "pending" as well.
sess.expire(u1, ['addresses'])
# only two addresses pulled from the DB, no "pending"
assert len(u1.addresses) == 2
- # flushes a2
sess.flush()
sess.expire_all()
assert len(u1.addresses) == 3
h6 = H6()
h6.h1a = h1
h6.h1b = x = H1()
- s.add(x)
+ assert x in s
h6.h1b.h2s.append(H2())
import operator
from sqlalchemy.test import testing
from sqlalchemy.util import OrderedSet
-from sqlalchemy.orm import mapper, relationship, create_session, \
- PropComparator, synonym, comparable_property, sessionmaker, \
- attributes, Session
+from sqlalchemy.orm import mapper, relationship, create_session, PropComparator, \
+ synonym, comparable_property, sessionmaker, attributes
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.interfaces import MapperOption
from sqlalchemy.test.testing import eq_, ne_
def test_cascade_doesnt_blowaway_manytoone(self):
"""a merge test that was fixed by [ticket:1202]"""
- s = Session()
-
- mapper(Address, addresses)
+ s = create_session(autoflush=True)
mapper(User, users, properties={
- 'addresses':relationship(Address,backref='user')
- })
+ 'addresses':relationship(mapper(Address, addresses),backref='user')})
- u1 = s.merge(User(id=1, name='ed'))
- a1 = Address(user=u1, email_address='x')
- s.add(a1)
-
+ a1 = Address(user=s.merge(User(id=1, name='ed')), email_address='x')
before_id = id(a1.user)
-
- # autoflushes a1, u1
- u2 = s.merge(User(id=1, name='jack'))
- a2 = Address(user=u2, email_address='x')
+ a2 = Address(user=s.merge(User(id=1, name='jack')), email_address='x')
after_id = id(a1.user)
other_id = id(a2.user)
eq_(before_id, other_id)