Sqlalchemy:次要关系更新

Sri*_*Sri 9 python postgresql orm sqlalchemy relationship

我有两个表,比如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) 
   ...
Run Code Online (Sandbox Code Playgroud)

让我们考虑一下这段代码.

a = A()
b1 = B()
b2 = B()
a.rels = [b1, b2]
...
#some place later
b3 = B()
a.rels = [b1, b3]  # errors sometimes
Run Code Online (Sandbox Code Playgroud)

有时,我在最后一行说错了

duplicate key value violates unique constraint a_b_pkey
Run Code Online (Sandbox Code Playgroud)

在我的理解中,我认为它试图再次将(a.id,b.id)添加到'sec'表中,从而导致唯一的约束错误.这是什么?如果是这样,我该如何避免这种情况?如果没有,为什么我会有这个错误?

dav*_*ism 10

问题是您要确保您创建的实例是唯一的.我们可以创建一个备用构造函数来检查现有未提交实例的缓存,或者在返回新实例之前查询数据库中的现有提交实例.

以下是这种方法的演示:

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
Run Code Online (Sandbox Code Playgroud)

create_unique方法的灵感来自SQLAlchemy wiki中示例.这个版本不那么复杂,有利于简单性而不是灵活性.我在生产系统中使用它没有任何问题.

可以添加明显的改进; 这只是一个简单的例子.该get_unique方法可以从a继承UniqueMixin,用于任意数量的模型.可以实现更灵活的参数记忆.这也撇开了多个线程插入Ants Aasma提到的冲突数据的问题; 处理更复杂,但应该是一个明显的扩展.我把它留给你.


Ant*_*sma 3

您提到的错误确实是由于向 sec 表插入了冲突的值而导致的。为了确保它来自您认为的操作,而不是之前的某些更改,请打开 SQL 日志记录并检查它在出错之前尝试插入哪些值。

当覆盖多对多集合值时,SQLAlchemy 会将集合的新内容与数据库中的状态进行比较,并相应地发出删除和插入语句。除非您正在研究 SQLAlchemy 内部结构,否则应该有两种方法会遇到此错误。

首先是并发修改:进程 1 获取值 a.rels 并注意到它为空,同时进程 2 也获取 a.rels,将其设置为 [b1, b2] 并提交刷新 (a,b1),(a, b2) 元组,进程 1 将 a.rels 设置为 [b1, b3],注意到之前的内容为空,当它尝试刷新第二元组 (a,b1) 时,它会收到重复键错误。在这种情况下,正确的操作通常是从顶部重试事务。在这种情况下,您可以使用可序列化事务隔离来获取序列化错误,该错误与导致重复键错误的业务逻辑错误不同。

当您通过将 rels 属性的加载策略设置为 来设法说服 SQLAlchemy 您不需要了解数据库状态时,就会发生第二种情况noload。这可以在通过添加参数定义关系时完成lazy='noload',或者在查询时调用.options(noload(A.rels))查询来完成。SQLAlchemy 将假设 sec 表没有与使用此策略加载的对象匹配的行。