sebastian
sebastian

Reputation: 9696

SQLAlchemy update parent when related child changes

I'm trying to model an entity that as one or more one-to-many relationships, such that it's last_modified attribute is updated, when

I've put together the following minimal example:

class Config(Base):
    __tablename__ = 'config'

    ID = Column('ID', Integer, primary_key=True)
    name = Column('name', String)
    last_modified = Column('last_modified', DateTime, default=now, onupdate=now)

    params = relationship('ConfigParam', backref='config')

class ConfigParam(Base):

    __tablename__ = 'config_params'

    ID = Column('ID', Integer, primary_key=True)
    ConfigID = Column('ConfigID', Integer, ForeignKey('config.ID'), nullable=False)

    key = Column('key', String)
    value = Column('value', Float)

@event.listens_for(Config.params, 'append')
@event.listens_for(Config.params, 'remove')
def receive_append_or_remove(target, value, initiator):
    target.last_modified = now()

@event.listens_for(ConfigParam.key, 'set')
@event.listens_for(ConfigParam.value, 'set')
def receive_attr_change(target, value, oldvalue, initiator):
    if target.config:
        # don't act if the parent config isn't yet set
        # i.e. during __init__
        target.config.last_modified = now()

This seems to work, but I'm wondering if there's a better way to do this?

Specifically, this becomes very verbose since my actual ConfigParam implementation has more attributes and I'm having multiple one-to-many relations configured on the parent Config class.

Upvotes: 5

Views: 3146

Answers (1)

Ilja Everilä
Ilja Everilä

Reputation: 52939

Take this with a huge grain of salt, it "seems" to work, could explode:

def rel_listener(t, v, i):
    t.last_modified = now()

def listener(t, v, o, i):
    if t.config:
        t.config.last_modified = now()

from sqlalchemy import inspect

for rel in inspect(Config).relationships:
    event.listen(rel, 'append', rel_listener)
    event.listen(rel, 'remove', rel_listener)

for col in inspect(ConfigParam).column_attrs:
    event.listen(col, 'set', listener)

Problem is that the inspections make no exceptions and columns such as 'ID' and 'ConfigID' will be bound to event listeners.

Another perhaps slightly less tedious form would be to just use a list of attributes to bind events to in a similar fashion:

for attr in ['key', 'value']:
    event.listen(getattr(ConfigParam, attr), 'set', listener)

This gives you control over what is bound to events and what is not.

Upvotes: 5

Related Questions