2012-02-10 50 views
21

這是我的燒瓶SQLAlchemy的聲明代碼:設置在SQLAlchemy的關係刪除孤兒導致Asse田:這AttributeImpl未配置爲跟蹤父母

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')) 
    ... 

然而,這樣的設置會導致Asse田:這AttributeImpl未配置爲跟蹤的父母。我對它進行了搜索,除了SQLAlchemy的開源代碼外沒有發現任何東西。因此,我創建了類方法Tag.delete_orphans()(它在上面的代碼中),每次我想一些孤兒都可能發生時調用它,但這看起來並不優雅。

任何想法或解釋爲什麼我的設置delete-orphan不起作用?

回答

61

好的,在這種情況下,您需要仔細觀察,儘管這裏有一個警告,可能應該成爲一個例外,我會研究這一點。這裏是你的榜樣的工作版本:

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時,可以使用多對多或多對一的關係。在關係()上設置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),那麼你說「沖洗」。 SQLAlchemy會看到「r1.tags,t1已被刪除,如果它是一個孤兒,我們需要刪除!OK,那麼讓我們去」標記「,然後掃描整個表以獲取所有剩餘的條目。」To一次只爲每個標籤做這個事情顯然會非常低效 - 如果您在會話中影響了幾百個標籤集合,那麼這些潛在的巨大查詢就會有幾百個。要做到這一點不會太天真,將會是一個非常複雜的功能添加,因爲工作單元傾向於一次考慮一個集合 - 它仍然會增加人們可能不想要的明顯查詢開銷。工作單位確實做得很好,但它試圖擺脫不尋常邊緣案例的業務,增加了許多複雜性和驚喜。實際上,「刪除孤兒」系統只有在對象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標準(當數據庫可以更高效地執行操作時,依靠將行拉入內存) en編號爲row by agonizing row)。與搜索沒有相關行的「外部連接」相比,「外部連接」在計劃者中往往更昂貴。

+3

很好的答案。感謝您提供詳細而詳細的解釋,並感謝您的工作代碼。我很高興通過工作實例學習和了解更多SQLAlchemy,因爲在文檔中很難從理論上理解和理解。 – 2012-03-17 20:33:56

+2

真是一個很好的答案! – NobRuked 2012-07-25 21:56:26

+2

夢幻般的答案 - 謝謝 – 2012-09-05 12:58:23