diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2009-01-20 21:35:57 +0000 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2009-01-20 21:35:57 +0000 |
| commit | 7c56371f81707b5979249b2f2b056f65488f1bab (patch) | |
| tree | 3d2d9cc427e8095e453327c56bca4b1c7fa36d20 /lib | |
| parent | 9fc05aae02015011052ad0da2bd602951b12c071 (diff) | |
| download | sqlalchemy-7c56371f81707b5979249b2f2b056f65488f1bab.tar.gz | |
- Further refined 0.5.1's warning about delete-orphan cascade
placed on a many-to-many relation. First, the bad news:
the warning will apply to both many-to-many as well as
many-to-one relations. This is necessary since in both
cases, SQLA does not scan the full set of potential parents
when determining "orphan" status - for a persistent object
it only detects an in-python de-association event to establish
the object as an "orphan". Next, the good news: to support
one-to-one via a foreign key or assocation table, or to
support one-to-many via an association table, a new flag
single_parent=True may be set which indicates objects
linked to the relation are only meant to have a single parent.
The relation will raise an error if multiple parent-association
events occur within Python.
- Fixed bug in delete-orphan cascade whereby two one-to-one
relations from two different parent classes to the same target
class would prematurely expunge the instance. This is
an extension of the non-ticketed fix in r4247.
- the order of "sethasparent" flagging in relation to
AttributeExtensions has been refined such that false setparents
are issued before the event, true setparents issued afterwards.
event handlers "know" that a remove event originates
from a non-orphan but need to know if its become an orphan,
and that append events will become non-orphans but need to know
if the event originates from a non-orphan.
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 10 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/properties.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 26 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/unitofwork.py | 3 |
5 files changed, 50 insertions, 9 deletions
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index e9d98ac34..7e64bda7a 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -388,6 +388,14 @@ def relation(argument, secondary=None, **kwargs): based on the foreign key relationships of the association and child tables. + :param single_parent=(True|False): + when True, installs a validator which will prevent objects + from being associated with more than one parent at a time. + This is used for many-to-one or many-to-many relationships that + should be treated either as one-to-one or one-to-many. Its + usage is optional unless delete-orphan cascade is also + set on this relation(), in which case its required (new in 0.5.2). + :param uselist=(True|False): a boolean that indicates if this property should be loaded as a list or a scalar. In most cases, this value is determined @@ -400,7 +408,7 @@ def relation(argument, secondary=None, **kwargs): :param viewonly=False: when set to True, the relation is used only for loading objects within the relationship, and has no effect on the unit-of-work - flush process. Relations with viewonly can specify any kind of + flush process. Relationships with viewonly can specify any kind of join conditions to provide additional views of related objects onto a parent object. Note that the functionality of a viewonly relationship has its limits - complicated join conditions may diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 3f2fc9b12..70b0738c9 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -565,13 +565,16 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): state.modified_event(self, False, previous) if self.trackparent: - if value is not None: - self.sethasparent(instance_state(value), True) if previous is not value and previous is not None: self.sethasparent(instance_state(previous), False) for ext in self.extensions: value = ext.set(state, value, previous, initiator or self) + + if self.trackparent: + if value is not None: + self.sethasparent(instance_state(value), True) + return value @@ -617,11 +620,12 @@ class CollectionAttributeImpl(AttributeImpl): def fire_append_event(self, state, value, initiator): state.modified_event(self, True, NEVER_SET, passive=PASSIVE_NO_INITIALIZE) + for ext in self.extensions: + value = ext.append(state, value, initiator or self) + if self.trackparent and value is not None: self.sethasparent(instance_state(value), True) - for ext in self.extensions: - value = ext.append(state, value, initiator or self) return value def fire_pre_remove_event(self, state, initiator): diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index c83e03599..22806e364 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -359,6 +359,7 @@ class RelationProperty(StrategizedProperty): passive_updates=True, remote_side=None, enable_typechecks=True, join_depth=None, comparator_factory=None, + single_parent=False, strategy_class=None, _local_remote_pairs=None, query_class=None): self.uselist = uselist @@ -370,6 +371,7 @@ class RelationProperty(StrategizedProperty): self.direction = None self.viewonly = viewonly self.lazy = lazy + self.single_parent = single_parent self._foreign_keys = foreign_keys self.collection_class = collection_class self.passive_deletes = passive_deletes @@ -910,9 +912,11 @@ class RelationProperty(StrategizedProperty): "the child's mapped tables. Specify 'foreign_keys' " "argument." % (str(self))) - if self.cascade.delete_orphan and self.direction is MANYTOMANY: + if self.cascade.delete_orphan and not self.single_parent and \ + (self.direction is MANYTOMANY or self.direction is MANYTOONE): util.warn("On %s, delete-orphan cascade is not supported on a " - "many-to-many relation. This will raise an error in 0.6." % self) + "many-to-many or many-to-one relationship when single_parent is not set. " + " Set single_parent=True on the relation()." % self) def _determine_local_remote_pairs(self): if not self.local_remote_pairs: diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 7195310cd..22ef7ded2 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -10,7 +10,7 @@ import sqlalchemy.exceptions as sa_exc from sqlalchemy import sql, util, log from sqlalchemy.sql import util as sql_util from sqlalchemy.sql import visitors, expression, operators -from sqlalchemy.orm import mapper, attributes +from sqlalchemy.orm import mapper, attributes, interfaces from sqlalchemy.orm.interfaces import ( LoaderStrategy, StrategizedOption, MapperOption, PropertyOption, serialize_path, deserialize_path, StrategizedProperty @@ -33,6 +33,10 @@ def _register_attribute(strategy, useobject, prop = strategy.parent_property attribute_ext = util.to_list(prop.extension) or [] + + if useobject and prop.single_parent: + attribute_ext.append(_SingleParentValidator(prop)) + if getattr(prop, 'backref', None): attribute_ext.append(prop.backref.extension) @@ -812,4 +816,24 @@ class LoadEagerFromAliasOption(PropertyOption): else: query._attributes[("user_defined_eager_row_processor", paths[-1])] = None +class _SingleParentValidator(interfaces.AttributeExtension): + def __init__(self, prop): + self.prop = prop + + def _do_check(self, state, value, oldvalue, initiator): + if value is not None: + hasparent = initiator.hasparent(attributes.instance_state(value)) + if hasparent and oldvalue is not value: + raise sa_exc.InvalidRequestError("Instance %s is already associated with an instance " + "of %s via its %s attribute, and is only allowed a single parent." % + (mapperutil.instance_str(value), state.class_, self.prop) + ) + return value + def append(self, state, value, initiator): + return self._do_check(state, value, None, initiator) + + def set(self, state, value, oldvalue, initiator): + return self._do_check(state, value, oldvalue, initiator) + + diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index 5f32884e7..c756045a1 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -65,7 +65,8 @@ class UOWEventHandler(interfaces.AttributeExtension): prop = _state_mapper(state).get_property(self.key) if newvalue is not None and prop.cascade.save_update and newvalue not in sess: sess.add(newvalue) - if prop.cascade.delete_orphan and oldvalue in sess.new: + if prop.cascade.delete_orphan and oldvalue in sess.new and \ + prop.mapper._is_orphan(attributes.instance_state(oldvalue)): sess.expunge(oldvalue) return newvalue |
