← Назад к вопросам
Что можно написать, когда одна таблица ссылается на другую с помощью on_delete?
2.0 Middle🔥 241 комментариев
#Python Core#Soft Skills
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
on_delete в Foreign Key: все варианты
Вопрос о том, что можно использовать в параметре on_delete когда одна таблица ссылается на другую. Разберу все возможные стратегии и когда их использовать.
Что такое on_delete?
on_delete — это параметр Foreign Key, который определяет, что произойдёт с дочерней записью, когда удалят родительскую запись.
Пример в SQL
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE
-- ON DELETE CASCADE — одна из стратегий
);
1. CASCADE (Каскадное удаление)
Описание
# Если удалить родителя, удаляются все дочерние записи
from sqlalchemy import ForeignKey, Column, Integer, String
from sqlalchemy.orm import relationship, DeclarativeBase
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
# При удалении пользователя удалятся ВСЕ его заказы
orders = relationship('Order', cascade='all, delete-orphan')
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
amount = Column(Integer)
SQL
ALTER TABLE orders ADD CONSTRAINT orders_user_fk
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- Результат:
DELETE FROM users WHERE id = 1;
-- Автоматически удалятся все orders с user_id = 1
Когда использовать CASCADE
cascade_use_cases = {
"ownership_relationship": "Заказы принадлежат пользователю",
"composition": "Order Item's должны существовать только с Order",
"temporary_data": "Временные данные, зависящие от основных",
"comments_on_posts": "Комментарии принадлежат посту",
"example": {
"User delete": "Delete User → Delete all User's Orders",
"Post delete": "Delete Post → Delete all Post's Comments"
}
}
Реальный пример
class BlogPost(Base):
__tablename__ = 'blog_posts'
id = Column(Integer, primary_key=True)
title = Column(String(200))
author_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
# При удалении автора удалятся все его посты
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
content = Column(String(1000))
post_id = Column(Integer, ForeignKey('blog_posts.id', ondelete='CASCADE'))
# При удалении поста удалятся все его комментарии
2. SET_NULL (Установить NULL)
Описание
# Если удалить родителя, в дочерних записях внешний ключ становится NULL
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
customer_id = Column(
Integer,
ForeignKey('customers.id', ondelete='SET NULL'),
nullable=True # Важно! Должно быть nullable
)
amount = Column(Integer)
# Результат:
# DELETE FROM customers WHERE id = 1;
# У all orders будет customer_id = NULL
SQL
ALTER TABLE orders ADD CONSTRAINT orders_customer_fk
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL;
Когда использовать SET_NULL
set_null_use_cases = {
"optional_relationship": "Необязательная связь",
"referrer_deletion": "Удаление referrer, но data сохраняется",
"supervisor": "Сотрудник может потерять supervisor",
"affiliate": "Affiliate link, но user record живёт",
"example": {
"Delete affiliate": "Affiliate deleted → user.affiliate_id = NULL",
"Delete supervisor": "Supervisor deleted → employee.supervisor_id = NULL"
}
}
Реальный пример
class Employee(Base):
__tablename__ = 'employees'
id = Column(Integer, primary_key=True)
name = Column(String(100))
supervisor_id = Column(
Integer,
ForeignKey('employees.id', ondelete='SET NULL'),
nullable=True
)
# Если supervisor удалён, employee остаётся, но supervisor_id = NULL
class AffiliateLink(Base):
__tablename__ = 'affiliate_links'
id = Column(Integer, primary_key=True)
user_id = Column(
Integer,
ForeignKey('users.id', ondelete='SET NULL'),
nullable=True
)
# Если user удалён, link живёт с user_id = NULL
3. SET_DEFAULT (Установить значение по умолчанию)
Описание
# Если удалить родителя, в дочерних записях устанавливается дефолтное значение
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
category_id = Column(
Integer,
ForeignKey('categories.id', ondelete='SET DEFAULT'),
default=1 # Default category ID
)
# Если категория удалена, все статьи переходят в default категорию
SQL
ALTER TABLE articles ADD CONSTRAINT articles_category_fk
FOREIGN KEY (category_id) REFERENCES categories(id)
ON DELETE SET DEFAULT;
-- Результат:
DELETE FROM categories WHERE id = 5;
-- У articles с category_id = 5 будет category_id = 1 (default)
Когда использовать SET_DEFAULT
set_default_use_cases = {
"default_category": "Статьи переходят в default категорию",
"fallback_option": "Нужно fallback значение",
"always_need_value": "Поле не может быть NULL",
"example": {
"Delete category": "All articles → category_id = default_category_id"
}
}
4. RESTRICT (Запретить удаление)
Описание
# Если есть дочерние записи, удаление родителя запрещено
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
user_id = Column(
Integer,
ForeignKey('users.id', ondelete='RESTRICT'),
nullable=False
)
# Если у пользователя есть заказы, удалить пользователя нельзя
SQL
ALTER TABLE orders ADD CONSTRAINT orders_user_fk
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT;
-- Результат:
DELETE FROM users WHERE id = 1;
-- Ошибка: FOREIGN KEY constraint failed (если есть orders)
Когда использовать RESTRICT
restrict_use_cases = {
"protection": "Защита от случайного удаления",
"integrity": "Ничего не должно зависеть от deleted record",
"explicit_cleanup": "Требуется явная очистка",
"audit_trail": "Нужно сохранить историю",
"example": {
"active_orders": "Нельзя удалить пользователя с active orders"
}
}
Реальный пример
class Department(Base):
__tablename__ = 'departments'
id = Column(Integer, primary_key=True)
name = Column(String(100))
class Employee(Base):
__tablename__ = 'employees'
id = Column(Integer, primary_key=True)
name = Column(String(100))
department_id = Column(
Integer,
ForeignKey('departments.id', ondelete='RESTRICT')
)
# Нельзя удалить department, если в нём есть сотрудники
5. NO ACTION (Проверка при commit)
Описание
# Похоже на RESTRICT, но проверка при commit, а не сразу
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
user_id = Column(
Integer,
ForeignKey('users.id', ondelete='NO ACTION')
)
Разница между RESTRICT и NO ACTION
# RESTRICT — проверка сразу (immediate)
# NO ACTION — проверка при commit (deferred)
# На практике похожи, но NO ACTION допускает:
# 1. Удалить parent
# 2. Обновить child
# 3. Commit
# Работает если дочерний record обновляется в same transaction
6. SET (Установить значение из функции)
Описание
# Установить значение, вычисленное функцией (редко используется)
# Это более продвинутая опция, которая редко используется в ORMs
# но доступна в сыром SQL
# CREATE TRIGGER
CREATE OR REPLACE FUNCTION before_user_delete()
RETURNS TRIGGER AS $$
BEGIN
UPDATE orders SET user_id = NULL WHERE user_id = OLD.id;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER user_delete_trigger
BEFORE DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION before_user_delete();
Сравнительная таблица
comparison = {
"CASCADE": {
"effect": "Удаляет дочерние записи",
"use_case": "Ownership relationship",
"danger": "Высокая (потеря данных)",
"example": "User delete → Orders deleted"
},
"SET_NULL": {
"effect": "Обнуляет внешний ключ",
"use_case": "Optional relationship",
"danger": "Низкая",
"example": "Supervisor delete → employee.supervisor_id = NULL"
},
"SET_DEFAULT": {
"effect": "Устанавливает default значение",
"use_case": "Нужно default значение",
"danger": "Средняя",
"example": "Category delete → article.category_id = 1"
},
"RESTRICT": {
"effect": "Запрещает удаление",
"use_case": "Защита от случайного удаления",
"danger": "Низкая",
"example": "Dept with employees не удаляется"
},
"NO ACTION": {
"effect": "Как RESTRICT, но deferred",
"use_case": "Complex scenarios",
"danger": "Низкая",
"example": "Проверка при commit"
}
}
Практический выбор
# Как выбрать on_delete стратегию:
decision_tree = {
"Composition (has-many)": "CASCADE (Post → Comments)",
"Ownership (belongs-to, может быть NULL)": "SET_NULL (Employee → Supervisor)",
"Reference без NULL": "SET_DEFAULT или RESTRICT",
"Protection important": "RESTRICT (Department → Employees)",
"Need flexibility": "SET_NULL",
"Default fallback": "SET_DEFAULT"
}
Реальный пример: E-commerce
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric
from sqlalchemy.orm import relationship, DeclarativeBase
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100))
# Каскадное удаление всех связанных данных
orders = relationship('Order', cascade='all, delete-orphan')
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
user_id = Column(
Integer,
ForeignKey('users.id', ondelete='CASCADE') # CASCADE
)
items = relationship('OrderItem', cascade='all, delete-orphan')
class OrderItem(Base):
__tablename__ = 'order_items'
id = Column(Integer, primary_key=True)
order_id = Column(
Integer,
ForeignKey('orders.id', ondelete='CASCADE') # CASCADE
)
product_id = Column(
Integer,
ForeignKey('products.id', ondelete='RESTRICT') # Защита
)
class Product(Base):
__tablename__ = 'products'
id = Column(Integer, primary_key=True)
name = Column(String(200))
# Нельзя удалить product если на неё есть order_items
class Review(Base):
__tablename__ = 'reviews'
id = Column(Integer, primary_key=True)
user_id = Column(
Integer,
ForeignKey('users.id', ondelete='SET_NULL'), # SET_NULL
nullable=True
)
product_id = Column(
Integer,
ForeignKey('products.id', ondelete='CASCADE') # CASCADE
)
# Если user удалён, review остаётся (анонимный)
# Если product удалён, review удаляется
Заключение
Выбирайте on_delete в зависимости от семантики:
- CASCADE — дочерние зависят от родителя (comments → posts)
- SET_NULL — опциональная связь (supervisor, optional)
- SET_DEFAULT — нужно fallback значение
- RESTRICT — защита от случайного удаления
- NO ACTION — как RESTRICT, но deferred
Большинство случаев решаются CASCADE или SET_NULL.