31

これは私の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'),
    )

基本的には、Declarative による多対多のタグ付けです。タグ付けからいくつかのエントリを削除するとき、SQLAlchemy でオーファンを片付けてほしいです。ドキュメントでわかったように、この機能を有効にするには、次のようにする必要があります。

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

ただし、このような設定はAssertionError: This AttributeImpl is not configured to trackparents につながります。私はそれをグーグルで検索しましたが、SQLAlchemy のオープンソース コード以外は何も見つかりませんでした。したがって、クラスメソッドTag.delete_orphans()(上記のコードにあります) を作成して、孤児が発生する可能性があると思うたびに呼び出すようにしましたが、それはあまりエレガントではないようです。

delete-orphan私の設定がうまくいかない理由はありますか?

4

1 に答える 1

84

この場合は、もっと詳しく調べる必要がありますが、例外になる可能性が高い警告がここにあり、それを調べます。あなたの例の実用的なバージョンは次のとおりです。

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 が設定されていない場合、多対多または多対 1 の関係で孤立削除カスケードはサポートされません。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.

それがあなたの「片親」です - 「孤児の削除」機能は、子がその片親の範囲内に完全に存在する、いわゆるライフサイクル関係でのみ機能します。したがって、「孤立」で多対多を使用する意味は事実上ありません。サポートされているのは、関連テーブルでこの動作を取得したいと本当に望んでいたためです (レガシー DB のものなど)。

そのためのドキュメントは次のとおりです。

delete-orphan カスケードは、各子オブジェクトが一度に 1 つの親しか持てないことを意味するため、ほとんどの場合、1 対多の関係で構成されます。多対 1 または多対多の関係に設定するのは、より厄介です。このユース ケースの場合、SQLAlchemy では、relationship() を single_parent=True 関数で構成する必要があります。これにより、オブジェクトが一度に 1 つの親のみに関連付けられるようにする Python 側の検証が確立されます。

「孤児を一掃したい」と言うとき、何を意味しますか? ここで、あなたが言うならr1.tags.remove(t1)、あなたは「フラッシュ」と言ったことを意味します. SQLAlchemy は、「r1.tags、t1 が削除されました。孤立している場合は、削除する必要があります。よし、「タグ付け」に進み、テーブル全体をスキャンしましょう。残っているすべてのエントリ。" 一度に各タグに対して単純にこれを行うのは、明らかに非効率的です。セッションで数百のタグ コレクションに影響を与えると、これらの潜在的に巨大なクエリが数百になることになります。作業ユニットは一度に 1 つのコレクションの観点から考える傾向があるため、非常に複雑な機能の追加 - それでも、人々が本当に望んでいない明白なクエリ オーバーヘッドが追加される可能性があります。多くの複雑さと驚きを追加する異常なエッジケースのビジネスから離れようとします. 実際には、「delete-orphan」システムは、オブジェクト B がメモリ内のオブジェクト A から切り離されたときにのみ機能します。データベースまたはそのようなもの、それ'

ここで「delete orphans」で行っていることは正しい方向に進んでいますが、それをイベントに貼り付けて、より効率的なクエリを使用し、不要なものをすべて一度に削除してみましょう。

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 基準で削除できる場合は、オブジェクトを削除するためにオブジェクトをメモリにプルする必要はありません (データベースがより効率的に操作を実行できる場合にメモリに行をプルすることに依存することは、プログラミングを苦しめることによって行と呼ばれています) )。「NOT EXISTS」は、プランナーでより高価になる傾向がある OUTER JOIN と比較して、関連する行がないことを検索する場合にも非常にうまく機能します。

于 2012-02-13T16:58:18.860 に答える