Automatically update an abnormal attribute using AttributeExtension

I'm having issues with AttributeExtension SQLAlchemy.

In fact, I store the de-normalized sum attribute in the Partent table, because I need it quite often for sorting. However, I would like the attribute to be updated whenever the value of one of its children changes.

Unfortunately, the set () method of the AttributeExtension attribute is never called, so changes are not recognized. Using a property adjuster that also updates the parent may work, but I would like to know how to use the AttributeExtension SQLAlchemy property (version: 0.6beta2) correctly.

Here is a small (executable) piece of code that 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

!
Christoph

+3
1

, child, child.value. - :

class ValueAttributeExtension(AttributeExtension):
  ...

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

EDIT-1: :

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
+3

Source: https://habr.com/ru/post/1733522/


All Articles