web-dev-qa-db-ja.com

SQLAlchemy関係にdelete-Orphanを設定すると、AssertionErrorが発生します:このAttributeImplは、親を追跡するように構成されていません

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

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

_delete-Orphan_の設定が機能しない理由についてのアイデアや説明はありますか?

29
Honza Javorek

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

_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.
_

したがって、重要な部分は次のとおりです。SA警告:Role.tagsで、single_parentが設定されていない場合、delete-Orphanカスケードは多対多または多対1の関係ではサポートされません。関係に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.
_

これが「ひとり親」です。「削除-孤立」機能は、ライフサイクル関係と呼ばれるものでのみ機能し、子は完全にひとり親の範囲内に存在します。したがって、「Orphan」で多対多を使用することに実質的に意味はありません。サポートされているのは、一部の人々が実際に、関連テーブルを使用してこの動作を取得したかったためです(おそらくレガシーDBのもの)。

ここに ドキュメント そのために:

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

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

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

_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行を苦しめることによって プログラミング)。 「NOTEXISTS」は、プランナーでより高価になる傾向があるOUTER JOINと比較して、関連する行がないことを検索する場合にも非常にうまく機能します。

72
zzzeek