События SQLAlchemy after_create для всех моделей

Я работаю над проектом, который требует общей настройки для каждой создаваемой модели. На сегодняшний день я выполнил большую часть работы с помощью наследования моделей. Вот мой блок кода, чтобы дать вам лучшее представление:

app.core.dba.mixins:

class AuditExtension(MapperExtension):
    """
    AuditExtension enforces the audit column values, and ensures any interaction with
    SQLAlchemy cannot override the values
    """

    def before_insert(self, mapper, connection, instance):
        instance.created_dt = datetime.utcnow()
        instance.created_by = audit_session_user()

        instance.updated_dt = datetime.utcnow()
        instance.updated_by = audit_session_user()

    def before_update(self, mapper, connection, instance):
        # Never update the created columns
        instance.created_dt = instance.created_dt
        instance.created_by = instance.created_by

        instance.updated_dt = datetime.utcnow()
        instance.updated_by = audit_session_user()


class AuditColumns(object):

    """ Generate the column schema for simple table level auditing. """
    created_dt = Column(DateTime,
                        default=datetime.utcnow(),
                        nullable=False)
    created_by = Column(String(64),
                        #ForeignKey('operators.username', ondelete="RESTRICT"),
                        nullable=False)

    updated_dt = Column(DateTime,
                        default=datetime.utcnow(),
                        nullable=False,
                        onupdate=datetime.utcnow())

    updated_by = Column(String(64),
                        #ForeignKey('operators.username', ondelete="RESTRICT"),
                        nullable=False)

    __mapper_args__ = {
        'extension': AuditExtension()}

Затем мои модели наследуют AuditColumns:

class ObjectTypes(Base, AuditColumns):
    __tablename__ = 'object_types'

    id = Column(BigInteger, primary_key=True)
    name = Column(String, nullable=False, unique=True)

    def __repr__(self):
        return self.name

Моя проблема в том; мое решение для форсирования данных аудита работает до тех пор, пока операция содержится в приложении фляги и SQLAlchemy - это не мешает любому, у кого есть доступ к базе данных, обновлять значения.

Поэтому теперь мне нужно реализовать триггер для каждой модели, наследующей AuditColumns. Я нашел этот пост Sqlalchemy mixins/и прослушиватель событий - и он описывает метод для before_insert /update (который у меня раньше работал), но не для «after_create».

Теперь я добавил это в свой код файла миксинов (сразу после кода аудита выше:

trig_ddl = DDL("""
            CREATE TRIGGER tr_audit_columns BEFORE INSERT OR UPDATE
            ON test_table
            FOR EACH ROW EXECUTE PROCEDURE
            ss_test();
        """)

event.listen(AuditColumns, 'after_create', trig_ddl)

Однако, когда я запускаю тестовый пример:

Base.metadata.drop_all(db.get_engine(app))
Base.metadata.create_all(db.get_engine(app))

Я получаю следующую ошибку:

File "D:\Devel\flask-projects\sc2\app\core\dba\mixins.py", line 59, in <module>
    event.listen(AuditColumns, 'after_create', trig_ddl)
  File "D:\Devel\flask-projects\env\lib\site-packages\sqlalchemy\event.py", line 43, in listen
    (identifier, target))
sqlalchemy.exc.InvalidRequestError: No such event 'after_create' for target '<class 'app.core.dba.mixins.AuditColumns'>'

Я предполагаю, что это потому, что это еще не стол; но как бы я глобально определил прослушиватель событий для создания таблицы, который будет выполнять этот тип команды?

Я знаю, что мне нужно сделать триг_ддл динамическим (что, я не думаю, будет слишком сложно, но мне, по крайней мере, нужно выяснить глобальный элемент этого).

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

Любой толчок в правильном направлении был бы замечательным.


person Trent    schedule 09.07.2013    source источник
comment
было бы совершенно невозможно указать, что все изменения должны проходить через orm? попытка сохранить бизнес-логику как в приложении , так и в базе данных кажется рецептом будущих головных болей. Кажется вполне разумным решить использовать orm, сохранить логику на уровне приложения и запретить (или быть очень осторожным) прямой доступ к БД через элементы управления процессом/доступом.   -  person second    schedule 10.07.2013
comment
В настоящее время мы часто используем прямой доступ к БД для миграции, массовых исправлений данных и т. д. Но вполне возможно, что все эти вещи могут быть принудительно реализованы с помощью юнит-тестов, написанных на py/sqlalchemy в будущем...   -  person Trent    schedule 10.07.2013


Ответы (1)


Ну, вам нужно объединить события здесь, чтобы вы могли получить это Table:

@event.listens_for(AuditColumns, "instrument_class", propagate=True)
def instrument_class(mapper, class_):
    if mapper.local_table is not None:
        trigger_for_table(mapper.local_table)

def trigger_for_table(table):
    trig_ddl = DDL("""
                CREATE TRIGGER tr_%s_audit_columns BEFORE INSERT OR UPDATE
                ON %s
                FOR EACH ROW EXECUTE PROCEDURE
                ss_test();
            """ % (table.name, table.name))

    event.listen(table, 'after_create', trig_ddl)

любой подкласс AuditColumms отображается, mapper.local_table уже будет там (как и class.__table__, то же самое), вы применяете событие DDL в этой точке.

person zzzeek    schedule 11.07.2013
comment
ОК, я обратился к документам и из того, что я могу понять, это работает, сначала прослушивая любую новую модель, которая пытается наследовать AuditColumns - затем она использует преобразователь для определения таблицы, наследующей расширение, а затем вызывает функцию trigger_for_table просто сгенерировать DDL и сгенерировать прослушиватель для ЭТОЙ конкретной модели? Было бы очень здорово, если бы у вас была какая-либо дополнительная информация об инструментальном_классе, поскольку документация немного расплывчата (или, может быть, слишком специфична, чтобы я ее не совсем понял)... Кстати, это работает; но я действительно понимаю, ПОЧЕМУ это работает. - person Trent; 11.07.2013
comment
Instrument_class — это событие, сделайте это, как только класс, который является подклассом AuditColumns, будет сопоставлен с таблицей. это мало чем отличается от того, если вы создали метакласс, который что-то делал на __init__(), например. при создании нового класса. - person zzzeek; 11.07.2013