Changing Attribute Behavior

Simple Validators

A quick way to add a “validation” routine to an attribute is to use thevalidates() decorator. An attribute validator can raisean exception, halting the process of mutating the attribute’s value, or canchange the given value into something different. Validators, like allattribute extensions, are only called by normal userland code; they are notissued when the ORM is populating the object:

  1. from sqlalchemy.orm import validates
  2.  
  3. class EmailAddress(Base):
  4. __tablename__ = 'address'
  5.  
  6. id = Column(Integer, primary_key=True)
  7. email = Column(String)
  8.  
  9. @validates('email')
  10. def validate_email(self, key, address):
  11. assert '@' in address
  12. return address

Changed in version 1.0.0: - validators are no longer triggered withinthe flush process when the newly fetched values for primary keycolumns as well as some python- or server-side defaults are fetched.Prior to 1.0, validators may be triggered in those cases as well.

Validators also receive collection append events, when items are added to acollection:

  1. from sqlalchemy.orm import validates
  2.  
  3. class User(Base):
  4. # ...
  5.  
  6. addresses = relationship("Address")
  7.  
  8. @validates('addresses')
  9. def validate_address(self, key, address):
  10. assert '@' in address.email
  11. return address

The validation function by default does not get emitted for collectionremove events, as the typical expectation is that a value being discardeddoesn’t require validation. However, validates() supports receptionof these events by specifying include_removes=True to the decorator. Whenthis flag is set, the validation function must receive an additional booleanargument which if True indicates that the operation is a removal:

  1. from sqlalchemy.orm import validates
  2.  
  3. class User(Base):
  4. # ...
  5.  
  6. addresses = relationship("Address")
  7.  
  8. @validates('addresses', include_removes=True)
  9. def validate_address(self, key, address, is_remove):
  10. if is_remove:
  11. raise ValueError(
  12. "not allowed to remove items from the collection")
  13. else:
  14. assert '@' in address.email
  15. return address

The case where mutually dependent validators are linked via a backrefcan also be tailored, using the include_backrefs=False option; this option,when set to False, prevents a validation function from emitting if theevent occurs as a result of a backref:

  1. from sqlalchemy.orm import validates
  2.  
  3. class User(Base):
  4. # ...
  5.  
  6. addresses = relationship("Address", backref='user')
  7.  
  8. @validates('addresses', include_backrefs=False)
  9. def validate_address(self, key, address):
  10. assert '@' in address.email
  11. return address

Above, if we were to assign to Address.user as in someaddress.user = some_user,the validate_address() function would _not be emitted, even though an appendoccurs to some_user.addresses - the event is caused by a backref.

Note that the validates() decorator is a convenience function built ontop of attribute events. An application that requires more control overconfiguration of attribute change behavior can make use of this system,described at AttributeEvents.

  • sqlalchemy.orm.validates(*names, **kw)
  • Decorate a method as a ‘validator’ for one or more named properties.

Designates a method as a validator, a method which receives thename of the attribute as well as a value to be assigned, or in thecase of a collection, the value to be added to the collection.The function can then raise validation exceptions to halt theprocess from continuing (where Python’s built-in ValueErrorand AssertionError exceptions are reasonable choices), or canmodify or replace the value before proceeding. The function shouldotherwise return the given value.

Note that a validator for a collection cannot issue a load of thatcollection within the validation routine - this usage raisesan assertion to avoid recursion overflows. This is a reentrantcondition which is not supported.

  • Parameters
    • *names – list of attribute names to be validated.

    • include_removes – if True, “remove” events will besent as well - the validation function must accept an additionalargument “is_remove” which will be a boolean.

    • include_backrefs

defaults to True; if False, thevalidation function will not emit if the originator is an attributeevent related via a backref. This can be used for bi-directionalvalidates() usage where only one validator should emit perattribute operation.

New in version 0.9.0.

See also

Simple Validators - usage examples for validates()

Using Custom Datatypes at the Core Level

A non-ORM means of affecting the value of a column in a way that suitsconverting data between how it is represented in Python, vs. how it isrepresented in the database, can be achieved by using a custom datatype that isapplied to the mapped Table metadata. This is more common in thecase of some style of encoding / decoding that occurs both as data goes to thedatabase and as it is returned; read more about this in the Core documentationat Augmenting Existing Types.

Using Descriptors and Hybrids

A more comprehensive way to produce modified behavior for an attribute is touse descriptors. These are commonly used in Python using the property()function. The standard SQLAlchemy technique for descriptors is to create aplain descriptor, and to have it read/write from a mapped attribute with adifferent name. Below we illustrate this using Python 2.6-style properties:

  1. class EmailAddress(Base):
  2. __tablename__ = 'email_address'
  3.  
  4. id = Column(Integer, primary_key=True)
  5.  
  6. # name the attribute with an underscore,
  7. # different from the column name
  8. _email = Column("email", String)
  9.  
  10. # then create an ".email" attribute
  11. # to get/set "._email"
  12. @property
  13. def email(self):
  14. return self._email
  15.  
  16. @email.setter
  17. def email(self, email):
  18. self._email = email

The approach above will work, but there’s more we can add. While ourEmailAddress object will shuttle the value through the emaildescriptor and into the _email mapped attribute, the class levelEmailAddress.email attribute does not have the usual expression semanticsusable with Query. To provide these, we instead use thehybrid extension as follows:

  1. from sqlalchemy.ext.hybrid import hybrid_property
  2.  
  3. class EmailAddress(Base):
  4. __tablename__ = 'email_address'
  5.  
  6. id = Column(Integer, primary_key=True)
  7.  
  8. _email = Column("email", String)
  9.  
  10. @hybrid_property
  11. def email(self):
  12. return self._email
  13.  
  14. @email.setter
  15. def email(self, email):
  16. self._email = email

The .email attribute, in addition to providing getter/setter behavior when we have aninstance of EmailAddress, also provides a SQL expression when used at the class level,that is, from the EmailAddress class directly:

  1. from sqlalchemy.orm import Session
  2. session = Session()
  3.  
  4. sqladdress = session.query(EmailAddress).\
  5. filter(EmailAddress.email == 'address@example.com').\
  6. one()
  7. SELECT address.email AS address_email, address.id AS address_id
  8. FROM address
  9. WHERE address.email = ?
  10. ('address@example.com',)
  11.  
  12. address.email = 'otheraddress@example.com'
  13. sqlsession.commit()
  14. UPDATE address SET email=? WHERE address.id = ?
  15. ('otheraddress@example.com', 1)
  16. COMMIT

The hybrid_property also allows us to change the behavior of theattribute, including defining separate behaviors when the attribute isaccessed at the instance level versus at the class/expression level, using thehybrid_property.expression() modifier. Such as, if we wanted to add ahost name automatically, we might define two sets of string manipulationlogic:

  1. class EmailAddress(Base):
  2. __tablename__ = 'email_address'
  3.  
  4. id = Column(Integer, primary_key=True)
  5.  
  6. _email = Column("email", String)
  7.  
  8. @hybrid_property
  9. def email(self):
  10. """Return the value of _email up until the last twelve
  11. characters."""
  12.  
  13. return self._email[:-12]
  14.  
  15. @email.setter
  16. def email(self, email):
  17. """Set the value of _email, tacking on the twelve character
  18. value @example.com."""
  19.  
  20. self._email = email + "@example.com"
  21.  
  22. @email.expression
  23. def email(cls):
  24. """Produce a SQL expression that represents the value
  25. of the _email column, minus the last twelve characters."""
  26.  
  27. return func.substr(cls._email, 0, func.length(cls._email) - 12)

Above, accessing the email property of an instance of EmailAddresswill return the value of the _email attribute, removing or adding thehostname @example.com from the value. When we query against the emailattribute, a SQL function is rendered which produces the same effect:

  1. sqladdress = session.query(EmailAddress).filter(EmailAddress.email == 'address').one()
  2. SELECT address.email AS address_email, address.id AS address_id
  3. FROM address
  4. WHERE substr(address.email, ?, length(address.email) - ?) = ?
  5. (0, 12, 'address')

Read more about Hybrids at Hybrid Attributes.

Synonyms

Synonyms are a mapper-level construct that allow any attribute on a classto “mirror” another attribute that is mapped.

In the most basic sense, the synonym is an easy way to make a certainattribute available by an additional name:

  1. class MyClass(Base):
  2. __tablename__ = 'my_table'
  3.  
  4. id = Column(Integer, primary_key=True)
  5. job_status = Column(String(50))
  6.  
  7. status = synonym("job_status")

The above class MyClass has two attributes, .job_status and.status that will behave as one attribute, both at the expressionlevel:

  1. >>> print(MyClass.job_status == 'some_status')
  2. my_table.job_status = :job_status_1
  3.  
  4. >>> print(MyClass.status == 'some_status')
  5. my_table.job_status = :job_status_1

and at the instance level:

  1. >>> m1 = MyClass(status='x')
  2. >>> m1.status, m1.job_status
  3. ('x', 'x')
  4.  
  5. >>> m1.job_status = 'y'
  6. >>> m1.status, m1.job_status
  7. ('y', 'y')

The synonym() can be used for any kind of mapped attribute thatsubclasses MapperProperty, including mapped columns and relationships,as well as synonyms themselves.

Beyond a simple mirror, synonym() can also be made to referencea user-defined descriptor. We can supply ourstatus synonym with a @property:

  1. class MyClass(Base):
  2. __tablename__ = 'my_table'
  3.  
  4. id = Column(Integer, primary_key=True)
  5. status = Column(String(50))
  6.  
  7. @property
  8. def job_status(self):
  9. return "Status: " + self.status
  10.  
  11. job_status = synonym("status", descriptor=job_status)

When using Declarative, the above pattern can be expressed more succinctlyusing the synonym_for() decorator:

  1. from sqlalchemy.ext.declarative import synonym_for
  2.  
  3. class MyClass(Base):
  4. __tablename__ = 'my_table'
  5.  
  6. id = Column(Integer, primary_key=True)
  7. status = Column(String(50))
  8.  
  9. @synonym_for("status")
  10. @property
  11. def job_status(self):
  12. return "Status: " + self.status

While the synonym() is useful for simple mirroring, the use caseof augmenting attribute behavior with descriptors is better handled in modernusage using the hybrid attribute feature, whichis more oriented towards Python descriptors. Technically, a synonym()can do everything that a hybrid_property can do, as it also supportsinjection of custom SQL capabilities, but the hybrid is more straightforwardto use in more complex situations.

  • sqlalchemy.orm.synonym(name, map_column=None, descriptor=None, comparator_factory=None, doc=None, info=None)
  • Denote an attribute name as a synonym to a mapped property,in that the attribute will mirror the value and expression behaviorof another attribute.

e.g.:

  1. class MyClass(Base):
  2. __tablename__ = 'my_table'
  3.  
  4. id = Column(Integer, primary_key=True)
  5. job_status = Column(String(50))
  6.  
  7. status = synonym("job_status")
  • Parameters
    • name – the name of the existing mapped property. Thiscan refer to the string name ORM-mapped attributeconfigured on the class, including column-bound attributesand relationships.

    • descriptor – a Python descriptor that will be usedas a getter (and potentially a setter) when this attribute isaccessed at the instance level.

    • map_column

For classical mappings and mappings againstan existing Table object only. if True, the synonym()construct will locate the Column object upon the mappedtable that would normally be associated with the attribute name ofthis synonym, and produce a new ColumnProperty that insteadmaps this Column to the alternate name given as the “name”argument of the synonym; in this way, the usual step of redefiningthe mapping of the Column to be under a different name isunnecessary. This is usually intended to be used when aColumn is to be replaced with an attribute that also uses adescriptor, that is, in conjunction with thesynonym.descriptor parameter:

  1. my_table = Table(
  2. "my_table", metadata,
  3. Column('id', Integer, primary_key=True),
  4. Column('job_status', String(50))
  5. )
  6.  
  7. class MyClass(object):
  8. @property
  9. def _job_status_descriptor(self):
  10. return "Status: %s" % self._job_status
  11.  
  12.  
  13. mapper(
  14. MyClass, my_table, properties={
  15. "job_status": synonym(
  16. "_job_status", map_column=True,
  17. descriptor=MyClass._job_status_descriptor)
  18. }
  19. )

Above, the attribute named _job_status is automaticallymapped to the job_status column:

  1. >>> j1 = MyClass()
  2. >>> j1._job_status = "employed"
  3. >>> j1.job_status
  4. Status: employed

When using Declarative, in order to provide a descriptor inconjunction with a synonym, use thesqlalchemy.ext.declarative.synonym_for() helper. However,note that the hybrid properties featureshould usually be preferred, particularly when redefining attributebehavior.

  1. -

info

Optional data dictionary which will be populated into theInspectionAttr.info attribute of this object.

New in version 1.0.0.

  1. -

comparator_factory

A subclass of PropComparatorthat will provide custom comparison behavior at the SQL expressionlevel.

Note

For the use case of providing an attribute which redefines bothPython-level and SQL-expression level behavior of an attribute,please refer to the Hybrid attribute introduced atUsing Descriptors and Hybrids for a more effective technique.

See also

Synonyms - Overview of synonyms

synonym_for() - a helper oriented towards Declarative

Using Descriptors and Hybrids - The Hybrid Attribute extension provides anupdated approach to augmenting attribute behavior more flexiblythan can be achieved with synonyms.

Operator Customization

The “operators” used by the SQLAlchemy ORM and Core expression languageare fully customizable. For example, the comparison expressionUser.name == 'ed' makes usage of an operator built into Pythonitself called operator.eq - the actual SQL construct which SQLAlchemyassociates with such an operator can be modified. Newoperations can be associated with column expressions as well. The operatorswhich take place for column expressions are most directly redefined at thetype level - see thesection Redefining and Creating New Operators for a description.

ORM level functions like column_property(), relationship(),and composite() also provide for operator redefinition at the ORMlevel, by passing a PropComparator subclass to the comparator_factoryargument of each function. Customization of operators at this level is arare use case. See the documentation at PropComparatorfor an overview.