언어, 프레임워크/Python & Django

[Django] ForeignKey, ManyToManyField, OneToOneField 😑

jaee 2025. 1. 12. 20:00

 

틀린 내용이 있을 수 있습니다.
발견하시면 말씀 부탁드립니다! 🙇


 

이전 회사에서는 ORM을 사용하지 않았는데 현재 다니는 회사에서는 ORM을 사용 중이다. 맨 처음 개발 공부할 때 node.js ORM인 Sequelize를 사용했던걸 제외하면 ORM 사용은 처음이라 번거롭고 불편했다(솔직히 지금도 가끔 불편함). 어쩌다보니 불평으로 포스팅 시작😬. 어찌 됐든 Django ORM을 사용할 때 필수로 알아야 하는, 모델 간 관계를 표현할 때 사용하는 3가지 필드(Relationship Field)들에 대해 정리해 보겠다. (장고 5.2 기준)

 

1. ForeignKey 

ForeingKey 필드는 모델들이 ManyToOne 관계일 때 사용한다. 먼저 ForeignKey를 사용할 때 필요한 필수 인자부터 확인해 보자.

class ForeignKey(to, on_delete, **options)

ForeignKey는 총 2개의 필수 인자를 받고 있다. 첫 번째 인자(to)는 부모 모델을 의미한다. 그리고 두 번째 인자(on_delete)는 외래키가 참조하는 값이 삭제될 때 연관 모델 값을 어떻게 처리할지에 대한 설정값이며, 좀 더 풀어서 설명하자면 부모가 삭제될 때 자식을 어떻게 처리할지에 대한 설정이다.

 

이제 장고 공식 문서에 나와있는 예시 코드를 통해 ForeignKey로 엮인 모델 간의 관계를 이해해 보자.

from django.db import models


class Manufacturer(models.Model):
    name = models.TextField()

class Car(models.Model):
    manufacturer = models.ForeignKey(
    	Manufacturer, 
        on_delete=models.CASCADE,
        related_name='cars'
    )

일단 Car 모델에 manufacturer이라는 이름을 가진 필드는 "ForeignKey"로 정의된 외래키 필드이다. ForeignKey 첫 번째 인자로 Manufacturer 모델이 들어가 있으므로 Car 모델은 Manufacturer 모델을 참조하는 형태이다.  ForeignKey로 정의된 필드가 있는 모델(Car)이 자식 모델, ForeignKey의 첫 번째 인자로 들어온 모델(Manufacturer)이 부모 모델이며, 부모는 여러 개의 자식을 가질 수 있는 반면 자식은 하나의 부모만 가질 수 있기 때문에 ManyToOne(N:1) 관계고 이해하면 된다.

 

위에서 related_name 옵션을 사용했는데 related_name은 역참조용 이름을 정의하는 옵션이라고 생각하면 된다.

# related_name을 사용 안 했을때
manufacturer = Manufacturer.objects.get(id=1)
car = manufacturer.car_set.all()

# related_name을 사용 했을때
manufacturer = Manufacturer.objects.get(id=1)
car = manufacturer.cars.all()

 

그럼 부모 모델 객체를 삭제할 시 자식 모델 객체에는 어떤 영향이 있을까? (참고로 자식 모델 객체를 삭제해도 부모 모델 객체에는 아무 영향이 없다)건 ForeignKey의 두 번째 인자인 on_delete 설정에 따라 결정되며 총 7개로 구분된다. 여기 중에서 작업하며 주로 사용하는 건 CASCADE, PROTECT, SET_NULL이고, 구글링을 했을 때도 이 3가지가 주로 사용된다고 한다.

설정값 설명
CASCADE 부모가 삭제되면 자식도 같이 삭제
PROTECT 자식이 존재하면 부모는 삭제 불가. 삭제 시도시 ProtectedError 발생
SET_NULL 부모가 삭제되면 자식은 NULL로 처리
SET_DEFAULT 부모가 삭제되면 자식은 기본값으로 처리
SET() 부모가 삭제되면 SET() 인자로 들어간 값으로 처리 (특정값 또는 callable한 객체)
DO_NOTHING 부모가 삭제돼도 아무런 액션을 하지 않음
RESTRICT PROTECT와 비슷하게 자식이 존재하면 부모 삭제 불가. 삭제 시도시 RestrictedError 발생

PROTECT와 RESTRICT의 역할 차이점이 애매한데, 장고 공식 문서에 나와있는 PROTECT와 RESTRICT의 차이점을 보면 PROTECT와 달리 RESTRICT를 사용하는 경우 참조된 객체가 다른 객체를 참조하고 있고, 이 다른 객체가 동일한 연산(same operation)에서 CASECADE로 삭제되는 경우에 삭제가 허용된다고 한다.

Unlike PROTECT, deletion of the referenced object is allowed if it also references a different object that is being deleted in the same operation, but via a CASCADE relationship.

 

2. OneToOneField

모델끼리 1:1 관계임을 나타낼 때 OneToOneField를 사용하며 모델 확장이 필요할 때 유용하게 이용할 수 있다. OneToOneField는 옵션값 중에 parent_link가 있는 것을 제외하면 ForeignKey와 동일한 인자를 받는다.

class OneToOneField(to, on_delete, parent_link=False, **options)

 

공식 문서에서 가져온 예시 코드를 보자. 아래 코드에서는 MySpecialUser 모델을 장고에서 기본으로 제공하는 User 모델과 OneToOneField로 연관 지음으로써 모델을 확장했다. 

from django.conf import settings
from django.db import models


class MySpecialUser(models.Model):
    worker = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )
    supervisor = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="supervisor_of",
    )

 

3. ManyToManyField

이름에서 알 수 있듯 모델 간 ManyToMany(N:M) 관계인 경우 사용하며 필수 인자는 연관 모델이다.

class ManyToManyField(to, **options)

 

ManyToManyField는 연관된 모델 중 어디에 선언해도 사용하는데 문제는 없지만, 논리적 주체가 되는 모델에 ManyToManyField를 사용하는 게 일반적이다. 아래 예시에서는 Paper 모델에 ManyToManyField를 사용했는데 왜냐하면 '보통은 논문의 저자가 누구인지를 확인' = '논문이 주체'라고 생각했기 때문이다.

from django.db import models

# 저자
class Author(models.Model):
    name = models.CharField(max_length=100)

# 논문
class Paper(models.Model):
    name = models.CharField(max_length=20)
    authors = models.ManyToManyField(Author)

 

ManyToManyField로 모델 관계를 정의하면 매개 역할을 하는 중간 테이블이 자동으로 생성된다. 자동으로 생성된 중간 테이블에는 모델 간의 관계 정보 외에 다른 정보는 저장되지 않는다. 만약 중간 테이블에 두 모델의 관계뿐만 아니라 다른 부가 정보도 추가하고 싶다면 아래 예시처럼 through 옵션을 통해 중간 테이블을 명시할 수 있다.

from django.db import models

# 저자
class Author(models.Model):
    name = models.CharField(max_length=100)

# 논문
class Paper(models.Model):
    name = models.CharField(max_length=20)
    authors = models.ManyToManyField(
    	Author,
        through='PaperAuthor',  # 중간 테이블 명시함
    )

# Paper-Author 중간 테이블 
class BookAuthor(models.Model):
    paper = models.ForeignKey(Paper, on_delete=models.CASCADE)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    contribution_percentage = models.IntegerField(default=100)

 

 

 

 


참고 자료