我有两个表,比如A和B.两者都有一个主键ID。他们有多对多的关系,SEC。
SEC = Table('sec', Base.metadata,
Column('a_id', Integer, ForeignKey('A.id'), primary_key=True, nullable=False),
Column('b_id', Integer, ForeignKey('B.id'), primary_key=True, nullable=False)
)
class A():
...
id = Column(Integer, primary_key=True)
...
rels = relationship(B, secondary=SEC)
class B():
...
id = Column(Integer, primary_key=True)
...
让我们考虑一下这段代码。
a = A()
b1 = B()
b2 = B()
a.rels = [b1, b2]
...
#some place later
b3 = B()
a.rels = [b1, b3] # errors sometimes
有时,我在最后一行说错误
duplicate key value violates unique constraint a_b_pkey
根据我的理解,我认为它会尝试再次将(a.id,b.id)添加到'sec'表中,从而导致唯一的约束错误。这是什么?如果是这样,我该如何避免这种情况?如果没有,为什么我会有这个错误?
答案 0 :(得分:9)
问题是您要确保您创建的实例是唯一的。我们可以创建一个备用构造函数来检查现有未提交实例的缓存,或者在返回新实例之前查询数据库中的现有提交实例。
以下是这种方法的演示:
from sqlalchemy import Column, Integer, String, ForeignKey, Table
from sqlalchemy.engine import create_engine
from sqlalchemy.ext.declarative.api import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
engine = create_engine('sqlite:///:memory:', echo=True)
Session = sessionmaker(engine)
Base = declarative_base(engine)
session = Session()
class Role(Base):
__tablename__ = 'role'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False, unique=True)
@classmethod
def get_unique(cls, name):
# get the session cache, creating it if necessary
cache = session._unique_cache = getattr(session, '_unique_cache', {})
# create a key for memoizing
key = (cls, name)
# check the cache first
o = cache.get(key)
if o is None:
# check the database if it's not in the cache
o = session.query(cls).filter_by(name=name).first()
if o is None:
# create a new one if it's not in the database
o = cls(name=name)
session.add(o)
# update the cache
cache[key] = o
return o
Base.metadata.create_all()
# demonstrate cache check
r1 = Role.get_unique('admin') # this is new
r2 = Role.get_unique('admin') # from cache
session.commit() # doesn't fail
# demonstrate database check
r1 = Role.get_unique('mod') # this is new
session.commit()
session._unique_cache.clear() # empty cache
r2 = Role.get_unique('mod') # from database
session.commit() # nop
# show final state
print session.query(Role).all() # two unique instances from four create calls
create_unique
方法的灵感来自example from the SQLAlchemy wiki。这个版本不那么复杂,有利于简单性而不是灵活性。我在生产系统中使用它没有任何问题。
显然可以添加改进;这只是一个简单的例子。 get_unique
方法可以从UniqueMixin
继承,用于任意数量的模型。可以实现更灵活的参数记忆。这也撇开了多个线程插入Ants Aasma提到的冲突数据的问题;处理更复杂,但应该是一个明显的扩展。我把它留给你。
答案 1 :(得分:3)
您提到的错误实际上是将一个冲突的值插入到sec表中。为了确保它来自您认为的操作,而不是之前的某些更改,请打开SQL日志记录并检查它在错误输出之前尝试插入的值。
当覆盖多对多集合值时,SQLAlchemy会将集合的新内容与数据库中的状态进行比较,并相应地发出delete和insert语句。除非您在SQLAlchemy内部进行讨论,否则应该有两种方法可以解决此错误。
首先是并发修改:进程1获取值a.rels并注意到它是空的,同时进程2也获取a.rels,将其设置为[b1,b2]并提交刷新(a,b1), (a,b2)元组,进程1将a.rels设置为[b1,b3],注意到先前的内容为空,当它尝试刷新sec元组(a,b1)时,它会得到重复的键错误。在这种情况下,正确的操作通常是从顶部重试事务。在这种情况下,您可以使用serializable transaction isolation来获取序列化错误,该错误与导致重复键错误的业务逻辑错误不同。
第二种情况发生在您通过将rels属性的加载策略设置为noload
来说服SQLAlchemy您不需要知道数据库状态时。在通过添加lazy='noload'
参数或查询时调用.options(noload(A.rels))
来定义关系时,可以执行此操作。 SQLAlchemy将假定sec表没有匹配的行,因为对象加载了这个策略。