Lina's Toolbox

Model Relationship (M:N) / 좋아요, 팔로우 기능 구현 본문

스파르타 내일 배움 캠프 AI 웹개발 과정/Django framework

Model Relationship (M:N) / 좋아요, 팔로우 기능 구현

Woolina 2024. 9. 16. 05:04

ManyToMany Relationship

M:N 관계에 대해 이해해서 Django Model과 ORM을 활용하여 구현해보자!

 

좋아요 생각해보기

🤔 좋아요 기능은 어떻게 구현을 해야할까요?

⇒ 모든 기능은 로직을 고민하고 손으로 구현하는 방법 뿐입니다!

  • 좋아요 기능이 뭔가요?
    • User가 Article에 좋아요(Like)를 누르는 것입니다.
  • 저장해야할 데이터는 뭘까요?
    • User가 어떤 Article에 좋아요를 눌렀는지 저장하면 됩니다. 

 

1차 구현

  • User(1) - Article(N) :
  • ⇒ 한 명의 유저는 여러 Article에 좋아요를 누를 수 있으니까요!

articles/models.py

class Article(models.Model):
    title = models.CharField(max_length=50)
    content = models.TextField()
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="articles"
    )
    ...
    like_user = models.ForeignKey(
        settings.AUTH_USER_MODEL, 
        on_delete=models.CASCADE, 
        related_name="like_articles",
        null=True
    )

 

users/models.py

class User(AbstractUser):
    ...

 

 

  • 괜찮을까요?

User 1이 Article 1을 좋아요를 눌렀다면?

User 2가 Article 3을 좋아요를 눌렀다면?

 

User 3이 Article 1에 좋아요를 눌렀다면?

 

→ 이건 불가능!!

→ 이건 내용은 똑같지만 아예 다른 객체이죠,,?!

⇒ 따라서, 1:N 관계로는 한계가 있습니다!

 

2차 구현

  • User는 여러 Article에 좋아요를 누를 수 있습니다.
    → 좋아요 테이블을 따로 만들어요! 
    • articles/models.py 동일
class Article(models.Model):
    title = models.CharField(max_length=50)
    content = models.TextField()
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="articles"
    )
    ...

 

users/models.py 동일

class User(AbstractUser):
    ...

 

articles/models.py

class ArticleLike(models.Model):
    article = models.ForeignKey(
        Article, on_delete=models.CASCADE, related_name="likes"
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="likes"
    )

 

  • 괜찮을까요?
    • User 1이 Article 1을 좋아요를 눌렀다면?

 

User 2가 Article 3을 좋아요를 눌렀다면?

 

User 3이 Article 1에 좋아요를 눌렀다면?


ManyToMany

✔️ 이제 공식문서 익숙하시죠?
https://docs.djangoproject.com/en/4.2/ref/models/fields/#manytomanyfield
  • 다대다(M:N) 관계 설정시 사용하는 모델 필드입니다.
    • 예시) 좋아요→ 하나의 게시글도 여러명의 유저에게 좋아요를 받을 수 있어요!
      → 한 명의 유저는 여러개의 게시글을 좋아할 수 있어요!
  • 중계 테이블을 이용해서 관계를 표현합니다.
  • models.ManyToManyField(<classname>)을 이용해서 설정합니다.
  • M2M 관계가 설정되면 역참조시 사용가능한 _set이름의 RelatedManager를 생성합니다. (related_name 으로 변경 가능)
  • add(), remove()를 이용해서 관련 객체를 추가, 삭제할 수 있습니다.

 

좋아요 구현하기

  • articles/models.py 수정
class Article(models.Model):
    ...
    like_users = models.ManyToManyField(
        settings.AUTH_USER_MODEL, related_name="like_articles"
    )

related manager를 통해 양방향으로 추가할 수 있다. 조인같은거 필요없이!! → 넘 편리한딩?

 

수정 후 migration 실행!!

 

 

✔️ 중계테이블은요??

  • Django는 m2m 필드 추가시 자동으로 중계테이블을 설정합니다!

user_id와 article_id가 있는 중계테이블

 

 

ORM 연습하기

  1. article과 user가져오기

 

2. 좋아요 추가하기 & 조회하기

  • user_1 → article_3 : 좋아요
  • user_2 → article_3 : 좋아요
  • article_3을 좋아하는 모든 user 목록

  • user_1 → article_4 : 좋아요
  • user_1이 좋아하는 모든 article 목록

3. article_article_like_users 중계테이블

 


💡 만약에, 좋아요에 ‘정도’를 표현하고 싶다면?

  • 중계테이블을 내가 직접 정의해줄 수도 있어요!
    • through를 사용하여 중계테이블 지정이 가능합니다
      (장고가 만들어주는 테이블말고 이 테이블을 중계테이블로 사용할거야!)


 

articles/urls

urlpatterns = [
    ...
    path("<int:pk>/like/", views.like, name="like"),
    ...
]

 

articles/views

@require_POST
def like(request, pk):
    if request.user.is_authenticated:
        article = get_object_or_404(Article, pk=pk)
        if article.like_users.filter(pk=request.user.pk).exists():
            article.like_users.remove(request.user)
        else:
            article.like_users.add(request.user)
    else:
        return redirect("accounts:login")

    return redirect("articles:articles")

like을 누르면 DB에 수정이 일어나므로 post요청

 

 

articles/templates/articles/articles.html (1차)

{% for article in articles %}

		...
		
		<form actions="{% url "article:like" article.pk %}" method="POST">
				{% csrf_token &}
				<input type="sumit" value="좋아요">
		</form>
		
{% endfor %}

 

 

articles/templates/articles/articles.html (2차)

{% for article in articles %}

		...
		
		<form actions="{% url "article:like" article.pk %}" method="POST">
				{% csrf_token &}
				{% if user in article.like_users.all %}
						<input type="sumit" value="좋아요 취소">
				{% else %}
						<input type="sumit" value="좋아요">
				{% endif %}
		</form>
		
{% endfor %}

 


 

팔로우 생각해보기

🤔 팔로우는 어떻게 구현할 수 있을까요?

  • 한 명의 유저는 여러명의 유저를 팔로우 할 수 있어요!
  • 한 명의 유저는 여러명의 팔로워를 가질 수 있어요!

M:N 관계

 

그런데 말입니다!

from django.db import models
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    following = models.ManyToManyField(????, related_name='followers')

누구랑 m2m 관계를 맺어야 할까요?

USER - USER 즉, 관계를 맺는것도 내 자신입니다!

 

following = models.ManyToManyField('self', related_name='followers')

 

 

💡 symmetrical

  • M2M Field가 동일한 모델(self)과 관계를 맺는 경우에 사용합니다.
  • symmetrical=True인 경우 한 방향에서 관계를 맺으면 반대 관계도 설정됩니다. (대칭)
    • 기본값은 True입니다. 

'self'를 사용하는 순간 symmetrical 옵션 사용가능하다.

내가 나를 팔로우 할수는 없으므로 여기서는 False사용!


팔로우 구현하기

  • accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    following = models.ManyToManyField(
        "self", symmetrical=False, related_name="followers"
    )
  • 마이그레이션

생성된 테이블 확인

 

 

users/urls.py

from django.urls import path
from . import views

app_name = "users"
urlpatterns = [
    ...
    path("<int:user_id>/follow/", views.follow, name="follow"),
]

 

users/views.py

from django.shortcuts import render, redirect
from django.views.decorators.http import require_POST
from django.shortcuts import get_object_or_404
from django.contrib.auth import get_user_model


def users(request):
    return render(request, "users/users.html")


def profile(request, username):
    member = get_object_or_404(get_user_model(), username=username)
    context = {
        "member": member,
    }
    return render(request, "users/profile.html", context)


@require_POST
def follow(request, user_pk):
    if request.user.is_authenticated:
        member = get_object_or_404(get_user_model(), pk=user_pk)
        if request.user != member:
            if request.user in member.followers.all():
                member.followers.remove(request.user)
            else:
                member.followers.add(request.user)
        return redirect("users:profile", member.username)
    return redirect("accounts:login")

 

users/templates/users/profile.html (1차)

{% extends 'base.html' %}

{% block content %}
    <h1>{{ member.username }}의 프로필 페이지</h1>

    <div>
        <h2>username : {{ member.username }}</h2>
        <p>
            팔로워 : {{ member.followers.count }}명
            팔로잉 : {{ member.following.count }}명
        </p>
    </div>

    <div>
        <form action="{% url "users:follow" member.pk %}" method="POST">
            {% csrf_token %}
                <button type="submit">팔로우</button>
        </form>
    </div>

    <a href="/index/">홈으로 돌아가기</a>

{% endblock content %}

 

 

users/templates/users/profile.html (2차)

{% extends 'base.html' %}

{% block content %}
    <h1>{{ member.username }}의 프로필 페이지</h1>

    <div>
        <h2>username : {{ member.username }}</h2>
        <p>
            팔로워 : {{ member.followers.count }}명
            팔로잉 : {{ member.following.count }}명
        </p>
    </div>

    <div>
        <form action="{% url "users:follow" member.pk %}" method="POST">
            {% csrf_token %}
            {% if user in member.followers.all %}
                <button type="submit">언팔로우</button>
            {% else %}
                <button type="submit">팔로우</button>
            {% endif %}
        </form>
    </div>

    <a href="/index/">홈으로 돌아가기</a>

{% endblock content %}