매개변수
on_delete
- Django 모델에서 외래 키(ForeignKey)나 일대일 필드(OneToOneField)를 정의할 때
- 참조하고 있는 부모 객체가 삭제될 경우 연결된 자식 객체들을 어떻게 처리할지 결정하는 필수 매개변수
CASCADE
부모가 삭제되면, 연관된 자식도 전부 삭제된다.
- 사용처: 게시글이 삭제되면 해당 게시글의 댓글도 모두 삭제되어야 할 때 주로 사용
user = models.ForeignKey(User, on_delete=models.CASCADE)
- 1. User가 삭제되면 → 해당 User의 Account/Transaction 등 모두 삭제됨.
- 2. 가장 위험하지만, 가장 많이 쓰임.
PROTECT
참조된 객체를 삭제하려고 할 때, 참조하는 객체가 하나라도 존재하면 삭제를 막고 ProtectedError 예외를 발생시킴
- 사용처: 결제 내역이 있는 사용자를 실수로 삭제하는 것을 방지할 때 유용
bank = models.ForeignKey(Bank, on_delete=models.PROTECT)
- 1. Bank에 연결된 Account가 하나라도 있으면 → Bank 삭제 시도 → 에러 발생 (ProtectedError)
- 2. 실수로 중요한 데이터가 삭제되는 것을 방지할 때 사용.
SET_NULL
부모가 삭제되면 자식의 ForeignKey 값을 NULL로 변경
- 이 옵션을 사용하려면 모델 필드에 반드시
null=True 설정이 포함되어야 함
- 사용처: 작성자가 탈퇴하더라도 그 작성자가 쓴 글은 "알 수 없음" 상태로 남겨두고 싶을 때 사용
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
- 1. Category 삭제 → Transaction.category = NULL로 변경
- 2. Transaction 자체는 삭제되지 않음 / 이 옵션은 null=True가 반드시 필요함
SET_DEFAULT
부모가 삭제되면 자식의 ForeignKey 값을 default 값으로 변경
- 필드에 default 값이 설정되어 있어야 함
- 사용처: 카테고리가 삭제되었을 때 해당 글들을 기본 카테고리(예: '미분류')로 이동시킬 때 사용
category = models.ForeignKey(
Category,
on_delete=models.SET_DEFAULT,
default=1
)
- 1. Category(3번)가 삭제됨 → 해당 카테고리 사용하던 Transaction은 모두 category=1(기본값)로 변경됨.
SET()
부모 삭제 시, 특정 함수나 값을 직접 지정해서 설정
- 사용처: 삭제된 사용자의 글을 특정 '탈퇴한 사용자 전용 더미 계정'에 할당할 때 사용
def get_deleted_category():
return Category.objects.get(name="삭제됨")
category = models.ForeignKey(
Category,
on_delete=models.SET(get_deleted_category)
)
- 1. Category가 삭제되면 category가 “삭제됨”이라는 카테고리로 자동 변경됨.
- 2. 데이터 복구/추적을 위해 자주 쓰는 방법.
DO_NOTHING
부모 삭제 시 자식에게 아무 처리도 하지 않음
- 주의
- 데이터베이스 백엔드에서 참조 무결성을 강제하고 있다면, 데이터베이스 수준에서 IntegrityError가 발생 가능
- 매우 특수한 상황이 아니라면 사용을 권장하지 않음
user = models.ForeignKey(User, on_delete=models.DO_NOTHING)
- 1. User 삭제 → Transaction에 user_id가 남아있음 → FK 제약 때문에 DB 오류 발생 가능
- 2. 거의 사용하지 않음 (에러 위험)
RESTRICT (Django 3.1+)
부모가 삭제되기 전에 연관된 자식이 있으면 삭제를 금지
- PROTECT와 유사하게 RestrictedError를 발생시켜 삭제를 막지만
- 다른 CASCADE 관계에 의해 함께 삭제되는 상황에서는 삭제를 허용
product = models.ForeignKey(Product, on_delete=models.RESTRICT)
- 1. PROTECT와 비슷하지만 예외 메시지가 더 명확함
- 2. Product가 사용 중이면 삭제 불가 / PROTECT보다 더 명확한 제어 가능
예시
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
def get_deleted_user_instance():
return User.objects.get_or_create(username='deleted_user')[0]
class Category(models.Model):
name = models.CharField(max_length=100)
class Article(models.Model):
"""
CASCADE 예시:
작성자(User)가 삭제되면 이 게시글(Article)도 데이터베이스에서 함께 삭제됩니다.
"""
author = models.ForeignKey(
User,
on_delete=models.CASCADE
)
title = models.CharField(max_length=200)
content = models.TextField()
class Comment(models.Model):
"""
SET_NULL 예시:
게시글(Article)이 삭제되더라도 댓글 데이터 자체는 남기고 싶을 때 사용합니다.
article_id 값은 비워집니다(NULL). null=True 설정이 필수입니다.
"""
article = models.ForeignKey(
Article,
on_delete=models.SET_NULL,
null=True
)
text = models.CharField(max_length=500)
class Payment(models.Model):
"""
PROTECT 예시:
결제 내역은 법적 보존 의무가 있을 수 있으므로,
Payment 객체를 가진 User를 삭제하려고 시도하면 에러를 발생시키고 삭제를 차단합니다.
"""
user = models.ForeignKey(
User,
on_delete=models.PROTECT
)
amount = models.DecimalField(max_digits=10, decimal_places=2)
class Document(models.Model):
"""
SET() 예시:
문서의 소유자가 삭제되었을 때, 문서를 삭제하거나 NULL로 두는 대신
사전에 정의한 특정 사용자(get_deleted_user_instance)에게 소유권을 넘깁니다.
"""
owner = models.ForeignKey(
User,
on_delete=models.SET(get_deleted_user_instance)
)
file_path = models.CharField(max_length=255)
참조 무결성
정의
- 외래 키(Foreign Key) 값이 항상 유효한 부모 테이블의 기본 키(Primary Key)를 가리켜야 한다는 DB규칙
왜 필요?
- 만약 Article(게시글) 테이블이 User(사용자) 테이블의 특정 ID를 참조하고 있는데
- 해당 사용자가 삭제되어 데이터베이스에 존재하지 않게 된다면 데이터의 모순(고아 레코드 발생)이 생김
Django의 역할
- Django의
on_delete 매개변수는 바로 이 데이터베이스 레벨의
- 참조 무결성 제약 조건(Constraint)을 애플리케이션 레벨에서 어떻게 안전하게 처리할 것인지 정의하는 도구
역참조
정의
- Django에서
ForeignKey를 정의하면
- 기본적으로 '자식 객체(참조하는 쪽)'에서 '부모 객체(참조되는 쪽)'로 접근할 수 있음
- 예: article.author
- 하지만 그 반대 방향인 '부모 객체'에서 '자식 객체들'을 조회하는 것을 역참조(Reverse Relation)라고 함
모델 예시
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Article(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
class Comment(models.Model):
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='comments')
text = models.CharField(max_length=500)
실무에서의 데이터 조회(Query) 예시
my_user = User.objects.get(id=1)
user_articles = my_user.article_set.all()
my_article = Article.objects.get(id=1)
article_comments = my_article.comments.all()
옵션
null=True
검사 기준: 데이터베이스 스키마
동작 계층 (Layer): Database
설명
- 데이터베이스 레벨에서 해당 컬럼에 NULL 값(빈 값)이 저장되는 것을 허용할지 결정함
- 문자열 기반 필드(CharField, TextField)에서는 사용을 지양함
- 빈 문자열 ''과 NULL 두 가지 빈 상태를 가지게 되기 때문
blank=True
동작 계층 (Layer): Application
설명
- 애플리케이션(Django Form, Admin) 레벨에서 입력 폼을 비워두는 것을 허용할지 결정
- 데이터베이스 스키마에는 영향을 주지 않음
- ForeignKey(또는 OneToOne, ManyToMany) 를 통해 역참조 할 때 사용하는 이름을 직접 지정하는 옵션
- 즉, "부모 → 자식" 방향의 조회 이름을 바꿔주는 것
사용하는 이유
- Django는 ForeignKey를 만들면 자동으로 역참조 이름을 만들어 주는데
- related_name을 사용하면 역참조 이름을 훨씬 직관적이고 깔끔하게 만듬
class Transaction(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE)
- 1. 이 경우 Django는 자동으로 account.transaction_set 이라는 이름을 만들어줌
- 2. 조회방법: account.transaction_set.all()
class Transaction(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE,
related_name="transactions")
- 1. 역참조 이름을 깔끔하게 만들어서 아래 처럼 사용 가능
- 2. account.transactions.all()
- 동일 모델에 여러 ForeignKey가 있을 때 충돌을 피함
class Message(models.Model):
sender = models.ForeignKey(User, on_delete=models.CASCADE)
receiver = models.ForeignKey(User, on_delete=models.CASCADE)
- 1. 이렇게 사용시 에러 발생
- Django는 자동으로 user.message_set을 만듬
- 하지만, sender도 user고 receiver도 user라서 충돌이 발생
class Message(models.Model):
sender = models.ForeignKey(User, on_delete=models.CASCADE,
related_name="sent_messages")
receiver = models.ForeignKey(User, on_delete=models.CASCADE,
related_name="received_messages")
- 1. 이렇게 사용하면
- 2. user.sent_messages.all() / user.received_messages.all()
- Serializer, View에서 직관적 코드 작성 가능
user.transactions.filter(...) = 의미가 바로 전달
user.transaction_set.filter(...) = 직관성이 확 떨어짐
class Transaction(models.Model):
account = models.ForeignKey(
Account,
on_delete=models.PROTECT,
related_name="transactions",
)
- 1. account.transactions.all() 로 최근 거래 조회 가능
verbose_name
- 모델 필드(또는 모델 전체)의 “사람이 읽기 쉬운 이름”을 지정하는 옵션
- 개발자가 쓰는 “코드 이름(field name)” 이 아닌 관리자(admin)나 폼(form)에서 표시할 때 사용
created_at = models.DateTimeField(auto_now_add=True)
- 1. Admin 화면: Created at
- 자동으로 snake_case → Title Case 로 바꿔서 보임
created_at = models.DateTimeField("생성 날짜", auto_now_add=True)
- 1. Django admin이나 폼: 생성 날짜
비유
- '작가(Author)'와 그 작가가 쓴 '책(Book)'의 관계
- 정방향 (Forward): 책 ➔ 작가
- 책의 입장에서 "내 작가가 누구지?" 하고 찾는 것은 쉬움
- Book 모델 안에 author라는 필드(외래 키)가 있기 때문 (book.author)
- 역방향 (Reverse): 작가 ➔ 책
- 작가의 입장에서 "내가 쓴 책이 뭐뭐 있지?" 하고 찾으려고 보면 문제가 생김
- Author 모델 안에는 책과 관련된 필드가 전혀 적혀있지 않기 때문
- 이때 Django ORM은 작가가 자신의 책들을 찾을 수 있도록 마법처럼 보이지 않는 '뒷문'을 하나 만들어 줌
- 이 뒷문의 이름표를 개발자가 직접 예쁘게 지어주는 것이 바로 related_name
정의
- Django 공식 문서의 ForeignKey.related_name 항목에서는 이를 다음과 같이 정의
- "The name to use for the relation from the related object back to this one."
- 관계된 객체(부모)에서 현재 객체(자식)로 되돌아오는 역참조 관계에 사용할 이름
- 개발자가 이 이름표(related_name)를 직접 지어주지 않으면
- Django는 규칙에 따라 임의로 이름표를 달아버림
그 규칙이 바로 참조하는_모델명소문자_set (예: book_set)
예시
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Book(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
jk_rowling = User.objects.get(name='J.K. Rowling')
her_books = jk_rowling.book_set.all()
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Book(models.Model):
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='books'
)
title = models.CharField(max_length=200)
jk_rowling = User.objects.get(name='J.K. Rowling')
her_books = jk_rowling.books.all()
하나의 모델 안에서 같은 모델을 두 번 이상 외래 키로 참조할 때
class Post(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='written_posts'
)
editor = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='edited_posts'
)
- Django는 author의 역참조 이름도 post_set으로 만들려 하고
- editor의 역참조 이름도 post_set으로 만들려다가 "이름이 충돌했습니다!" 라며
- SystemCheckError를 발생시키고 실행을 멈춤
- 따라서 각각의 관계가 어떤 의미인지 related_name을 통해 명확히 분리해 주어야 함