Установка удаления-сироты для отношения SQLAlchemy приводит к AssertionError: Этот AttributeImpl не настроен для отслеживания родителей - PullRequest
25 голосов
/ 11 февраля 2012

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

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

1 Ответ

69 голосов
/ 13 февраля 2012

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

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

...