views:

137

answers:

1

I am having some troubles with the AttributeExtension of SQLAlchemy.

Actually I am storing a de-normalized sum attribute in the Partent table, because I need it quite often for sorting purposes. However, I would like the attribute to get updated whenever the value of one of it's children is changed.

Unfortunately, the set() method of the AttributeExtension is never called and so, changes aren't recognized. Using a property-setter which updates also the parent might work, but I would like to know how to use the AttributeExtension of SQLAlchemy (version: 0.6beta2) correctly.

Here is a small (runnable) code snippet which demonstrates the problem:

from sqlalchemy import create_engine, Column, Integer, ForeignKey
from sqlalchemy.orm import relation, scoped_session, sessionmaker, \
         AttributeExtension
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine('sqlite:///:memory:', echo=True)
session = scoped_session(sessionmaker(bind=engine, autoflush=True))
Base = declarative_base()
Base.query = session.query_property()

class ChildrenAttributeExtension(AttributeExtension):
    active_history = True

    def append(self, state, child, initiator):
        parent = state.obj()
        parent.sum_of_children += child.value
        return child

    def remove(self, state, child, initiator):
        parent = state.obj()
        parent.sum_of_children -= child.value

    def set(self, state, child, oldchild, initiator):
        print 'set called' # gets never printed
        parent = state.obj()
        parent.sum_of_children += -oldchild.value + child.value
        return child


class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'), nullable=False)
    value = Column(Integer, nullable=False, default=0)


class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    sum_of_children = Column(Integer, nullable=False, default=0)

    children = relation('Child', backref='parent',
            extension=ChildrenAttributeExtension())

Base.metadata.create_all(engine)

# Add a parent
p = Parent()
session.add(p)
session.commit()

p = Parent.query.first()
assert p.sum_of_children == 0


# Add a child
c = Child(parent=p, value=5)
session.add(c)
session.commit()

p = Parent.query.first()
assert p.sum_of_children == 5

# Change a child
c = Child.query.first()
c.value = 3
session.commit()  # extension.set() doesn't get called

p = Parent.query.first()
assert p.sum_of_children == 3 # Assertion fails

Thanks for your help!
Christoph

+2  A: 

As far as I can see, you are looking for events on child, but change child.value. Something like this should do the trick:

class ValueAttributeExtension(AttributeExtension):
  ...

class Child(Base):
  ...
  value = ColumnProperty(Column(Integer, nullable=False, default=0), 
                         extension=ValueAttributeExtension()) 

EDIT-1: full working example below:

from sqlalchemy import create_engine, Column, Integer, ForeignKey
from sqlalchemy.orm import relation, scoped_session, sessionmaker, AttributeExtension, ColumnProperty
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine('sqlite:///:memory:', echo=False)
session = scoped_session(sessionmaker(bind=engine, autoflush=True))
Base = declarative_base()
Base.query = session.query_property()

class ValueAttributeExtension(AttributeExtension):
    active_history = True

    def append(self, state, child, initiator):
        assert False, "should not be called"

    def remove(self, state, child, initiator):
        assert False, "should not be called"

    def set(self, state, value, oldvalue, initiator):
        print 'set called', state.obj(), value, oldvalue
        child = state.obj()
        if not(child.parent is None):
            child.parent.sum_of_children += -oldvalue + value
        return value

class ChildrenAttributeExtension(AttributeExtension):
    active_history = True

    def append(self, state, child, initiator):
        print 'append called', state.obj(), child
        parent = state.obj()
        parent.sum_of_children += child.value
        return child

    def remove(self, state, child, initiator):
        print 'remove called', state.obj(), child
        parent = state.obj()
        parent.sum_of_children -= child.value

    def set(self, state, child, oldchild, initiator):
        print 'set called', state, child, oldchild
        parent = state.obj()
        parent.parent.sum_of_children += -oldchild.value + child.value
        #parent.sum_of_children += -oldchild.value + child.value
        return child

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'), nullable=False)
    value = ColumnProperty(Column(Integer, nullable=False, default=0),
                    extension=ValueAttributeExtension())

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    sum_of_children = Column(Integer, nullable=False, default=0)

    children = relation('Child', backref='parent',
                        extension=ChildrenAttributeExtension())

Base.metadata.create_all(engine)

# Add a parent
p = Parent()
session.add(p)
session.commit()

p = Parent.query.first()
assert p.sum_of_children == 0


# Add a child
c = Child(parent=p, value=5)
session.add(c)
session.commit()

p = Parent.query.first()
assert p.sum_of_children == 5

# Change a child
#c = Child.query.first()
c.value = 3 # fixed bug: = instead of ==
session.commit()  # extension.set() doesn't get called

p = Parent.query.first()
assert p.sum_of_children == 3 # Assertion is OK
stephan
agree. also you might want to watch for the (child.)parent (from backref of the relation, so that when it changes, you add/subtract the value as well.
van
@van: thanks for the working example
stephan
Thank you stephan and many thanks for the example code van! Works perfectly :)
tux21b