python - 当实例分配给关系时,sqlalchemy before_flush 事件处理程序看不到外键的变化

标签 python sqlalchemy

我有一个 before_flush 事件监听器,用于检查员工的经理是否更换。在这种情况下,将在 EmpManHist 表中自动创建一条记录。 manager 是对 Employee 表的自引用。 这是我的表定义:

class Employee(Base):
    __tablename__ = 'employees'

    emp_id = Column(String, primary_key=True, unique=True)
    name = Column(String, nullable=False)
    manager_id = Column(String, ForeignKey('employees.emp_id'))
    direct_reports = relationship('Employee', backref=backref('manager', remote_side=[emp_id]))

当我通过直接修改 ForeignKey (manager_id) 将新经理分配给员工时,事件监听器会正确拾取它。但是当我进行实例分配时,它不会:

# this code does not pick up the manager_id change in the 'before_flush' event listener
emp2.manager = emp3
dal.session.add(emp2)
dal.session.commit()

# this does
emp2.manager_id = '1'
dal.session.add(emp2)
dal.session.commit()

这是为什么呢? 我正在使用 python 3.6.3 和 sqlalchemy 1.1.13

下面是完整的工作示例:

from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy import event
from sqlalchemy.orm.attributes import get_history
import datetime


Base = declarative_base()


class DataAccessLayer(object):

    def __init__(self):
        self.conn_string = conn_string
        self.engine = None
        self.session = None
        self.Session = None
        self.echo = True

    def connect(self):
        self.engine = create_engine(self.conn_string, echo=self.echo)
        Base.metadata.create_all(self.engine)
        self.Session = sessionmaker(bind=self.engine)


class Employee(Base):
    __tablename__ = 'employees'

    emp_id = Column(String, primary_key=True, unique=True)
    name = Column(String, nullable=False)
    manager_id = Column(String, ForeignKey('employees.emp_id'))
    direct_reports = relationship('Employee', backref=backref('manager', remote_side=[emp_id]))


class EmpManHist(Base):
    __tablename__ = 'emp_man_history'

    id = Column(Integer, primary_key=True)
    emp_id = Column(String, ForeignKey('employees.emp_id'))
    man_id_from = Column(String, ForeignKey('employees.emp_id'))
    man_id_to = Column(String, ForeignKey('employees.emp_id'))
    when = Column(DateTime, default=datetime.datetime.now)

    manager_from = relationship('Employee', foreign_keys=[man_id_from])
    manager_to = relationship('Employee', foreign_keys=[man_id_to])


conn_string = 'sqlite:///:memory:'
dal = DataAccessLayer()
dal.echo = True
dal.connect()
dal.session = dal.Session()


@event.listens_for(dal.session, 'before_flush')
def _emp_history_update(session, flush_context, instances):
    print("BEFORE FLUSH")
    for instance in session.dirty:
        if not isinstance(instance, Employee):
            continue
        man_hist = get_history(instance, 'manager_id')
        if man_hist.added:
            if man_hist.deleted:
                man_deleted = str(man_hist.deleted[0])
            else:
                man_deleted = None
            emp_man_hist = EmpManHist(emp_id=instance.emp_id, man_id_from=man_deleted,
                                      man_id_to=str(man_hist.added[0]))
            session.add(emp_man_hist)


emp1 = Employee(emp_id='1', name="AAA")
emp2 = Employee(emp_id='2', name="BBB", manager_id='1')
emp3 = Employee(emp_id='3', name="CCC", manager_id='1')


dal.session.add(emp3)
dal.session.flush()
dal.session.add(emp1)
dal.session.add(emp2)

dal.session.commit()

# this code does not pick up the manager_id change in the 'before_flush' event listener
emp2.manager = emp3
dal.session.add(emp2)
dal.session.add(emp3)
dal.session.commit()

# this does
emp2.manager_id = '1'
dal.session.add(emp2)
dal.session.commit()

最佳答案

当关系属性更改时,SQLAlchemy 不会立即更新外键字段。所以你的问题的答案是 before_flush 事件在 SQLAlchemy 更新 Employee 实例的外键值之前被触发,作为 flush 的一部分手术。

在您自己专门更新 manager_id 属性的情况下,该属性会在 before_flush 事件被触发之前更改,这就是为什么您会在 _emp_history_update 函数在这种情况下。

您仍然可以使用 after_flush 事件做任何您想做的事情,因为那时 session.dirty 还没有被清除。所以我将您的事件监听器更改为:

@event.listens_for(session, 'after_flush')
def _emp_history_update(session, flush_context):
    for instance in session.dirty:
        if not isinstance(instance, Employee):
            continue
        man_hist = get_history(instance, 'manager_id')
        if man_hist.added:
            if man_hist.deleted:
                man_deleted = str(man_hist.deleted[0])
            else:
                man_deleted = None
            emp_man_hist = EmpManHist(emp_id=instance.emp_id, man_id_from=man_deleted,
                                      man_id_to=str(man_hist.added[0]))
            session.add(emp_man_hist)

这是测试代码:

emp1 = Employee(emp_id='1', name="AAA")
emp2 = Employee(emp_id='2', name="BBB", manager_id='1')
emp3 = Employee(emp_id='3', name="CCC", manager_id='1')

# I'm not using your DataAccessLayer object but that doesn't change anything
session.add_all([emp1, emp2, emp3])
# i've not explicitly called session.flush() as it's called by session.commit() anyway
session.commit()

# change the emp2's manager through relationship attribute
emp2.manager = emp3
# no need to re-add the Employee objects to the session
session.commit()

for change in session.query(EmpManHist).all():
    print(f'Employee {change.emp_id} changed to mgr_id {change.man_id_to}')

哪些输出:

Employee 2 changed to mgr_id 3

我注意到的另一件事是,在您的事件监听器的这一部分:

    if man_hist.added:
        if man_hist.deleted:
            man_deleted = str(man_hist.deleted[0])
        else:
            man_deleted = None
        emp_man_hist = EmpManHist(emp_id=instance.emp_id, man_id_from=man_deleted,
                                  man_id_to=str(man_hist.added[0]))
        session.add(emp_man_hist)

通过自己直接更改属性或更改 Employee.manager 关系属性来更改 Employee 实例的 manager_id,从不在 man_hist.deleted 中显示一个实例。因此,EmpManHist 实例的 man_id_from 属性始终为 None

这是从您的示例代码生成的 INSERTemp_man_hist 的日志,您可以从第二行的值列表中看到第二行与 man_id_from 对齐的值被分配 None:

2018-07-27 09:03:41,189 INFO sqlalchemy.engine.base.Engine INSERT INTO emp_man_history (emp_id, man_id_from, man_id_to, "when") VALUES (?, ?, ?, ?)
2018-07-27 09:03:41,189 INFO sqlalchemy.engine.base.Engine ('2', None, '1', '2018-07-27 09:03:41.188906')

关于python - 当实例分配给关系时,sqlalchemy before_flush 事件处理程序看不到外键的变化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51376652/

相关文章:

python - 如何在不拆分数据帧的情况下传递不同的数据集进行训练和测试。 (Python)?

postgresql - 如何将 pandas DataFrame 插入现有的 PostgreSQL 表?

python - SQLAlchemy 没有为多列 UniqueConstraints 生成正确的 SQL 语句

python - Sqlalchemy 使用数组过滤嵌套 json

python - sqlalchemy操作错误: (OperationalError) unable to open database file None None

使用 numpy 矩阵计算距离的 Pythonic 方法?

java - Python 相当于 Java 的 Set.add()?

python - 如何将 python argparse 与 sys.argv 以外的参数一起使用?

python - Sqlalchemy : association table for many-to-many relationship between template_id and department. 如何删除关系?

Python tkinter 标签小部件鼠标悬停