2012-02-10 5 views
21

to moja Kolba-SQLAlchemy kod deklaratywna:Ustawienie usuwania-sierotę na relacji sqlalchemy powoduje AssertionError: Ten AttributeImpl nie jest skonfigurowany do śledzenia rodziców

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

Zasadniczo, to wiele-do-wielu tagging z deklaratywnej. Usuwając niektóre wpisy z tagów, chcę aby SQLAlchemy uporządkowało sieroty. Jak dowiedział się w Dokumentach, aby włączyć tę funkcję, należy zrobić to:

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

Jednakże takie ustawienie prowadzi do AssertionError: Ten AttributeImpl nie jest skonfigurowany do śledzenia rodziców. Przeszukałem go i nie znalazłem nic poza otwartym kodem SQLAlchemy. Dlatego stworzyłem metodę klasy Tag.delete_orphans() (jest w powyższym kodzie), aby wywoływać ją za każdym razem, gdy wydaje mi się, że niektóre sieroty mogą się pojawić, ale to nie wydaje się być bardzo eleganckie.

Wszelkie pomysły i wyjaśnienia, dlaczego moje ustawienie z delete-orphan nie działa?

Odpowiedz

61

OK, w tym przypadku musisz przyjrzeć się uważniej, chociaż jest tu ostrzeżenie, które prawdopodobnie powinno stać się wyjątkiem, i przyjrzę się temu. Oto wersja robocza swoim przykładzie:

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() 

Teraz uruchom:

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

Więc tutaj jest ważną częścią: SAWarning: Na Role.tags, delete-sierota kaskada nie jest obsługiwana w wielu -to-wiele lub wiele-do-jednego relacji, gdy single_parent nie jest ustawiony. Ustaw single_parent = True na relacji().

więc błąd jest stały, jeśli powiesz to:

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

Ale można zauważyć, że nie jest to co chcesz:

r1 = Role() 
r2 = Role() 

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

wyjściowa:

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. 

To jest twój "samotny rodzic" - funkcja "usuń sierotę" działa tylko z tak zwanym lifecycle związek, w którym dziecko istnieje całkowicie w zakresie jego samotnego rodzica. Więc praktycznie nie ma sensu używanie "wielu-do-wielu" z "osieroconymi", i to jest obsługiwane tylko dlatego, że niektórzy naprawdę, naprawdę chcieli uzyskać to zachowanie z tabelą skojarzeń, niezależnie od tego (starsze pliki DB, być może).

Herezje the doc za to:

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.

Co zakłada się, gdy mówisz: „Chcę, żeby oczyścić sieroty”? Oznaczałoby to tutaj, że gdybyś miał powiedzieć r1.tags.remove(t1), to powiedziałeś "flush". SQLAlchemy zauważy, że "r1.tags, t1 został usunięty, a jeśli to sierota musimy usunąć! OK, więc chodźmy do" tagowania ", a następnie przeskanuj całą tabelę pod kątem wszelkich wpisów, które pozostają." rób to naiwnie dla każdego tagu na raz, byłoby to naprawdę nieefektywne - gdybyś miał wpływ na kilkaset kolekcji tagów w sesji, pojawi się kilkaset tych potencjalnie ogromnych zapytań.Zrobić to mniej niż naiwnie byłoby dość skomplikowanym dodatkiem do funkcji, ponieważ jednostka pracy ma tendencję do myślenia w kategoriach jednej kolekcji na raz - i wciąż dodawałoby namacalne zapytanie, że ludzie mogą nie chcieć. Jednostka pracy robi to, co robi naprawdę dobrze, ale stara się trzymać z dala od biznesu nietypowych przypadków, które dodają wiele komplikacji i niespodzianek. W rzeczywistości system "usuń-sierotę" wchodzi w grę tylko wtedy, gdy obiekt B jest odłączony od obiektu A w pamięci - nie ma skanowania bazy danych ani niczego podobnego, jest to o wiele prostsze - i proces płukania musi pozostać rzeczy tak proste, jak to możliwe.

To, co tutaj robisz z "usuwaniem sierot", jest na dobrej drodze, ale wrzućmy je do wydarzenia, a także użyj bardziej wydajnego zapytania i usuń wszystko, czego nie potrzebujemy za jednym razem:

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 

teraz ze sobą równo dostajemy tego zapytania na końcu:

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

więc nie trzeba ciągnąć przedmiotów do pamięci, aby je usunąć, kiedy możemy usuwać na prostym SQL kryterium (polegające na wciągnięciu wierszy do pamięci, gdy baza danych może wykonać operację bardziej efektywnie) pl o nazwie row by agonizing row programowanie). "NOT EXISTS" działa bardzo dobrze, gdy szuka nieobecności odpowiedniego wiersza, w porównaniu do OUTER JOIN, który jest zazwyczaj droższy w planowaniu.

+3

Świetna odpowiedź. Dziękuję za tak szczegółowe i pełne wyjaśnienia wyjaśnienie i dziękuję za działający kod. Cieszę się, że mogę nauczyć się i rozumieć więcej SQLAlchemy na przykładzie pracującym, ponieważ trudno jest go przeczytać i uzyskać wszystko od teorii w dokumentach. –

+2

Naprawdę doskonała odpowiedź! – NobRuked

+2

Fantastyczna odpowiedź - dziękuję –

Powiązane problemy