SQLAlchemy: когда использовать пассивный_deletes="all" (с DELETE CASCADE в [например] PostgreSQL)Python

Программы на Python
Ответить
Anonymous
 SQLAlchemy: когда использовать пассивный_deletes="all" (с DELETE CASCADE в [например] PostgreSQL)

Сообщение Anonymous »

SQLAlchemy предоставляет несколько вариантов обработки "каскадных" удалений - насколько я могу судить, либо ORM пытается обрабатывать каскадирование самостоятельно (т. е. где пассивный_deletes=False [по умолчанию] и cascade="{...}, delete" установлен в определении отношения()), либо он может быть "информирован" о конфигурации ON DELETE CASCADE на стороне базы данных, указав пассивный_deletes=True, в котором ORM может откладывать некоторые удаления в БД.
Насколько я понимаю из документации, первый случай используется по умолчанию, поскольку безопаснее всего не предполагать ничего о каскадной конфигурации бэкэнда (а некоторые бэкэнды имеют ограниченную/необычную/отсутствующую поддержку этой функции). Последнее позволяет оптимизировать и/или использовать более сложные сценарии, например, перекладывая «далеко идущие» (как по объему затронутых строк, так и по глубине каскадирования/рекурсии) каскады на сторону базы данных. В этом случае при удалении «родительской» строки ORM выдает только явные инструкции DELETE для этой строки и любых объектов, которые она «загрузила», но полагается на БД для обработки любых других каскадов.
Существует третий вариант: пассивный_deletes="all". Мне не совсем понятно, каков предполагаемый вариант использования этой опции, хотя из того, что я смог собрать (и проверить в ходе тестирования), кажется, что ORM not выдает DELETE для дочерних элементов (загруженных или нет) при удалении родителя. Результирующий SQL-запрос такой же, как если бы пассивный_deletes не был указан (по умолчанию = False) AND cascade="{...}, delete" также опущен (т. е. ORM "наивен" для конфигурации БД) AND {выполнены некоторые другие условия - см. пример [A]}. В сценарии, где ON DELETE CASCADE указан (как известно) во внутренней БД, результирующее состояние БД (после удаления) также идентично в этих случаях, хотя я инстинктивно подскажу, что явно объявленное каскадное поведение требуется, чтобы ORM «знал» о каскадных удалениях - хотя я не мог создать тестовый сценарий, в котором это «имело бы значение» (т. е. я не мог принудительно вызвать исключение или сгенерировать некоторые противоречивый результат).
Я построил несколько тестовых примеров с различными комбинациями параметров и наблюдал как выдаваемый SQL, так и достигнутую БД. Я не буду включать все это, так как результаты имеют некоторую избыточность, но ключевые примеры таковы (некоторые шаблоны SQLAlchemy опущены согласно их документации)
Инициализируйте БД с помощью чистого SQL — я использую PostgreSQL.

Код: Выделить всё

CREATE TABLE IF NOT EXISTS parent (
id int PRIMARY KEY
);

CREATE TABLE IF NOT EXISTS  child (
id int PRIMARY KEY,
parent_id int REFERENCES parent ON DELETE CASCADE
);

INSERT INTO parent VALUES (1);
INSERT INTO child VALUES (1, 1);
INSERT INTO child VALUES (2, 1);
INSERT INTO child VALUES (3, 1);
Случай A (контроль) – нет информации о каскаде, нет связи(), объявлен только ForeignKey()

Код: Выделить всё

class Parent(Base):
__tablename__ = "parent"
id: Mapped[int] = mapped_column(primary_key=True)
class Child(Base):
__tablename__ = "child"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
p1 = session.get(Parent, 1)
session.delete(p1)
session.commit()
Это приводит к каскадному удалению родительского элемента и всех дочерних элементов — созданного SQL только родительского объекта DELETE, причем каскад обрабатывается БД.
Случай B — добавление отношения()

Код: Выделить всё

class Parent(Base):
__tablename__ = "parent"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[list["Child"]] = relationship()
# (...  as above)
На этот раз родительский_id имеет (явное) значение NULLd для каждого дочернего элемента — как и ожидалось для поведения ORM по умолчанию.
Случай C — добавьте cascade="all, delete" в Relations()

Код: Выделить всё

# (...)
children: Mapped[list["Child"]] = relationship(cascade="all, delete")
# (...)
Теперь ORM выдает DELETE для дочерних элементов через WHERE родительский.id=1.
Случай D — добавьте пассивный_deletes=True

Код: Выделить всё

# (...)
children: Mapped[list["Child"]] = relationship(
cascade="all, delete", passive_deletes=True)
# (...)
Теперь ORM выдает тот же SQL-код, что и в случае A, хотя, по-видимому, на этот раз он «осведомлен» об удалении дочерних элементов (как/где/почему это имеет значение?)
Случай E — «Загрузить» дочерний элемент перед удалением

Код: Выделить всё

# (...)
p1 = session.get(Parent, 1)
c2 = p1.children[1]
session.delete(p1)
# (...)
Этот результат аналогичен случаю C, однако вместо использования WHERE родительский.id=1 ORM использует WHERE child.id = %(id)s::INTEGER ; [{'id': 1}, {'id': 2}, {'id': 3}].
Случай F – измените пассивный_deletes=True на пассивный_deletes="all"
Это вызывает исключение в строке p1 = session.get(Parent, 1), чтение "не может быть установлено пассивный_deletes='all' в сочетании с каскадом 'delete' или 'delete-orphan'"; Означает ли это, что использование пассивного_deletes='all' предназначено для случаев, когда БД (и только БД) обрабатывает каскадирование, и поэтому его нельзя настраивать в Relations()?
Случай G - удалите cascade="all, delete" из Relations()
Теперь выдаваемый SQL соответствует A (и D), хотя я не знаю, как проверить «осведомленность» ORM в каждом из этих трех случаев, чтобы определить различия (и, следовательно, вариант использования/лучшие практики в отношении пассивного_deletes="all").
Некоторые примечания/предостережения

[*]Я нашел этот комментарий, который может указывать на то, что он устарел/устарел: https://github.com/sqlalchemy/sqlalchem ... -441935264.

[*]В документации говорится, что это «отключает «обнуление» дочерних внешних ключей», и что «это очень особая настройка варианта использования», хотя я не могу ее придумать



Подробнее здесь: https://stackoverflow.com/questions/785 ... in-e-g-pos
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Python»