본문 바로가기

TIl

N:M 1

Many to many relationships N:M or M:N

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

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

왜 필요한가 이해하기

  • “병원 진료 시스템 모델 관계’를 만들며 M:N 관계의 역할과 필요성 이해하기
  • 환자와 의사 2개의 모델을 사용하여 모델 구조 구상하기

N:1의 한계

한 명의 의사에게 여러 환자가 예약할 수 있도록 설계

# hosplitals/models.py

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}'

외래키를 환자쪽에서 가지고 있음

doctor1 = Doctor.objects.create(name='allie')
doctor2 = Doctor.objects.create(name='barbie')
patient1 = Patient.objects.create(name='carol', doctor=doctor1)
patient2 = Patient.objects.create(name='duke', doctor=doctor2)
patient3 = Patient.objects.create(name='carol', doctor=doctor2)

한계 상황

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

동시에 예약을 남길 수는 없을 까?

patient4 = Patient.objects.create(name='duke', doctor=doctor1, doctor2)

문법 상 불가능

  • 동일한 환자지만 다른 의사에게도 진료 받기 위해 예약하기 위해서는 객체를 하나 더 만들어 진행해야 함
  • 외래 키 컬럼에 ‘1,2’ 형태로 저장하는 것은 DB 타입 문제로 불가능

⇒ 예약 테이블을 따로 만들자

중계 모델

1. 예약 모델 생성

  • 환자 모델의 외래 키를 삭제하고 별도의 예약 모델을 새로 생성
  • 예약 모델은 의사와 환자에 각각 N:1 관계를 가짐
# hospitals/models.py

# 외래키 삭제
class Patient(models.Model):
    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)

    def __str__(self):
        return f'{self.doctor_id}번 의사의 {self.patient_id}번 환자'

2. 예약 데이터 생성

의사와 환자 생성 후 예약 만들기

# shell

doctor1 = Doctor.objects.create(name='allie')
patient1 = Patient.objects.create(name='carol')

Reservation.objects.create(doctor=doctor1, patient=patient1)

3. 예약 정보 조회

  • 의사와 환자가 예약 모델을 통해 각각 본인의 진료 내역 확인
# 의사 -> 예약 정보 찾기
doctor1.reservation_set.all()

# 환자 -> 예약 정보 찾기
patient1.reservation_set.all()

4. 추가 예약 생성

  • 1번 의사에게 새로운 환자 예약 생성
patient2 = Patient.objects.create(name='duke')
Reservation.objects.create(doctor=doctor1, patient=patient2)

Reservation

ID  doctor_id  patient_id
1 1 1
2 2 1

Django에서는 ‘ManyToManyField’로 중개모델을 자동으로 생성

ManyToManyField() : M:N 관계 설정 모델 필드

환자 모델에 ManyToManyField 작성

  • 의사 모델에 작성해도 상관은 없음 but 참조/역참조 관계만 잘 기억할 것
# hospitals/models.py

class Patient(models.Model):
    # ManyToManyField 작성
    doctors = models.ManyToManyField(Doctor)
    name = models.TextField()

    def __str__(self):
        return f'{self.pk}번 환자 {self.name}'

# Reservation Class 주석 처리
  1. 생성된 중개 테이블 hospitals_patient_doctors 확인
  2. 의사 1명과 환자 2명 생성
doctor1 = Doctor.objects.create(name='allie')
patient1 = Patient.objects.create(name='carol')
patient2 = Patient.objects.create(name='duke')

3. 예약 생성 ( 환자에 의한)

# patient1이 doctor1에게 예약
patient1.doctors.add(doctor1)

# patient1 - 자신이 예약한 의사목록 확인
patient1.doctors.all()

# doctor1 - 자신의 예약된 환자목록 확인
doctor1.patient_set.all()

4. 예약 생성 ( 의사에 의한)

# doctor1이 patient2을 예약
doctor1.patient_set.add(patient2)

# doctor1 - 자신의 예약 환자목록 확인
doctor1.patient_set.all()

# patient1, 2 - 자신이 예약한 의사 목록 확인
patient2.doctors.all()
patient1.doctors.all()

5. 중개 테이블에서 예약 현황 확인

id  patient_id  doctor_id
1 1 1
2 2 1

6. 예약 취소하기 ( 삭제)

이전에는 Reservation을 찾아서 지워야 했다면, 이제는 .remove()로 삭제 가능

# docotr1이 patient1 진료 예약 취소
doctor1.patient_set.remove(patient1)
doctor1.patient_set.all() # patient2는 남아있음
patient1.doctors.all() # patient1 의 예약은 사라졌음

# patient2가 doctor1 진료 예약 취소
patient2.patient_set.remove(doctor1)
patient2.doctors.all() # 예약 사라짐
doctor1.patient_set.all() # doct1의 모든 예약 없음

1. models.ManytoManyField 를 통해 만들 때 Patient에 물리적인 공간이 만들어지는 것은 아니다.

2. Doctors와 Patient간에 어디에 ManytoMany 가 들어가냐는 참고/역참조의 차이일 뿐, 어디에 들어갈지는 큰 차이가 없음, 생각하기 편한 위치에 두면 된다.

만약? 예약 정보에 병의 증상, 예약일 등 추가 정보가 포함되어야 한다면?

through argument

중개 테이블에 추가 데이터를 사용해 M:N 관계를 형성하려는 경우에 사용

  1. Reservation Class 재작성 및 through 설정
  • 예약 정보에 ‘증상’과 ‘예약일’이라는 추가 데이터가 생김
# hospitals / models.py
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)
  1. 의사 1명과 환자 2명 생성
doctor1 = Doctor.objects.create(name='allie')
patient1 = Patient.objects.create(name='carol')
patient2 = Patient.objects.create(name='duke')
  1. 예약 생성 방법
# 1. Reservation class를 통한 예약 생성
reservation1 = Reservation(doctor=doctor1, patient=patient1, symptom='headache')
reservation1.save()
doctor1.patient_set.all()
patient1.doctors.all()

# 2. Patient 객체를 통한 예약 생성 ( Doctor 인스턴스도 방식 동일)
patient2.doctors.add(doctor1, through_defaults={'symptom': 'flu'})
doctor1.patient_set.all()
patient2.doctors.all()
  1. 예약 삭제
doctor1.patient_set.remove(patient1)
patient2.doctors.remove(doctor1)

M:N 관계 주요 사항

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

ManyToManyField(to, **options)

M:N 관계 설정 시 사용하는 모델 필드

대표 인자 3가지

  1. related_name
  2. symmetrical
  3. through

1. related_name arguments

  • 역참조시 사용하는 manager name을 변경
class Patient(models.Model):
    # ManyToManyField - related_name 작성
    doctors = models.ManyToManyField(Doctor, related_name='patients')
    name = models.TextField()
# 변경 전
doctor.patient_set.all()

# 변경 후
doctor.patients.all()

2. symmetrical arguments

  • 관계 설정 시 대칭 유무 설정 ( 기본 값 : True)

ManyToManyField가 동일한 모델을 가리키는 정의에서만 사용

p_id  d_id
1 2
2 1
class Person(models.Model):
	friends = models.ManyToManyField('self')
	# friends = models.ManyToManyField('self', symmetrical=False)

source 모델: 관계를 시작하는 모델

target 모델 : 관계의 대상이 되는 모델

  • True 일 경우
    • source 모델의 인스턴스가 target 모델의 인스턴스를 참조하면 자동으로 target 모델 인스턴스도 source 모델 인스턴스를 자동으로 참조하도록 함( 대칭)
    • 즉, 내가 당신의 친구라면 자동으로 당신도 내 친구가 됨
  • False일 경우
    • True와 반대 ( 대칭되지 않음)

3. through arguments

  • 사용하고자 하는 중개모델을 지정
  • 일반적으로 추가 데이터를 M:N 관계와 연결하려는 경우에 활용
class Patient(models.Model):
    doctors = models.ManyToManyField(Doctor, through='Reservation')
    
class Reservation(models.Model):
    ...

M:N 에서의 대표 methods

  • add()
    • 지정된 객체를 관련 객체 집합에 추가
    • 이미 존재하는 관계에서 사용하면 관계가 복지되지 않음
  • remove()
    • 관련 객체 집합에서 지정된 모델 객체를 제거

좋아요 기능 구현

모델 관계 설정

Many to Many relationships

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

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

Article(M) - User(N) : 0개 이상의 게시글은 0명 이상의 회원과 관련

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

1. Article 클래스에 ManyToManyField작성

# articles/models.py

from django.conf import settings

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

⇒ Migrations 시 에러발생함 == 역참조 매니저 충돌

역참조 매니저 충돌

  • 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_est) 과 user가 좋아요 누른 글(user.article_set)을 구분할 수 없게 됨

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

2. related_name 작성 후 Migration 재 진행

like_user = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='like_articles')

User-Article간 사용 가능한 전체 related manager

  • article.user
    • 게시글을 작성한 유저 - N:1
  • user.article_set
    • 유저가 작성한 게시글(역참조) - N:1
  • article,like_users
    • 게시글을 좋아요 한 유저 - M:N
  • user.like_articles
    • 유저가 좋아요 한 게시글(역참조) - M:N

기능 구현

# articles/urls
path('<int:article_pk>/likes/', views.likes, name='likes')
# articles/views.py
def likes(request, article_pk):
    # 어떤 게시글에 좋아요가 눌리는건지 조회
    article = Article.objects.get(pk=article_pk)

    # 좋아욜를 요청하는 유저
    user = request.user

    # 해당 게시글에 좋아요를 누른 유저 목록에 현재 요청하는 유저가 있을 경우
    if user in article.like_user.all(): 
        # 좋아요 취소
        article.like_user.remove(user)
    else:
        # 좋아요 진행
        article.like_user.add(user)
    
    # 요청하는 유저가 좋아요를 누른 게시글 목록에 지금 좋아요를 요청하는 게시글이 있을 경우
    # 위와 아래의 두 표현중 편한거로 사용 하면됨
    if article in user.like_articles.all():
        user.like_articles.remove(article)
    else:
        user.like_articles.add(article)
<!-- articles/index.html -->

<p>{{article.like_user.all|length}} 명이 이 글을 좋아합니다.</p>
<p>{{article.like_user.count}} 명이 이 글을 좋아합니다.</p>
<form action="{% url "articles:likes" article.pk%}" method="POST">
  {% csrf_token %}
  {% if request.user in article.like_user.all %}
  <input type="submit" value='좋아요 취소'>
  {% else %}
  <input type="submit" value='좋아요'>
  {% endif %}
</form>

TIP

@login_required + 하려던 행동 이어서 하기

/create/를 직접 입력해서 create 폼으로 갈수 있음 → login_required로 이를 방지함

→ login후 create 행동을 이어서 하게 만들기 위함

# articles/views.py
def login(request):
    ...
        if form.is_valid():
            auth_login(request, form.get_user())
            return redirect(request.GET.get('next') or 'articles:index')
    ...
<!-- accounts/login.html-->
<h1>Login</h1>
<form action="" method="POST">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit">
</form>

'TIl' 카테고리의 다른 글

N:1 1  (1) 2024.04.22
N:M 2  (1) 2024.04.22
DOM  (1) 2024.04.16
REST API 2  (0) 2024.04.16
REST API 1  (0) 2024.04.16