DB Many to Many(M:N)

김재훈·2024년 4월 8일

Django

목록 보기
12/15

Many to many relationships (N:M or M:N)

  • 한 테이블의 0개 이상의 레코드가 다른 테이블의 0개 이상의 레코드와 관련된 경우
    • 양쪽에서 모두 N:1 관계를 가짐

예시)

  1. 의사와 환자 간 모델 관계 설정
    • 한 명의 의사에게 여러 환자가 예약할 수 있도록 설계
from django.db import models


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

    def __str__(self):
        return f'{self.pk}번 의사 {self.name}'


class Patient(models.Model):
    doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE)
    name = models.TextField()

    def __str__(self):
        return f'{self.pk}번 환자 {self.name}'
  1. 의사와 환자 데이터 생성
    • 2명의 의사와 환자를 생성하고 환자는 서로 다른 의사에게 예약
In [1]: doctor1 = Doctor.objects.create(name='allie')
In [2]: doctor2 = Doctor.objects.create(name='barbie')
In [3]: patient1 = Patient.objects.create(name='carol', doctor=doctor1)      
In [4]: patient2 = Patient.objects.create(name='duke', doctor=doctor2)       

In [5]: doctor1
Out[5]: <Doctor: 1번 의사 allie>
In [6]: doctor2
Out[6]: <Doctor: 2번 의사 barbie>
In [7]: patient1
Out[7]: <Patient: 1번 환자 carol>
In [8]: patient2
Out[8]: <Patient: 2번 환자 duke>

N:1의 한계 상황

  • 1번 환자(carol)가 두 의사에게 모두 진료를 받고자 한다면 환자 테이블에 1번 환자 데이터가 중복으로 입력될 수 밖에 없음

    • hospitals_doctor

    • idname
      1allie
      2barbie
    • hospitals_patients

    • idnamedoctor_id
      1carol1
      2duke2
      3carol2
    • 예약 테이블을 따로 만들어야함

중개 모델

  1. 예약 모델 생성

    • 환자 모델의 외래 키를 삭제하고 별도의 예약 모델을 새로 생성
    • 예약 모델은 의사와 환자에 각각 N:1 관계를 가짐
    • hospitals_reservation
    • iddoctor_idpatient_id
      ...
  2. 예약 데이터 생성

    • 데이터베이스 초기화 후 Migration 진행 및 shell_plus 실행
    • 의사와 환자 생성 후 예약 만들기
    • doctor1 = Doctor.objects.create(name='allie')
      patient1 = Patient.objects.create(name='carol')
      Reservation.objects.create(doctor = doctor1, patient = patient1)
  1. 예약 정보 조회
    • 의사와 환자가 예약 모델을 통해 각각 본인의 진료 내역 확인
    • 의사 -> 예약 정보 찾기
      doctor1.reservation_set.all()
      <QuerySet [<Reservation: 1번 의사의 1번 환자>]>
      환자 -> 예약 정보 찾기
      patient1.reservation_set.all()
      <QuerySet [<Reservation: 1번 의사의 1번 환자>]>
  1. 추가 예약 생성
    • 1번 의사에게 새로운 환자 예약 생성
patient2 = Patient.objects.create(name='duke')
Reservation.objects.create(doctor=doctor1, patient=patient2)
  1. 예약 정보 조회
    • 1번 의사의 예약 정보 조회
      #의사 -> 환자 목록
      doctor1.reservation_set.all()
      <Queryset [<Reservation: 1번 의사의 1번 환자>, <Reservation: 1번 의사의 2번 환자>]>

ManyToManyField(to, **options)

  • Django에서는 'ManyToManyField'로 중개 모델을 자동으로 생성
  • M:N 관계 설정 모델 필드
  • 환자 모델에 ManyToManyField 작성
    • 의사 모델에 작성해도 상관 없으며 참조/역참조 관계만 잘 기억할 것
#hospitals/models.py

class Patient(models.Modle):
	# ManyToManyField 작성
    doctors = models.ManyToManyField(Doctor)
    name = models.TextField()
    
    def __str__(self):
    	return f'{self.pk}번 환자 {self.name}'

'through' argument

  • 중개 테이블에 '추가 데이터'를 사용해 M:N 관계를 형성하려는 경우에 사용
  • Reservation Class 재작성 및 through 설정
    • 이제는 예약 정보에 "증상"과 "예약일"이라는 추가 데이터가 생김
class Patient(models.Model):
	doctors = models.ManyToManyField(Doctor, through='Reservation')
    name = models.TextField()
    
    def __str__(self):
    	return f'{self.pk}번 환자 {self.name}'

Class Reservation(models.Model):
	doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE)
    patient = models.ForeignKey(Patient, on_delete=models.CASCADE)
    symptom = models.TextField()
    reserved_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
    	return f'{self.doctor.pk}번 의사의 {self.patient.pk}번 환자'

through_defaults

  • Patient 또는 Doctor의 인스턴스를 통한 예약 생성 (through_defaults)
patient2.doctors.add(doctor1, through_defaults={'symptom': 'flu'})

M:N 관계 주요 사항

  • M:N 관계로 맺어진 두 테이블에는 물리적인 변화가 없음
  • ManyToManyField는 중개 테이블을 자동으로 생성
  • ManyToManyField는 M:N 관계를 맺는 두 모델 어디에 위치해도 상관 없음
    • 대신 필드 작성 위치에 따라 참조와 역참조 방향을 주의할 것
  • N:1은 완전한 종속의 관계였지만 M:N은 종속적인 관계가 아니며 '의사에게 진찰받는 환자 & 환자를 진찰하는 의사' 이렇게 2가지 형태 모두 표현 가능

ManyToManyField의 대표 인자 3가지

  1. 'related_name' arguments
    • 역참조시 사용하는 manager name을 변경
  2. 'symmetrical' arguments
    • 관계 설정시 대칭 유무 설정
    • ManyToManyField가 동일한 모델을 가리키는 정의에서만 사용
    • 기본값 : True
    • True일 경우
      • source 모델의 인스턴스가 target 모델의 인스턴스를 참조하면 자동으로 target 모델 인스턴스도 source 모델 인스턴스를 자동으로 참조 하도록 함(대칭)
      • 즉, 내가 당신의 친구라면 자동으로 당신도 내 친구가 됨
    • False일 경우
      • True와 반대(대칭되지 않음)
  3. 'through' arguments
    • 사용하고자 하는 중개모델을 지정
    • 일반적으로 추가 데이터를 M:N 관계와 연결하려는 경우에 활용

좋아요 기능 구현

Many to many relationships

  • 한 테이블의 0개 이상의 레코드가 다른 테이블의 0개 이상의 레코드와 관련된 경우

  • 양 쪽에서 모두 N:1 관계를 가짐

  • Article(M) - User(N)

    • 0개 이상의 게시글은 0명 이상의 회원과 관련

      게시글은 회원으로부터 0개 이상의 좋아요를 받을 수 있고, 회원은 0개 이상의 게시글에 좋아요를 누를 수 있음

  1. 모델 관계 설정
    • Article 클래스에 ManyToManyField 작성
      #articles/models.py

class Article(modles.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
like_users = models.ManyToManyField(settings.AUTH_USER_MODEL)

  1. 역참조 매니저 충돌
  • N:1
    • "유저가 작성한 게시글"
    • user.article_set.all()
  • M:N
    • "유저가 좋아요 한 게시글"
    • user.article_set.all()
  • like_users 필드 생성 시 자동으로 역참조 매니저 .article_set가 생성됨
  • 그러나 이전 N:1(Article-User) 관계에서 이미 같은 이름의 매니저를 사용중
    • user.article_set.all() -> 해당 유저가 작성한 모든 게시글 조회
  • 'user가 작성한 글 (user.article_set)'과
    'user가 좋아요를 누른 글(user.article_set)'을 구분할 수 없게 됨

    user와 관계된 ForeignKey 혹은 ManyToManyField 둘 중 하나에 related_name 작성 필요

  1. related_name 작성 후 Migration 재진행
like_users = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='like_articles')

0개의 댓글