[Django] Modeling - Foreignkey & ManyToMany

해당 글은 장고 문서를 참고하여 작성했습니다.
참고 : https://docs.djangoproject.com

ManyToMany 필드

단순 ManyToMany 관계

manytomany인 다대다 관계의 예로를 pizzatopping을 들 수 있다. 다음은 단순 다대다 관계를 many_to_many패키지의 models.py에서 모델링한 것이다.

from django.db import models


class Topping(models.Model):
  name = models.CharField(max_length=50)

  def __str__(self):
    return self.name


class Pizza(models.Model):
  name = models.CharField(max_length=50)
  toppings = models.ManyToManyField(
    Topping,
    related_name='pizzas',
  )

  def __str__(self):
    return self.name

그리고 터미널창에 ./manage.py makemigrations, ./manage.py migrate해보자
정상으로 migrate되었다면, ./manage.py shellipython을 실행시켜서 동작시켜보자

In [1]: from models.many_to_many.models import Topping, Pizza

In [2]: Pizza.objects.create(name='페퍼로니')
Out[2]: <Pizza: 페퍼로니>

In [3]: Pizza.objects.create(name='콤비네이션')
Out[3]: <Pizza: 콤비네이션>

In [4]: Pizza.objects.create(name='치즈')
Out[4]: <Pizza: 치즈>

In [5]: Pizza.objects.create(name='슈퍼슈프림')
Out[5]: <Pizza: 슈퍼슈프림>

In [6]: 페퍼로니, 콤비네이션, 치즈, 슈퍼슈프림 = Pizza.objects.all()

In [7]: 페퍼로니
Out[7]: <Pizza: 페퍼로니>

...

In [10]: Topping.objects.create(name='햄')
Out[10]: <Topping: 햄>

In [11]: Topping.objects.create(name='치즈')
Out[11]: <Topping: 치즈>

In [12]: Topping.objects.create(name='페퍼로니햄')
Out[12]: <Topping: 페퍼로니햄>

In [13]: Topping.objects.create(name='피망')
Out[13]: <Topping: 피망>

In [14]: Topping.objects.create(name='올리브')
Out[14]: <Topping: 올리브>

In [15]: 햄, 피자치즈, 페퍼로니햄, 피망, 올리브 = Topping.objects.all()

In [16]: 햄
Out[16]: <Topping: 햄>

위처럼 Topping과 Pizza 종류를 만들고 변수명을 지정해주었다. 그리고 ManyToManyField로 지정해준 변수 toppings를 터미널창에서 사용해보자.

In [37]: 치즈.toppings
Out[37]: <django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager at 0x7fe7540d2898>

In [38]: 치즈.toppings.all()
Out[38]: <QuerySet []>

In [39]: 치즈.toppings.add(피자치즈)

In [40]: 치즈.toppings.all()
Out[40]: <QuerySet [<Topping: 치즈>]>

피자의 toppings는 피자와 다대다 관계로 이루어진 토핑들을 의미한다. 반대로 토핑과 연결된 피자를 알아보려면 다음과 같다.

In [1]: from models.many_to_many.models import Topping, Pizza

In [2]: t = Topping.objects.first()

In [3]: t
Out[3]: <Topping: 햄>

In [4]: t.pizzas.all()
Out[4]: <QuerySet [<Pizza: 슈퍼슈프림>]>

In [5]: t.pizzas
Out[5]: <django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager at 0x7eff89073fd0>

토핑의 ‘pizzas’는 토핑과 다대다 관계로 이루어진 피자들을 의미한다. 이는 Pizza 모델클래스에서 related_name으로 정의할 수 있다.

그렇게 토핑이 있는 피자의 관계를 피자 클래스에서 related_name=’pizzas’로 스스로를 가리키는 말을 정의할 수 있다.

중간모델이 있는 ManyToMany 관계

중간모델이 있는 ManyToMany 관계를 예를 들면 사람과 그룹으로 멤버를 이루는 관계를 들 수 있다. 이는 Person 클래스와 Group 클래스로 나타낼 수 있으며, 중간모델으로 Membership 클래스를 두어 어떤 사람이 그룹에 가입할 때 가입한 날짜가입한 이유 등을 중간모델에 입력할 수 있도록 한다.

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name


class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(
        Person,
        through='Membership',
    )

    def __str__(self):
        return self.name


class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

    def __str__(self):
        return '{person} - {group} ({date})'.format(
            person=self.person.name,
            group=self.group.name,
            date=self.date_joined,
        )

Membership 클래스에서는 외래키(ForeignKey)를 명시적으로 두 개인 PersonGroup 클래스를 두게 한다. 그리고 추가 정보로 입력하게 될 date_joinedinvite_reason을 두었다.

중간모델은 지켜야 할 항목이 있다.

  1. ManyToMany 필드로 두 테이블의 관계를 중간모델에서 나타낼 때, 무조건 한 테이블에서 하나의 ForeignKey로 나타내거나 두 개 이상일 경우 related_name으로 한 테이블에서 어떤 것을 가리키는지 명시적으로 지정해주어야 할 것이다.
  2. 중간모델에 보통 2개의 ForeignKey를 지정할 수 있다. 3개 이상의 ForeignKey 필드를 선언할 경우에는 through_fields 옵션을 설정해주어야한다. ```python class Person(models.Model): name = models.CharField(max_length=128)

    def str(self): return self.name

class Group(models.Model): name = models.CharField(max_length=128) members = models.ManyToManyField( Person, through=’Membership’, through_fields=(‘person’, ‘group’), )

def __str__(self):
    return self.name

class Membership(models.Model): # Person Class에서 person이 필수적으로 입력하는 정보로 명시 person = models.ForeignKey( Person, related_name=’memberships’, on_delete=models.CASCADE, ) group = models.ForeignKey(Group, on_delete=models.CASCADE) # Person Class에서 추천인의 정보를 명시하고 싶을 때 위와 같은 클래스를 참조하므로 위와 같이 related_name을 지정해주어야한다. recommender = models.ForeignKey( Person, related_name=’memberships_by_recommender’, on_delete=models.SET_NULL, blank=True, null=True, ) date_joined = models.DateField() invite_reason = models.CharField(max_length=64)

def __str__(self):
    return '{person} - {group} ({date})'.format(
        person=self.person.name,
        group=self.group.name,
        date=self.date_joined,
    ) ```