summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2009-01-20 21:35:57 +0000
committerMike Bayer <mike_mp@zzzcomputing.com>2009-01-20 21:35:57 +0000
commit7c56371f81707b5979249b2f2b056f65488f1bab (patch)
tree3d2d9cc427e8095e453327c56bca4b1c7fa36d20 /lib
parent9fc05aae02015011052ad0da2bd602951b12c071 (diff)
downloadsqlalchemy-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__.py10
-rw-r--r--lib/sqlalchemy/orm/attributes.py12
-rw-r--r--lib/sqlalchemy/orm/properties.py8
-rw-r--r--lib/sqlalchemy/orm/strategies.py26
-rw-r--r--lib/sqlalchemy/orm/unitofwork.py3
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