[Solved] onupdate based on another field with sqlalchemy declarative base

I use sqlalchemy with the pyramid framework, and i want to link a person to his geographical department using his postcode.
So i try to use the onupdate argument when defining the department_id column define the department_id.
see fallowing code:

from datetime import date
from emailing.models import Base, DBSession
from sqlalchemy import Column, Integer, Unicode, Text, DateTime, Sequence, Boolean, Date, UnicodeText, UniqueConstraint, Table, ForeignKey
from sqlalchemy.orm import scoped_session, sessionmaker, column_property, relationship, backref
from sqlalchemy.sql import func

class Person(Base):
    __tablename__ = u'person'
    id = Column(Integer, primary_key=True)

    firstName = Column(Unicode(255))
    lastName = Column(Unicode(255))

    created_at = Column(Date, default=func.now())
    updated_at = Column(Date, onupdate=func.now())

    department_id = Column(Integer(), ForeignKey('department.id'), onupdate=dep_id_from_postcode)
    department = relationship("Department", backref='persons')

    __table_args__ = (UniqueConstraint('firstName', 'lastName'), {})

    def dep_id_from_postcode(self):
        return int(self.postcode[:2]) 

on update for the updated_at field works fine, but for the deparment_id field it tell my:

NameError: name ‘dep_id_from_postcode’ is not defined

i’ve found documentation about python executed function here: http://docs.sqlalchemy.org/en/latest/core/schema.html?highlight=trigger#python-executed-functions
but nothing that uses another field to use in onupdate argument.

i hope i’m clear enought as i’m not a “natural english speaker”
Thank you all

Enquirer: Yohann


Solution #1:

SQLAlchemy has special mechanism for using other column value in default (i.e. onupdate) function called: Context-Sensitive Default Functions

The typical use case for this context with regards to default
generation is to have access to the other values being inserted or
updated on the row.

As Van pointed out you need to make sure that postcode is a field defined for Person or you’ll need to add functionality that will take care about getting postcode associated with Person instance.

What worked for me – regular function, not bound to any object.
SQLAlchemy will call it at the time of insert and/or update and pass special argument with “context” – which is not actual object you are updating.

So for your example I would do something like this.

def dep_id_from_postcode(context):
    postcode = context.current_parameters['postcode']
    return int(postcode[:2])

class Person(Base):
    postcode = Column(String)
    # ...

    # ...
    department_id = Column(Integer(), ForeignKey('department.id'), onupdate=dep_id_from_postcode)
    # ...

Be careful with this context argument – I end up with problem when context had None field’s value in some cases if I not updating ‘postcode’ value with the same operation.

Eclipse with pydev with debugger helped me to see what information is passed as context.

Respondent: vvladymyrov

Solution #2:

Move the function definition before its usage:

class Person(Base):
    # ...
    def dep_id_from_postcode(self):
        return int(self.postcode[:2])
    # ...
    department_id = Column(Integer(), ForeignKey('department.id'), onupdate=dep_id_from_postcode)
    # ...

Is the postcode really a field directly in Person? Because if it is not, you might need to handle this completely differently. For example, if the postcode is derived from the primary_address relationship, you need to check add/remove of the primary_address relationships and the changes in the related Address object for proper hooking.

Respondent: van

Solution #3:

Be careful with this context argument – I end up with a problem when context had None field’s value in some cases if I not updating ‘postcode’ value with the same operation.

As @vvladymyrov mentioned that he ended up with the problem that context will produce None if in case you are using field values which you are not updating. But still, you need to calculate another one.

For Example:

You have firstName and lastName that will be taken as input from a user. You also have fullName that you calculate based on firstName and lastName. The following is the code which you follow:

#The below code is the ERModel class in sqlalchemy
def calculateFullName(context):
    name = context.current_parameters['firstName'] + " " + context.current_parameters['lastName']
    return name

class User(Base):
    firstName= Column(String)
    lastName= Column(String)
    name= Column(String,default=calculateFullName,onupdate=calculateFullName)
    # ...
    # ...
# End of ER Model

Now let’s consider a case where you want to update a user’s lastName and internally the name also should get updated by using the calculateFullName function you can achieve it but there is a caveat if you try to do the following:

user = session.query(User).filter(User.id=<id>).one() # Here you will get the specific user based on id.
user.lastName = "XXX" #This is the new value you want to be updated for the existing user.

As mentioned, the above will call calculateFullName and you will get the context but in that context.current_parameters, the firstName will be None. (As you are not updating firstName you will get that value as None, if you want you can print(context.dict) to check what you are getting).

So the solution that I found for these kinds of cases (Update a column which depends on 2 columns on which the only one column is getting updated) is using session query update() function as following:

query = session.query(User).filter(User.id=<id>)  # This will return query object
query.update({"lastName"="XXX", "firstName"=query.one().__dict__.get("firstName")},syncronize_session=False)

As you are updating lastName and you need firstName to calculate the name. So you need to send firstName to context. As you already queried and get the current record from database to memory you can use that and send to query.update() such that you will get that in the context object.

Note: The approach that I took may not be the efficient one. Please guide me if I did any mistake. I am happy to learn.


The answers/resolutions are collected from stackoverflow, are licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0 .

Leave a Reply

Your email address will not be published.