Подтвердить что ты не робот

Установка delete-orphan в отношении SQLAlchemy вызывает AssertionError: этот атрибутImpl не настроен для отслеживания родителей

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

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

4b9b3361

Ответ 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 каскад delete-orphan не поддерживается для отношения "многие ко многим" или "многие к одному", если не указан 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 был удален, и если это сирота, нам нужно удалить! OK, так что давайте перейдем к" тегированию "и затем просканируем всю таблицу на наличие записей, которые оставаться ". Делать это наивно для каждого тега за раз было бы явно неэффективно - если бы вы затронули несколько сотен коллекций тегов в сеансе, было бы несколько сотен этих потенциально огромных запросов. Делать это менее наивно было бы довольно сложным добавлением функции, поскольку единица работы имеет тенденцию думать в терминах одной коллекции за раз - и это все равно добавляло бы ощутимые накладные расходы на запросы, которые люди могли бы на самом деле не хотеть. Единица работы делает то, что делает действительно хорошо, но старается держаться подальше от бизнеса необычных крайних случаев, которые добавляют много сложности и сюрпризов. На самом деле, система "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 (было вызвано обращение к извлечению строк в память, когда база данных может выполнять операцию более эффективно Строка путем мучительного программирования строки). "НЕ СУЩЕСТВУЕТ" работает очень хорошо при поиске отсутствия связанной строки, по сравнению с ВНЕШНИМ СОЕДИНЕНИЕМ, которое в планировщике обходится дороже.