python - 将实例分配给关系时,sqlalchemy before_flush 事件处理程序看不到外键的变化
问题描述
我有一个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)
通过自己直接更改属性或更改关系属性来更改实例的,manager_id
永远不会使实例显示在. 因此,您的实例属性始终为.Employee
Employee.manager
man_hist.deleted
man_id_from
EmpManHist
None
INSERT
这是从您的示例代码生成的日志,emp_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')
推荐阅读
- react-native - TypeError:无法在“SubtleCrypto”上执行“签名”:参数 2 不是“CryptoKey”类型
- excel - 如何将“01 02:52:12”转换为Excel中的时间戳并将其从UTC转换为PST?
- php - 如何用 PHP 替换 htaccess 中的字符串?
- javascript - 可以使用 Notion API 从 csv 列表中在页面上创建 Twitter 嵌入吗?
- javascript - Firebase Modular SDK V9.0.0+ TypeError:无法读取未定义 Firebase 的属性“应用程序”
- reactjs - React-router 一次重置所有位置状态
- fonts - 如何自定义 PowerBI 卡片视觉标签的字体
- python - 按列排序表
- python - 网页抓取文章得到 AttributeError: 'str' object has no attribute 'text'
- arrays - 使用堆栈查找数组中的下一个更大元素背后的真正直觉是什么