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 주석 처리
- 생성된 중개 테이블 hospitals_patient_doctors 확인
- 의사 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 관계를 형성하려는 경우에 사용
- 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명과 환자 2명 생성
doctor1 = Doctor.objects.create(name='allie')
patient1 = Patient.objects.create(name='carol')
patient2 = Patient.objects.create(name='duke')
- 예약 생성 방법
# 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()
- 예약 삭제
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가지
- related_name
- symmetrical
- 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 |