On Jan 4, 2009, at 7:04 PM, Arn Vollebregt wrote:
>
> Hi,
>
> It is my understanding that when one adds an object instance to a
> session, SQLAlchemy checks for the same object based on it's primary
> key.
I wouldn't say that's accurate - it only looks at the existing
"identity" assigned to the given instance, based on what already-
loaded database state is present. If you put two brand new Foo()
objects in the session both with "id=1", SQLA wouldn't complain at
all. It lets the database do its job of detecting that constraint,
and the error is an IntegrityError raised by the DBAPI within the flush.
> Does
> this mean SQLAlchemy has no knowledge of the unique constraints in the
> table definition?
that is correct. The "unique" flag is only used for DDL generation,
or if the application wants to inspect the Table itself to make
decisions.
> And if so, is there a way to change this behavior,
> without querying the database?
SQLA core never chooses to reinvent functionality that's the database
already provides, so you'd have to build something on your end which
hashes all instances in a dictionary based on the desired unique
attributes. I've hopefully made this easy via the attached example
(SQLA 0.5, py2.5). If it works for you we should add it to the wiki's
UsageRecipes section.
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/sqlalchemy?hl=en
-~----------~----~----~----~------~----~------~--~---
from sqlalchemy.orm import object_session, column_property
from sqlalchemy.orm.interfaces import SessionExtension, AttributeExtension
import weakref
def _unique_dict(session):
"""Returns a weak valued dictionary associated with a Session which tracks constrained values.
Keys are tuples of the form:
(cls, (constraint_key1, constraint_key2, ...), (value1, value2, ...))
Values are instances of cls which are the current holders of the constraint
value indicated by the key.
"""
if not hasattr(session, '_unique_dict'):
session._unique_dict = weakref.WeakValueDictionary()
return session._unique_dict
class UniqueConstraintChecker(SessionExtension):
"""Listens for attach events and checks against a constraint dictionary for violations.
The dictionary is of the form {cls:(attr1, attr2, ...), cls:(attr1, attr2, ...), ...} indicating
classes and a tuple of attribute names which comprise a unique constraint.
"""
def __init__(self, constraints):
self._constraints = constraints
def after_attach(self, session, instance):
cls = instance.__class__
if cls in self._constraints:
unique_dict = _unique_dict(session)
constraint = self._constraints[cls]
composed_value = tuple([getattr(instance, k, None) for k in constraint])
if (cls, constraint, composed_value) in unique_dict:
raise Exception("Unique key %r already present" % (composed_value, ))
unique_dict[(cls, constraint, composed_value)] = instance
class UniqueConstraintListener(AttributeExtension):
"""Listens for set events on a single attribute and checks for constraint violations
against a composed constraint definition.
"""
def __init__(self, constraint, attrname):
self.constraint = constraint
self.attrname = attrname
def set(self, state, value, oldvalue, initiator):
instance = state.obj()
session = object_session(instance)
if session:
cls = instance.__class__
unique_dict = _unique_dict(session)
old_composed_value = tuple([oldvalue if k == self.attrname else getattr(instance, k, None) for k in self.constraint])
new_composed_value = tuple([value if k == self.attrname else getattr(instance, k, None) for k in self.constraint])
if (cls, self.constraint, new_composed_value) in unique_dict:
raise Exception("Unique key %r already present" % (new_composed_value, ))
else:
del unique_dict[(cls, self.constraint, old_composed_value)]
unique_dict[(cls, self.constraint, new_composed_value)] = instance
return value
def UniqueColumn(*args, **kw):
"""Produce a declarative column_property() with a unique constraint checker."""
constraint = kw.pop('constraint')
attrname = kw.pop('attrname')
return column_property(
Column(*args, **kw),
extension=UniqueConstraintListener(constraint, attrname)
)
if __name__ == '__main__':
from sqlalchemy import Column, Integer, create_engine, UniqueConstraint
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite://', echo=True)
Base = declarative_base()
user_constraint = ('foo', 'bar')
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
foo = UniqueColumn(Integer, nullable=False, attrname='foo', constraint=user_constraint)
bar = UniqueColumn(Integer, nullable=False, attrname='bar', constraint=user_constraint)
__table_args__ = (UniqueConstraint(*user_constraint),{})
Base.metadata.create_all(engine)
sess = sessionmaker(
bind=engine,
extension=UniqueConstraintChecker({User:user_constraint})
)()
u1 = User(foo=5, bar=12)
u2 = User(foo=6, bar=12)
u3 = User(foo=6, bar=9)
sess.add_all([u1, u2, u3])
try:
sess.add(User(foo=6, bar=12)) # can't add a new object with a duplicate
except Exception, e:
print e
else:
assert False
try:
u2.foo = 5 # can't change an object already in the session to a duplicate key
except Exception, e:
print e
else:
assert False
# change the original to something else, now you can change u2.foo to 5
u1.bar = 19
u2.foo = 5
sess.commit()