← Назад к вопросам

Что можно написать, когда одна таблица ссылается на другую с помощью 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.