Есть ли способ прозрачно выполнить проверку на объектах SQLAlchemy?

Есть ли способ выполнить проверку на объект после (или как) свойств, установленных перед сеансом?

Например, у меня есть модель домена Device, которая имеет свойство mac. Я хотел бы убедиться, что свойство mac содержит действительное и дезинфицированное значение mac перед его добавлением или обновлением в базе данных.

Похоже, что подход Pythonic должен делать большинство вещей как свойства (включая SQLAlchemy). Если бы я закодировал это на PHP или Java, я бы, вероятно, решил создать методы getter/setter для защиты данных и дать мне возможность справиться с этим в самой модели домена.

public function mac() { return $this->mac; }
public function setMac($mac) {
    return $this->mac = $this->sanitizeAndValidateMac($mac);
}
public function sanitizeAndValidateMac($mac) {
    if ( ! preg_match(self::$VALID_MAC_REGEX) ) {
        throw new InvalidMacException($mac);
    }
    return strtolower($mac);
}

Что такое Pythonic способ справиться с этим типом ситуации с помощью SQLAlchemy?

(Хотя я знаю, что валидация и должна быть обработана в другом месте (т.е. веб-фреймворк), я хотел бы выяснить, как обрабатывать некоторые из этих правил проверки подлинности в домене, поскольку они неизбежно возникают часто.)

UPDATE

Я знаю, что я мог бы использовать property, чтобы сделать это при обычных обстоятельствах. Ключевой частью является то, что я использую SQLAlchemy с этими классами. Я не понимаю, как именно SQLAlchemy выполняет свою магию, но я подозреваю, что создание и переопределение этих свойств может привести к нестабильным и/или непредсказуемым результатам.

Ответы

Ответ 1

Вы можете добавить проверку данных внутри своих классов SQLAlchemy с помощью декоратора @validates().

Из документов - Простые валидаторы:

Валидатор атрибутов может вызывать исключение, останавливать процесс изменения значения атрибутов или изменять данное значение во что-то другое.

from sqlalchemy.orm import validates

class EmailAddress(Base):
    __tablename__ = 'address'

    id = Column(Integer, primary_key=True)
    email = Column(String)

    @validates('email')
    def validate_email(self, key, address):
        # you can use assertions, such as
        # assert '@' in address
        # or raise an exception:
        if '@' not in address:
            raise ValueError('Email address must contain an @ sign.')
        return address

Ответ 2

Да. Это можно сделать красиво, используя MapperExtension.

# uses sqlalchemy hooks to data model class specific validators before update and insert
class ValidationExtension( sqlalchemy.orm.interfaces.MapperExtension ):
    def before_update(self, mapper, connection, instance):
        """not every instance here is actually updated to the db, see http://www.sqlalchemy.org/docs/reference/orm/interfaces.html?highlight=mapperextension#sqlalchemy.orm.interfaces.MapperExtension.before_update"""
        instance.validate()
        return sqlalchemy.orm.interfaces.MapperExtension.before_update(self, mapper, connection, instance)
    def before_insert(self, mapper, connection, instance):
        instance.validate()
        return sqlalchemy.orm.interfaces.MapperExtension.before_insert(self, mapper, connection, instance)


sqlalchemy.orm.mapper( model, table, extension = ValidationExtension(), **mapper_args )

Вы можете проверить before_update, потому что не каждый экземпляр здесь фактически обновляется до db.

Ответ 3

"Похоже, что подход Pythonic - это делать большинство вещей как свойства"

Он меняется, но это близко.

"Если бы я закодировал это на PHP или Java, я бы, вероятно, решил создать методы getter/setter..."

Хорошо. Это достаточно Pythonic. Ваши функции getter и setter связаны с свойством; это очень хорошо.

Какой вопрос?

Вы спрашиваете, как записать property?

Тем не менее, "прозрачная проверка" - если я правильно прочитал ваш пример кода - может быть, не совсем такая хорошая идея.

Ваша модель и ваша проверка, вероятно, должны храниться отдельно. Общепринято иметь несколько валидаций для одной модели. Для некоторых пользователей поля являются необязательными, фиксированными или не используются; это приводит к нескольким проверкам.

Вы будете счастливее, следуя шаблону проектирования Django, используя Form для проверки, отдельно отформатируйте модель.