Published on

django many to many 관계와 중간테이블 구조

Authors

저번 글에서 ManyToManyField를 통해 여러 개의 관계를 가질 수 있는 다대다(Many-to-Many) 관계를 쉽게 설정하는 메서드를 알아봤습니다.

이번 글에서는 ManyToManyField의 동작 방식과 중간 테이블의 개념을 설명하고, 왜 해당 모델의 ManyToManyField 에서 옵션으로 null=True가 의미없는지 기록합니다.

1. ManyToMany 기본 모델

여기서 Course 모델의 students 필드는 여러 학생과의 다대다 관계를 설정합니다.

(한 코스에서 여러학생이 들을 수 있고, 한 학생이 여러 코스의 수업을 들을 수 있는 개념입니다.)

class Student(models.Model):
    name = models.CharField(max_length=100)

class Course(models.Model):
    name = models.CharField(max_length=100)
    students = models.ManyToManyField(Student)

2. 중간 테이블의 개념

Django는 ManyToManyField를 사용하여 두 모델 간의 관계를 설정할 때 자동으로 중간 테이블을 생성합니다.

중간 테이블은 두 모델(Student, Course) 간의 관계를 관리하는 실제 데이터베이스 테이블 입니다.

따로 생성하지 않아도, 내부적으로 course_students 라는 이름의 중간 테이블을 생성합니다.

이 중간 테이블은 두 모델의 Primary Key 값만을 저장합니다.

해당 테이블은 아래 SQL 문을 통해 생성되는 구조입니다.


CREATE TABLE course_students (
    id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    course_id INTEGER NOT NULL,
    student_id INTEGER NOT NULL,
    FOREIGN KEY (course_id) REFERENCES course(id),
    FOREIGN KEY (student_id) REFERENCES student(id)
);

  • course_id: Course 모델의 기본 키(Primary Key).
  • student_id: Student 모델의 기본 키(Primary Key).
  • 이 두 외래 키가 ManyToMany 관계를 관리하는 방식입니다.
  • 참고)PostgreSQL 10 이상에서는 GENERATED BY DEFAULT AS IDENTITY 를 통해 PK(ID)를 지정할 수 있고, 이전에는 SERIAL 가 사용되었다고 합니다.

이 중간 테이블에서 서로 어떤 PK 가 어떤 PK 와 관계 되어있는지 알기 때문에, 연결(묶음)이 되는 것 입니다.

3. 왜 null=True 가 의미가 없는가?

일반적으로 Django 모델 필드에 null=True 옵션을 설정하면 해당 필드에 NULL 값을 허용합니다.

some_field = models.OneToOneField(…생략…, null=True)

하지만 ManyToManyField는 중간 테이블을 사용하여 관계를 저장하기 때문에 필드에 NULL 값을 허용할 수 없습니다.

중간 테이블의 외래 키는 항상 두 모델 간의 유효한 관계를 가져야 하기 때문에, NULL 값은 해당 관계가 성립하지 않음을 의미하게 됩니다.

따라서 null=TrueManyToManyField에서 아무런 의미가 없으며, Django는 이 옵션을 허용하지 않습니다.

3.1 [System Warning] null has no effect on ManyToManyField

null=TrueManyToManyField 에 설정하고 서버를 run 하게 되면, system에서 모델에 대한 validation을 해주고 아래처럼 [Warning]System check identified some issues을 통해 알려줍니다.

참고로 model migration 을 해도 문제 없습니다. 하지만 의미가 없으니 하지 않는게 맞겠습니다.

System check identified some issues:

WARNINGS:
some_app.SomeModel.work_plans: (fields.W340) null has no effect on ManyToManyField.
null has no effect on ManyToManyField 참고이미지

3.2 blank=True 의 역할

반면, blank=True폼 레벨에서의 유효성 검사와 관련이 있습니다.

이는 해당 필드를 폼에서 비워둘 수 있는지를 결정합니다.

즉, ManyToManyField가 있는 폼에서 필드를 비워도 관계 설정이 이루어지지 않을 뿐, 데이터베이스 상에서 문제가 발생하지는 않습니다.


class Course(models.Model):
    name = models.CharField(max_length=100)
    students = models.ManyToManyField(Student, blank=True)

위 코드에서 blank=True를 설정함으로써, 폼에서 students 필드를 비워두는 것이 허용됩니다.

4. 중간 테이블 커스텀 하기

때로는 중간 테이블에 추가 정보를 저장하고 싶을 수 있습니다. 이 경우 Django에서 직접 중간 테이블을 정의할 수 있습니다.

예를 들어, 학생이 강의를 언제 수강했는지 기록하기를 원하는 상황이라면, 아래처럼 중간 테이블을 명시적으로 정의할 수 있습니다.


class Enrollment(models.Model): # 등록(기록)
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    enrollment_date = models.DateField()

class Course(models.Model):
    name = models.CharField(max_length=100)
    students = models.ManyToManyField(Student, through='Enrollment')

이 방식에서는 중간 테이블인 Enrollment 모델이 추가 정보(enrollment_date)를 담고 있으며, 두 모델 간의 관계를 직접적으로 관리하게 됩니다.

5. 레퍼런스 링크

hongreat 블로그의 글을 봐주셔서 감사합니다!