Ведение журнала аудита SQLAlchemy; как обрабатывать удаляет?

m использует модифицированную версию примера кода управления версиями, поставляемого с SQLAlchemy, для записи идентификатора пользователя и даты внесения изменений. Тем не менее, я также хочу изменить его, поэтому удаление выполняется путем пометкиis_deleted флаг типа вместо запуска реального SQLDELETE, Моя проблема в том, что яЯ не уверен, как захватить удаление и заменить его обновлением.

Вот'Что я имею до сих пор:

''' http://docs.sqlalchemy.org/en/rel_0_8/orm/examples.html?highlight=versioning#versioned-objects '''
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import mapper, class_mapper, attributes, object_mapper, scoping
from sqlalchemy.orm.session import Session
from sqlalchemy.orm.exc import UnmappedClassError, UnmappedColumnError
from sqlalchemy import Table, Column, ForeignKeyConstraint, DateTime, String, Boolean
from sqlalchemy import event
from sqlalchemy.orm.properties import RelationshipProperty
from datetime import datetime
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql.expression import false

def col_references_table(col, table):
    for fk in col.foreign_keys:
        if fk.references(table):
            return True
    return False

def _history_mapper(local_mapper):
    cls = local_mapper.class_

    # set the "active_history" flag
    # on on column-mapped attributes so that the old version
    # of the info is always loaded (currently sets it on all attributes)
    for prop in local_mapper.iterate_properties:
        getattr(local_mapper.class_, prop.key).impl.active_history = True

    super_mapper = local_mapper.inherits
    super_history_mapper = getattr(cls, '__history_mapper__', None)

    polymorphic_on = None
    super_fks = []
    if not super_mapper or local_mapper.local_table is not super_mapper.local_table:
        cols = []
        for column in local_mapper.local_table.c:
            if column.name.startswith('version_'):
                continue

            col = column.copy()
            col.unique = False

            if super_mapper and col_references_table(column, super_mapper.local_table):
                super_fks.append((col.key, list(super_history_mapper.local_table.primary_key)[0]))

            cols.append(col)

            if column is local_mapper.polymorphic_on:
                polymorphic_on = col

        if super_mapper:
            super_fks.append(('version_datetime', super_history_mapper.base_mapper.local_table.c.version_datetime))
            super_fks.append(('version_userid', super_history_mapper.base_mapper.local_table.c.version_userid))
            super_fks.append(('version_deleted', super_history_mapper.base_mapper.local_table.c.version_deleted))
            cols.append(Column('version_datetime', DateTime, default=datetime.now, nullable=False, primary_key=True, info={'colanderalchemy': {'exclude': True}}))
            cols.append(Column('version_userid', String(60), ForeignKey("user.login"), nullable=True, info={'colanderalchemy': {'exclude': True}}))
            cols.append(Column('version_deleted', Boolean, server_default=false(), nullable=False, info={'colanderalchemy': {'exclude': True}}))
        else:
            cols.append(Column('version_datetime', DateTime, default=datetime.now, nullable=False, primary_key=True, info={'colanderalchemy': {'exclude': True}}))
            cols.append(Column('version_userid', String(60), ForeignKey("user.login"), nullable=True, info={'colanderalchemy': {'exclude': True}}))
            cols.append(Column('version_deleted', Boolean, server_default=false(), nullable=False, info={'colanderalchemy': {'exclude': True}}))

        if super_fks:
            cols.append(ForeignKeyConstraint(*zip(*super_fks)))

        table = Table(local_mapper.local_table.name + '_history', local_mapper.local_table.metadata,
           *cols
        )
    else:
        # single table inheritance.  take any additional columns that may have
        # been added and add them to the history table.
        for column in local_mapper.local_table.c:
            if column.key not in super_history_mapper.local_table.c:
                col = column.copy()
                col.unique = False
                super_history_mapper.local_table.append_column(col)
        table = None

    if super_history_mapper:
        bases = (super_history_mapper.class_,)
    else:
        bases = local_mapper.base_mapper.class_.__bases__
    versioned_cls = type.__new__(type, "%sHistory" % cls.__name__, bases, {})

    m = mapper(
            versioned_cls,
            table,
            inherits=super_history_mapper,
            polymorphic_on=polymorphic_on,
            polymorphic_identity=local_mapper.polymorphic_identity
            )
    cls.__history_mapper__ = m

    if not super_history_mapper:
        local_mapper.local_table.append_column(
            Column('version_datetime', DateTime, default=datetime.now, nullable=False, primary_key=False, info={'colanderalchemy': {'exclude': True}})
        )
        local_mapper.add_property("version_datetime", local_mapper.local_table.c.version_datetime)
        local_mapper.local_table.append_column(
            Column('version_userid', String(60), ForeignKey("user.login"), nullable=True, info={'colanderalchemy': {'exclude': True}})
        )
        local_mapper.add_property("version_userid", local_mapper.local_table.c.version_userid)
        local_mapper.local_table.append_column(
            Column('version_deleted', Boolean, server_default=false(), nullable=False, info={'colanderalchemy': {'exclude': True}})
        )
        local_mapper.add_property("version_deleted", local_mapper.local_table.c.version_deleted)


class Versioned(object):
    @declared_attr
    def __mapper_cls__(cls):
        def map(cls, *arg, **kw):
            mp = mapper(cls, *arg, **kw)
            _history_mapper(mp)
            return mp
        return map


def versioned_objects(iter):
    for obj in iter:
        if hasattr(obj, '__history_mapper__'):
            yield obj

def create_version(obj, session, deleted = False):
    obj_mapper = object_mapper(obj)
    history_mapper = obj.__history_mapper__
    history_cls = history_mapper.class_

    obj_state = attributes.instance_state(obj)

    attr = {}

    obj_changed = False

    for om, hm in zip(obj_mapper.iterate_to_root(), history_mapper.iterate_to_root()):
        if hm.single:
            continue

        for hist_col in hm.local_table.c:
            if hist_col.key.startswith('version_'):
                continue

            obj_col = om.local_table.c[hist_col.key]

            # get the value of the
            # attribute based on the MapperProperty related to the
            # mapped column.  this will allow usage of MapperProperties
            # that have a different keyname than that of the mapped column.
            try:
                prop = obj_mapper.get_property_by_column(obj_col)
            except UnmappedColumnError:
                # in the case of single table inheritance, there may be
                # columns on the mapped table intended for the subclass only.
                # the "unmapped" status of the subclass column on the
                # base class is a feature of the declarative module as of sqla 0.5.2.
                continue

            # expired object attributes and also deferred cols might not be in the
            # dict.  force it to load no matter what by using getattr().
            if prop.key not in obj_state.dict:
                getattr(obj, prop.key)

            a, u, d = attributes.get_history(obj, prop.key)

            if d:
                attr[hist_col.key] = d[0]
                obj_changed = True
            elif u:
                attr[hist_col.key] = u[0]
            else:
                # if the attribute had no value.
                attr[hist_col.key] = a[0]
                obj_changed = True

    if not obj_changed:
        # not changed, but we have relationships.  OK
        # check those too
        for prop in obj_mapper.iterate_properties:
            if isinstance(prop, RelationshipProperty) and \
                attributes.get_history(obj, prop.key).has_changes():
                obj_changed = True
                break

    if not obj_changed and not deleted:
        return

    attr['version_datetime'] = obj.version_datetime
    attr['version_userid'] = obj.version_userid
    attr['version_deleted'] = obj.version_deleted
    hist = history_cls()
    for key, value in attr.items():
        setattr(hist, key, value)
    session.add(hist)
    obj.version_datetime = datetime.now()
    obj.version_userid = getattr(session, 'userid', None)
    obj.version_deleted = deleted

def versioned_session(session):
    @event.listens_for(session, 'before_flush')
    def before_flush(session, flush_context, instances):
        for obj in versioned_objects(session.deleted):
            create_version(obj, session, deleted = True)
        for obj in versioned_objects(session.dirty):
            create_version(obj, session)

def add_userid_to_session(userid, session):
    if isinstance(session, scoping.scoped_session):
        thread_local_session = session.registry()
        thread_local_session.userid = userid
    elif isinstance(session, Session):
        session.userid = userid
    else:
        raise TypeError("Not sure how to add the userid into session of type {}".format(type(session))) 

И здесь'как ям с его помощью (все несущественные части были вырезаны):

Base = declarative_base()

class User(Versioned, Base):
    __tablename__ = 'user'
    login = Column(String(60), primary_key=True, nullable=False)

    groups = association_proxy('user_to_groups', 'group', creator=lambda group: UserToGroup(group_name=group.name))

    def __init__(self, login, groups=None):
        self.login = login
        if groups:
            for group in groups:
                self.groups.append(group)

class Group(Versioned, Base):
    __tablename__ = 'group'
    name = Column(String(100), primary_key=True, nullable=False)
    description = Column(String(100), nullable=True)

    users = association_proxy('group_to_user', 'user', creator=lambda user: UserToGroup(user_login=user.login))

    def __eq__(self, other):
        return self.name == other.name

class UserToGroup(Versioned, Base):
    __tablename__ = 'user_to_group'
    user_login = Column(String(60), ForeignKey(User.login), primary_key=true)
    group_name = Column(String(100), ForeignKey(Group.name), primary_key=true)

    user = relationship(User, backref=backref('user_to_groups', cascade='all, delete-orphan'))
    group = relationship(Group, backref=backref('group_to_user', cascade='all, delete-orphan'))


session.configure(bind=engine)
add_userid_to_session("test", session.registry())
versioned_session(session)
user = session.query(User).filter(User.login=='test').one()
user.groups.remove(Group(name ="g:admin"))

До запуска этого кода в базе данных в настоящее время есть один пользователь с именем 'тестовое задание' и две группы, к которым присоединен пользовательг: админ а также 'г: суперамин.

Что он в данный момент делает: Скопируйте существующую запись user_to_group для 'тест '=> 'г: админ сопоставление и скопировать его в таблицу истории. Затем удалите запись из user_to_group.

Что я'мне бы хотелось, чтобы это скопировало значение в таблицу истории и затем обновило запись в user_to_group, чтобыversion_deleted установить на .I 'true

я думаю, что способ сделать это состоит в том, чтобы вырвать запись из сеанса.Поэтому я изменил порядок с исходного кода) и изменил его, поместив в файл session.dirty. Я'Я просто не уверен, что "безопасный» способ сделать это.

Другая проблема (которая, вероятно, потребует другого вопроса) заключается в том, как определить отношения, которые рассматриваются в другой таблице, поскольку в настоящее время система делает копиюпользователь» строка в таблицу истории, а затем обновляет информацию о версии, несмотря на то, что в строку не было внесено никаких реальных изменений.

РЕДАКТИРОВАТЬ: ЯМы решили сделать что-то немного по-другому, но все еще есть проблема ... Вместо того, чтобы "удален» пометить в живых таблицах Я фактически удаляю содержимое и записываю другой элемент истории, указывающий, когда произошло удаление. Если я'м, удалив объект напрямую, тогда это работает правильно. Если я удаляю объект из отношений, яЯ не могу сделать это правильно.DELETE получить'выданы в таблицу отношений для удаления ссылки, но я не могукажется, не понять, как обнаружить это удаление в "create_version» метод.

Например, если я делаю:

group = session.query(Group).filter(Group.name=='g:admin').one()
group.users.remove(group.users[0])

Объекты не помещаются в session.deleted. Я могу обнаружить какое-то удаление черезattributes.get_history(obj, prop.key), но, похоже, указывает на удалениеUserToGroup объект изGroup (который я хочу обнаружить и записать элемент истории), но затем также указывает на удалениеGroup отUserToGroup объект (который я нене хочу ничего делать, потому что фактическаяGroup не удаляется).

Ответы на вопрос(0)

Ваш ответ на вопрос