在SQLAlchemy关系上设置delete孤儿会导致断言错误:此AttributeImpl未配置为跟踪父级

2024-04-20 07:35:19 发布

您现在位置:Python中文网/ 问答频道 /正文

这是我的Flask SQLAlchemy声明代码:

from sqlalchemy.ext.associationproxy import association_proxy
from my_flask_project import db


tagging = db.Table('tagging',
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id', ondelete='cascade'), primary_key=True),
    db.Column('role_id', db.Integer, db.ForeignKey('role.id', ondelete='cascade'), primary_key=True)
)


class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), unique=True, nullable=False)

    def __init__(self, name=None):
        self.name = name

    @classmethod
    def delete_orphans(cls):
        for tag in Tag.query.outerjoin(tagging).filter(tagging.c.role_id == None):
            db.session.delete(tag)


class Role(db.Model):

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='cascade'))
    user = db.relationship('User', backref=db.backref('roles', cascade='all', lazy='dynamic'))
    ...
    tags = db.relationship('Tag', secondary=tagging, cascade='all', backref=db.backref('roles', cascade='all'))
    tag_names = association_proxy('tags', 'name')

    __table_args__ = (
        db.UniqueConstraint('user_id', 'check_id'),
    )

基本上,它是用声明性的多对多标记。从标记中删除一些条目时,我希望SQLAlchemy整理孤儿。正如我在文档中发现的,要启用此功能,我应该执行以下操作:

class Role(db.Model):
    ...
    tags = db.relationship('Tag', secondary=tagging, cascade='all,delete-orphan', backref=db.backref('roles', cascade='all'))
    ...

但是,这样的设置会导致断言错误:此AttributeImpl未配置为跟踪父级。我搜索了它,除了SQLAlchemy的开源代码之外,什么也找不到。因此,我创建了classmethodTag.delete_orphans()(在上面的代码中)来在每次我认为可能发生孤儿时调用它,但这似乎不是很优雅。

为什么我的设置delete-orphan不起作用?


Tags: keynameidtruedbtagcolumninteger
1条回答
网友
1楼 · 发布于 2024-04-20 07:35:19

好的,在这种情况下,你需要更仔细地观察,尽管这里有一个警告,可能会成为一个例外,我将对此进行研究。下面是您示例的工作版本:

from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base

Base= declarative_base()

tagging = Table('tagging',Base.metadata,
    Column('tag_id', Integer, ForeignKey('tag.id', ondelete='cascade'), primary_key=True),
    Column('role_id', Integer, ForeignKey('role.id', ondelete='cascade'), primary_key=True)
)

class Tag(Base):

    __tablename__ = 'tag'
    id = Column(Integer, primary_key=True)
    name = Column(String(100), unique=True, nullable=False)

    def __init__(self, name=None):
        self.name = name

class Role(Base):
    __tablename__ = 'role'

    id = Column(Integer, primary_key=True)
    tag_names = association_proxy('tags', 'name')

    tags = relationship('Tag', 
                        secondary=tagging, 
                        cascade='all,delete-orphan', 
                        backref=backref('roles', cascade='all'))


e = create_engine("sqlite://", echo=True)

Base.metadata.create_all(e)

s = Session(e)

r1 = Role()
r1.tag_names.extend(["t1", "t2", "t3"])
s.add(r1)
s.commit()

现在让我们运行:

... creates tables
/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/properties.py:918: SAWarning: On Role.tags, delete-orphan cascade is not supported on a many-to-many or many-to-one relationship when single_parent is not set.   Set single_parent=True on the relationship().
  self._determine_direction()
Traceback (most recent call last):
  ... stacktrace ...
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/attributes.py", line 349, in hasparent
    assert self.trackparent, "This AttributeImpl is not configured to track parents."
AssertionError: This AttributeImpl is not configured to track parents.

所以这里有一个重要的部分:SAWarning:在Role.tags上,当没有设置single_parent时,在多对多或多对一关系上不支持delete orphan cascade。在relationship()上设置single_parent=True。

所以,如果你这样说,错误就被修正了:

tags = relationship('Tag', 
                    secondary=tagging, 
                    cascade='all,delete-orphan', 
                    single_parent=True,
                    backref=backref('roles', cascade='all'))

但是,你可能会发现,这并不是你真正想要的:

r1 = Role()
r2 = Role()

t1, t2 = Tag("t1"), Tag("t2")
r1.tags.extend([t1, t2])
r2.tags.append(t1)

输出:

sqlalchemy.exc.InvalidRequestError: Instance <Tag at 0x101503a10> is already associated with an instance of <class '__main__.Role'> via its Role.tags attribute, and is only allowed a single parent.

这就是你的“单亲”——“删除孤儿”功能只适用于所谓的生命周期关系,即孩子完全存在于单亲范围内。因此,在“孤立”中使用多对多实际上没有意义,它得到支持只是因为有些人真的,真的想通过关联表获得这种行为而不管(可能是遗留数据库的东西)。

这里是the doc表示:

delete-orphan cascade implies that each child object can only have one parent at a time, so is configured in the vast majority of cases on a one-to-many relationship. Setting it on a many-to-one or many-to-many relationship is more awkward; for this use case, SQLAlchemy requires that the relationship() be configured with the single_parent=True function, which establishes Python-side validation that ensures the object is associated with only one parent at a time.

当你说“我要它来清除孤儿”时意味着什么?这意味着,如果你说r1.tags.remove(t1),那么你说“flush”。SQLAlchemy会看到,“r1.tags,t1已被删除,如果它是孤儿,我们需要删除!好的,让我们开始“标记”,然后对整个表进行扫描,查找所有剩余的条目一次对每个标记进行这种简单的操作显然是非常低效的——如果您在一个会话中影响了几百个标记集合,那么将有几百个潜在的巨大查询。不那么天真地这样做将是一个相当复杂的特性添加,因为工作单元往往一次只考虑一个集合——而且它仍然会增加人们可能并不真正想要的明显的查询开销。工作单元做得很好,但它试图远离那些增加了许多复杂性和惊喜的不寻常的边缘案例。实际上,“delete orphan”系统只在对象B与内存中的对象A分离时才起作用-没有扫描数据库或类似的东西,它要简单得多-刷新过程必须使事情尽可能简单。

所以你在这里用“delete孤儿”做的事情是正确的,但是让我们把它放到一个事件中,并使用一个更有效的查询,一次性删除我们不需要的所有内容:

from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import event

Base= declarative_base()

tagging = Table('tagging',Base.metadata,
    Column('tag_id', Integer, ForeignKey('tag.id', ondelete='cascade'), primary_key=True),
    Column('role_id', Integer, ForeignKey('role.id', ondelete='cascade'), primary_key=True)
)

class Tag(Base):

    __tablename__ = 'tag'
    id = Column(Integer, primary_key=True)
    name = Column(String(100), unique=True, nullable=False)

    def __init__(self, name=None):
        self.name = name

class Role(Base):
    __tablename__ = 'role'

    id = Column(Integer, primary_key=True)
    tag_names = association_proxy('tags', 'name')

    tags = relationship('Tag', 
                        secondary=tagging,
                        backref='roles')

@event.listens_for(Session, 'after_flush')
def delete_tag_orphans(session, ctx):
    session.query(Tag).\
        filter(~Tag.roles.any()).\
        delete(synchronize_session=False)

e = create_engine("sqlite://", echo=True)

Base.metadata.create_all(e)

s = Session(e)

r1 = Role()
r2 = Role()
r3 = Role()
t1, t2, t3, t4 = Tag("t1"), Tag("t2"), Tag("t3"), Tag("t4")

r1.tags.extend([t1, t2])
r2.tags.extend([t2, t3])
r3.tags.extend([t4])
s.add_all([r1, r2, r3])

assert s.query(Tag).count() == 4

r2.tags.remove(t2)

assert s.query(Tag).count() == 4

r1.tags.remove(t2)

assert s.query(Tag).count() == 3

r1.tags.remove(t1)

assert s.query(Tag).count() == 2

每次刷新后,我们都会在最后得到这个查询:

DELETE FROM tag WHERE NOT (EXISTS (SELECT 1 
FROM tagging, role 
WHERE tag.id = tagging.tag_id AND role.id = tagging.role_id))

因此,我们不需要为了删除对象而将对象拉入内存,而只需要根据简单的SQL标准进行删除(当数据库能够更有效地执行操作时,依赖于将行拉入内存,这被称为row by agonizing row编程)。“不存在”在搜索缺少相关行时也非常有效,而外部连接在计划器中往往更昂贵。

相关问题 更多 >