Установка удаления-сироты в отношении SQLAlchemy вызывает 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: This AttributeImpl не настроен для отслеживания родителей. Я погуглил и не нашел ничего, кроме открытого кода SQLAlchemy. Поэтому я создал метод класса Tag.delete_orphans() (это в коде выше), чтобы вызывать его каждый раз, когда я думаю, что могут возникнуть некоторые сироты, но это не кажется очень элегантным.

Любые идеи или объяснения, почему моя настройка с delete-orphan не работает?


person Honza Javorek    schedule 10.02.2012    source источник


Ответы (1)


Хорошо, в этом случае вам нужно присмотреться, хотя здесь есть предупреждение, которое, вероятно, должно стать исключением, и я рассмотрю это. Вот рабочая версия вашего примера:

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.

Это ваш «один родитель» — функция «удалить сироту» работает только с так называемыми отношениями жизненного цикла, когда дочерний элемент существует полностью в рамках своего единственного родителя. Таким образом, практически нет смысла использовать «многие ко многим» с «сиротой», и это поддерживается только потому, что некоторые люди действительно хотели получить такое поведение с таблицей ассоциаций независимо (возможно, устаревшие материалы БД).

Вот документ для этого:

Каскад удаления-сироты подразумевает, что каждый дочерний объект может иметь только одного родителя за раз, поэтому в подавляющем большинстве случаев настраивается на отношение «один ко многим». Установка его на отношения «многие-к-одному» или «многие-ко-многим» более неудобна; для этого варианта использования SQLAlchemy требует, чтобы отношение() было настроено с помощью функции single_parent=True, которая устанавливает проверку на стороне Python, которая гарантирует, что объект одновременно связан только с одним родителем.

Что подразумевается, когда вы говорите: «Я хочу, чтобы он вычистил сирот»? Здесь это означало бы, что если бы вы сказали r1.tags.remove(t1), то вы сказали бы «флеш». SQLAlchemy увидит: «r1.tags, t1 был удален, и если он потерян, нам нужно удалить! Хорошо, поэтому давайте перейдем к «пометке», а затем сканируем всю таблицу на предмет любых записей. которые остаются." Делать это наивно для каждого тега за раз было бы явно неэффективно - если бы вы воздействовали на несколько сотен коллекций тегов в сеансе, было бы несколько сотен таких потенциально огромных запросов. Сделать это менее чем наивно было бы довольно сложным добавлением функции, поскольку единица работы имеет тенденцию думать с точки зрения одной коллекции за раз, и это все равно добавило бы ощутимые накладные расходы на запросы, которые люди могут не захотеть. Единица работы делает то, что делает действительно хорошо, но старается не вмешиваться в необычные пограничные случаи, которые добавляют много сложности и неожиданностей. На самом деле, система «удалить-сироту» вступает в игру только тогда, когда объект 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 (полагаясь на извлечение строк в память, когда база данных может выполнять операцию более эффективно, называется строка за мучительной строкой программирование). «НЕ СУЩЕСТВУЕТ» также очень хорошо работает при поиске отсутствия связанной строки по сравнению с ВНЕШНИМ СОЕДИНЕНИЕМ, которое в планировщике имеет тенденцию быть более дорогим.

person zzzeek    schedule 13.02.2012
comment
Отличный ответ. Спасибо за такое подробное и многословное объяснение и спасибо за рабочий код. Я рад узнать и понять больше SQLAlchemy на рабочем примере, потому что это трудно читать и получить все это из теории в документах. - person Honza Javorek; 18.03.2012
comment
продолжать хвалить :), действительно потрясающий ответ - person Humoyun Ahmad; 11.02.2017