[Python] Django - ManyToManyField

김상웅·2022년 6월 9일
0

[파이썬]

목록 보기
12/17

✅ 소개


이번 포스팅에서도 httpie를 이용하여 HTTP 통신 연습을 해보겠습니다.

이전 포스팅에서는 일대다 관계를 갖고 있는 두개의 테이블에 데이터를 추가하고, 조회하는 작업을 했는데요.

이번에는 아래의 사진과 같이 다대다 관계를 갖고 있는 두개의 테이블에 대해 CRUD를 구현해보려고 합니다.

다대다관계를 갖는 테이블의 경우 중간테이블을 생성하는 모델링 방식을 사용하는데요.

저는 Django에서 다대다관계를 정의할 수 있는 ManyToManyField를 사용해보고,
ManyToManyField를 사용하였을 때의 장점이 무엇인지 알아보겠습니다.

이번 CRUD 구현을 위한 준비 요소는 다음과 같습니다.

프로젝트 준비 요소

  1. 초기세팅 - 가상환경, 파이썬 장고 기반의 작업 디렉토리
  2. 데이터베이스 - MySQL 연동
  3. httpie 설치
    sudo apt install httpie

Github 소스코드 보기

Github의 브랜치는 feature/movie를 선택해야 합니다.
movies 디렉토리에서 다음 파일의 코드를 볼 수 있습니다.

  1. urls.py
  2. models.py
  3. view.py


✅ 모델링


위의 사진에서도 볼 수 있지만, 다대다관계의 테이블은 중간 테이블을 통해 서로를 참조할 수 있습니다.

ManyToManyField를 활용하여 다대다관계를 설정한 코드는 아래와 같습니다.

📌 Models.py - ManyToManyField 사용

from django.db import models

# Create your models here.

class Movie(models.Model) :
  title             = models.CharField(max_length=45)
  release_date      = models.DateField()
  running_time_min  = models.IntegerField(null=True)
  actors            = models.ManyToManyField("Actor", db_table="movie_actor" through=movie_actor)
  # 영화가 배우를 정참조 ↔ 배우는 영화를 역참조(_set)
  # through를 통해 DB내에 자동으로 생성되는 movie_actor 테이블 생성

  class Meta: 
    db_table = "movies"

class Actor(models.Model) :
  first_name    = models.CharField(max_length=45)
  last_name     = models.CharField(max_length=45)
  date_of_birth = models.DateField()

  class Meta : 
    db_table = "actors"

Movie 테이블은 Actor 테이블의 정보를 정참조합니다.

반대로 Actor 테이블은 Movie 테이블을 역참조합니다.

Models.py 파일에 중간테이블을 선언하는 class가 존재하지 않습니다.
하지만, 이 모델만으로도 데이터베이스 상에 아래와 같이 중간 테이블이 형성됩니다.

ManyToManyField를 사용하면서 models.py 파일 내부에 중간테이블을 수동으로 작성해줄 수도 있습니다.

ManyToManyField의 내부 속성으로 through="name" name 값을 지정해주면 중간테이블의 테이블명을 수동으로 지정할 수 있습니다.

📌 Models.py - ManyToManyField 미사용

ManyToManyField를 사용하지 않은 코드를 보려면 여기를 클릭해주세요.
(branch가 main인지 확인!)



✅ View 로직


이제 응답을 위해 데이터를 처리하는 로직을 설계해야합니다.

우선 코드를 보겠습니다.

📌 Views.py - ManyToManyField 사용

from django.shortcuts import render

# Create your views here.
import json

from django.http import JsonResponse
from django.views import View

from movies.models import Movie, Actor

class MovieView(View):

  def get(self, request):
    movies  = Movie.objects.all()
    results = []

    for movie in movies :
      title   = movie.title
      runtime = movie.running_time_min

      results.append(
        {
          "제목": title,
          "상영시간": runtime,
          "출연진" : [actor.last_name + actor.first_name for actor in movie.actors.all()]
          # models.py 파일에서 Movie 테이블의 MtoMField로 설정한 actors에 바로 접근 가능
        }
      )
  
    return JsonResponse({"영화 정보": results }, status=200)

class ActorView(View):
  
  def get(self, request):
    actors  = Actor.objects.all()
    results = []

    for actor in actors :
      name   = actor.last_name + actor.first_name

      results.append(
        {
          "이름": name,
          "출연작": [movie.title for movie in actor.movie_set.all()]
          # models.py 파일에서 Actor 테이블은 Movie를 역참조
          # 그렇기 때문에 movie에 바로 접근할 수 없고 movie_set 접근
        }
      )
    

    return JsonResponse({"배우 정보": results}, status=200)

정참조

Movie 테이블은 ManyToManyField인 actors 필드로 Actor 테이블 정보에 바로 접근할 수 있습니다.

그렇기 때문에 movie.actor.all()을 통해 Actor 테이블의 데이터를 바로 참조할 수 있습니다.

역참조

반면, Actor 테이블은 Movie 테이블에 직접 접근할 수 없습니다.

그렇기 때문에 actor.movie_set.all()을 통해 Movie 테이블의 데이터에 접근할 수 있습니다.

중간 테이블을 사용한 경우

중간 테이블을 사용한 경우에는 Movie와 Actor가 서로 직접, 혹은 _set을 통해 접근이 불가능할 것입니다.

이 때는 중간테이블을 import하거나, 참조하고 있는 중간테이블에 접근한 이후 참조하고 있는 다른 테이블에 접근해야 할 것입니다.

📌 Views.py - ManyToManyField 미사용

여기를 클릭하면 import한 중간테이블을 거쳐 서로 참조하고 있는 테이블에 접근하는 코드를 볼 수 있습니다.
(비교해보세요!)

관계의 흐름이 보여 읽기 쉬워보여도 참조하는 테이블의 관계가 많아진다면 어떨까요?

신경써야하는 부분이 많아지고 코드의 가독성도 떨어질 것입니다.



✅ URL conf


요청 처리는 코드만 가볍게 살펴보고 넘어가겠습니다.

엔드포인트를 생성할 때 의미에 맞는 url 경로를 설정해주어야겠습니다.

현재 App은 movies앱이며, main/urls.py에서도 /movies url 설정되어 있습니다.

📌 urls.py

from django.urls import path

from .views import ActorView, MovieView
urlpatterns = [
    path('/movie', MovieView.as_view()),
    path('/actor', ActorView.as_view()),
]

서버 예시)
http://127.0.0.1:/8000/moives/movie >> MovieView
http://127.0.0.1:/8000/moives/actor >> ActorView



ManyToManyField - Shell을 이용한 데이터 생성


중간 테이블을 직접 생성하여 데이터를 shell을 통해 직접 추가하는 경우,
중간 테이블과의 관계를 따로 직접 설정해주어 테이블 간의 관계를 연결할 수 있었습니다.

하지만 ManyToManyField를 사용하면서 데이터를 추가하는 과정에서 어려움이 있었는데요.

다음 방법을 이용할 수 있겠습니다.

우선, Django Shell을 켜줍니다!

python manage.py shell

다음, 사용하는 테이블을 불러옵니다.

from movies.models from Model, Actor

Movie/Actor 테이블에 데이터를 추가해줍니다.

Movie.objects.create(title="범죄도시2", relasea_date="2022-05-18" running_time_min=106)
Movie.objects.create(title="범죄도시", relasea_date="2017-10-03" running_time_min=121)
Movie.objects.create(title="영화", relasea_date="2022-06-09" running_time_min=100)

Actor.objects.create(last_name="마", first_name="동석", date_of_birth="1973-03-01")
Actor.objects.create(last_name="김", first_name="상웅", date_of_birth="1996-12-06")
Actor.objects.create(last_name="손", first_name="석구", date_of_birth="1983-02-17")

결과는 아래 사진과 같습니다.

이제 두개의 테이블을 연결해주겠습니다.

Movie.objects.get(id=5).actors.add(Actor.objects.get(id=7))
Movie.objects.get(id=5).actors.add(Actor.objects.get(id=8))
Movie.objects.get(id=6).actors.add(Actor.objects.get(id=7))
Movie.objects.get(id=6).actors.add(Actor.objects.get(id=8))
Movie.objects.get(id=6).actors.add(Actor.objects.get(id=9))

결과는 아래 사진과 같습니다.
자동으로 생성된 중간테이블에 각각의 테이블 필드의 id값이 저장되어 있습니다.



✅ ManyToManyField를 사용하면 좋은점


  1. models.py에 클래스를 추가하지 않고 중간테이블을 생성할 수 있습니다.
    신경써야 하는 class 수가 줄어들 것입니다.

  2. 각 테이블의 데이터에 쉽게 접근할 수 있습니다.
    views.py 로직을 설계할 때 중간테이블을 거치지 않고 각 테이블에 접근할 수 있습니다.

profile
누구나 이해할 수 있도록

0개의 댓글