SQLAlchemy orm attribute post-update hook?

217 views
Skip to first unread message

Amir Elaguizy

unread,
Nov 27, 2013, 12:48:48 PM11/27/13
to sqlal...@googlegroups.com
If I have a model like:

class Test(Base):
    value = sqlalchemy.Column(db.String)

and I have a function like:

def on_value_change(model, oldValue):
    # Do stuff


I'd like on_value_change called *after* Test.value has been changed

I know it's possible to do it *before* Test.value has changed and choose to accept or reject the change via an orm event.

However, the problem with that is that since the change has not been applied to Test yet I have to pass around the new value in addition to passing around the instance of Test.

Is it possible to do this?

Thanks!
Amir

Michael Bayer

unread,
Nov 27, 2013, 7:26:58 PM11/27/13
to sqlal...@googlegroups.com

On Nov 27, 2013, at 12:48 PM, Amir Elaguizy <aela...@gmail.com> wrote:

> If I have a model like:
>
> class Test(Base):
> value = sqlalchemy.Column(db.String)
>
> and I have a function like:
>
> def on_value_change(model, oldValue):
> # Do stuff
>
>
> I'd like on_value_change called *after* Test.value has been changed

yeah there’s been a bit of discussion about that but it isn’t present in a simple way. attribute mechanics already take up a lot of overhead and add lots of complexity so adding an “after” event isn’t something I’m in a hurry to do.

In the rare occasions that I need this, sometimes what I will do is, just set the value within the before event, then work with it - I haven’t done this much so YMMV:

@event.listens_for(A.data, "set")
def set(target, value, oldvalue, initiator):
target.__dict__['data'] = value
work_with(target)
return value

the reason __dict__ is used is otherwise you trigger an endless loop with the event. The reason doing things in this way is dangerous (and why it’s extra hard to make this work) is that if you pass around “target” to other parts of your app, which are themselves doing things with attributes, now you have a nesting pattern going on that can easily enter more endless recursion types of issues.

usually what I’ll do is just stick to simple things and use a descriptor like a synonym or a hybrid to set the value, which does what it needs after the set event. that’s pretty much the normal Python way of doing this sort of thing in any case. Attribute events are in particular tailored towards validating / processing the immediate value given, not so much calling out into the bigger ecosystem of the application, as it is already occurring within a critical part of the attribute mechanics.


signature.asc

Amir Elaguizy

unread,
Nov 28, 2013, 8:24:09 AM11/28/13
to sqlal...@googlegroups.com
What do you think about the pattern I've implemented for this purpose using metaclasses?


I've also pasted the code here but it's nicer to look at on github via the link above:

import logging
import sqlalchemy as sqla
import sqlalchemy.ext.declarative as decl

from .signals import signaler

log = logging.getLogger(u"cratejoy")


class _hooked(object):
    def __init__(self, validate_func, normalize_func, field_name, private_name):
        self.validate_func = validate_func
        self.normalize_func = normalize_func
        self.field_name = field_name
        self.private_name = private_name

    def __get__(self, instance, owner):
        if not instance:
            return getattr(owner, self.private_name)

        val = getattr(instance, self.private_name)

        return val

    def __set__(self, instance, val):
        namespace = instance.__class__.__name__ + "." + self.field_name

        if self.normalize_func:
            val = self.normalize_func(val)

        if self.validate_func:
            assert self.validate_func(val)

        old_value = None
        if hasattr(instance, self.private_name):
            old_value = getattr(instance, self.private_name)

        signaler.signal(namespace + ":before_update", instance=instance, new_value=val, old_value=old_value)
        setattr(instance, self.private_name, val)
        signaler.signal(namespace + ":after_update", instance=instance, new_value=val, old_value=old_value)


class DispatchingModelMeta(decl.DeclarativeMeta):
    def __new__(cls, name, bases, attrs):
        new_attrs = {}
        for key, val in attrs.iteritems():
            if isinstance(val, sqla.Column):
                log.debug(u"{} Column {} {}".format(name, key, val))
                if not val.name:
                    val.name = key

                val.key = key

                validator_name = 'validate_' + key
                normalize_name = 'normalize_' + key
                private_name = '_' + key

                validator_func = None
                normalize_func = None

                if validator_name in attrs:
                    validator_func = attrs[validator_name]

                if normalize_name in attrs:
                    normalize_func = attrs[normalize_name]

                new_attrs[private_name] = val
                new_attrs[key] = _hooked(validate_func=validator_func, normalize_func=normalize_func, field_name=key, private_name=private_name)
            else:
                new_attrs[key] = val

        return super(DispatchingModelMeta, cls).__new__(cls, name, bases, new_attrs)

Michael Bayer

unread,
Nov 28, 2013, 11:30:46 AM11/28/13
to sqlal...@googlegroups.com
if it works, that’s fine.  as far as metaclasses I like to recommend using SQLAlchemy class instrumentation events instead so that there’s no complication re: declarative’s own metaclass and such.


-- 
You received this message because you are subscribed to the Google Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email tosqlalchemy+...@googlegroups.com.
To post to this group, send email to sqlal...@googlegroups.com.
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/groups/opt_out.

signature.asc
Reply all
Reply to author
Forward
0 new messages