본문 바로가기

TIl

N:M 2

팔로우 기능 구현

프로필 페이지

  • 각 회원의 개인 프로필 페이지에 팔로우 기능을 구현하기 위해 프로필 페이지를 먼저 구현하기
# accounts/urls.py

urlpatterns = [
	path('profile/<username>/',views.profile, name='profile'),
]

/<str:username>/ 이라고만 해둔다면, 해당 url 밑의 모든 url이 문자열로 검색하게 됨,

ex) login 이라는 유저를 찾게 된다

# accounts/views.py

from django.contrib.auth import get_user_model

def profile(request, username):
    User = get_user_model()
    person = User.objects.get(username=username)
    context = {
        'person':person
    }
    return render(request, 'accounts/profile.html', context)
<!-- accounts/profile.html -->
<h1>{{person.username}}님의 프로필 페이지</h1>
<h2>{{person.username}}님이 작성한 게시글</h2>
{% for article in person.article_set.all %}
  <div>{{article.title}}</div>
{% endfor %}
<h2>{{person.username}}님이 작성한 댓글 목록</h2>
{% for comment in person.comment_set.all %}
  <div>{{comment.content}}</div>  
{% endfor %}

<h2>{{person.username}}님이 좋아요 한 글 목록</h2>
{% for article in person.like_articles.all %}
  <div>{{article.title}}</div>  
{% endfor %}
<!-- articles/index.html -->
<a href="{% url "accounts:profile" user.username%}">내 프로필</a>
{% for article in articles %}
  <p>작성자: 
    <a href="{% url "accounts:profile" article.user.username %}">{{ article.user }}</a>
  </p>

팔로우 기능 구현

User(M) - User(N)

0명 이상의 회원은 0명 이상의 회원과 관련

= 회원은 0명 이상의 팔로워를 가질 수 있고, 0명 이상의 다른 회원들을 팔로잉 할 수 있음

# accounts/models.py
class User(AbstractUser):
    following = models.ManyToManyField('self', symmetrical=False, related_name='followers')
  • 참조
    • 내가 팔로우 하는 사람들 (팔로잉, followings)
  • 역참조
    • 상대방 입장에서 나는 팔로워 중 한 명( 팔로워, followers)
  • 바뀌어도 상관 없으나 관계 조회 시 생각하기 편한 방향으로 정한 것
  • symmetrical 속성으로 대칭 관계는 False로 한다
# accounts/urls.py
urlpatterns = [
	path('<int:user_pk>/follow/', views.follow, name='follow'),
]
# accouts/views.py

@login_required
def follow(request, user_pk):
    me = request.user
    you = get_user_model().objects.get(pk=user_pk)

    # 요청하는 사람이 상대방의 팔로워 목록에 있는지 없는지
    if me in you.followers.all():
        you.followers.remove(me)
        # me.followings.remove(you) # 동일함
    else:
        you.followers.add(me)
        # me.followings.add(you) # 동일함
    return redirect('accounts:profile', you.username)
# accounts/profile.html

<div>
  팔로잉 : {{person.followings.all|length}} / 팔로워 : {{person.followers.all|length}}
</div>
{% if request.user != person %}
<form action="{% url "accounts:follow" person.pk%}" method="POST">
  {% csrf_token %}
  {% if request.user in person.followers.all %}
  <input type="submit" value="언 팔로우">
  {% else %}
  <input type="submit" value="팔로우">
  {% endif %}
</form>
{% endif %}

참고

.exists() QuerySet에 결과가 포함되어 있으면 True를 반환하고 결과가 포함되어 있지 않으면 False를 반환

⇒ 큰 QuerySet에 있는 특정 객체 검색에 유용

# articles/views.py
if request.user in article.like_users.all()
=> if article.like_users.filter(pk=request.user.pk).exists():

if request.user in person.followers.all():
=> if person.followers.filter(pk=request.user.pk).exists():

Fixtures

Django가 데이터베이스로 가져오는 방법을 알고 있는 데이터 모음

⇒ 데이터는 데이터베이스 구조에 맞추어 작성 되어 있음

사용 목적 : 초기 데이터 제공

왜 초기 데이터가 필요한가?

  • 협업하는 유저 A, B가 있다고 생각해보기
    1. A가 먼저 프로젝트를 작업 후 원격 저장소에 push 진행
      • gitignore로 인해 DB는 업로드하지 않기 때문에 A가 생성한 데이터도 업로드 X
    2. B가 원격 저장소에서 A가 push한 프로젝트를 pull (혹은 clone)
      • 결과적으로 B는 DB가 없는 프로젝트를 받게 됨
  • 이처럼 프로젝트의 앱을 처음 설정할 때 동일하게 준비 된 데이터로 데이터베이스를 미리 채우는 것이 필요한 순간이 있음

⇒ Django에서는 fixtures를 사용해 앱에 초기 데이터(initial data)를 제공

활용

M:N 까지 모두 작성된 Django 프로젝트에서 유저, 게시글, 댓글 등 각 데이터를 최소 2~3개 이상 생성해두기

fixtures 관련 명령어

  • dumpdata 생성 ( 데이터 추출)
  • loaddata 로드(데이터 입력)

dumpdata

데이터베이스의 모든 데이터를 추출

# 작성 예시
$ python manage.py dumpdata [app_name[.ModelName] [app_name[.ModelName] ...]] > filename.json

# articles 의 article을 들여쓰기 4칸을 써서 json파일로 저장했다
$ python manage.py dumpdata --indent 4 articles.article > articles.json
$ python manage.py dumpdata --indent 4 accounts.user > users.json
$ python manage.py dumpdata --indent 4 articles.comment > comments.json

loaddata

Fixtures 데이터를 데이터베이스로 불러오기

Fixtures 파일 기본 경로 app_name/fixtures/

⇒ Django는 설치된 모든 app의 디렉토리에서 fixtures 폴더 이후의 경로로 fixtures 파일을 찾아 load

# 해당 위치로 fixture 파일 이동
articles/
	fixtures/
		articles.json
		user.json
		comments.json

load 진행 후 데이터가 잘 입력되었는지 확인

$ python manage.py loaddata articles.json users.json comments.json

만약 안될때?

데이터에 한글이 들어가 있어서 생기는 문제

json 파일을 텍스트 에디터로 열어서 UTF-8 로 저장하면 해결 할 수 있다.

loaddata 순서 주의사항

  • 만약 loaddata를 한번에 실행하지 않고 별도로 실행한다면 모델 관계에 따라 load 순서가 중요할 수 있음
    • comment는 article에 대한 key 및 user에 대한 key가 필요
    • article은 user에 대한 key가 필요
  • 즉, 현재 모델 관계에서는 user -< article → comment 순으로 data를 load 해야 오류가 발생하지 않음
# python manage.py loaddata users.json
# python manage.py loaddata articles.json
# python manage.py loaddata comments.json

모든 모델을 한번에 dump 하기

# 3개의 모델을 하나의 json 파일로
$ python manage.py dumpdata --indent 4 articles.article articles.comment accounts.user > data.json

# 모든 모델을 하나의 json 파일로
$ python manage.py dumpdata --indent 4 > data.json

loaddata 시 encoding codec 관련 에러가 발생하는 경우

  • 2가지 방법 중 택 1
  1. dumpdata 시 추가 옵션 작성
$ python -Xutf8 manage.py dumpdata [생략]
  1. 메모장 활용
    1. 메모장으로 json 파일 열기
    2. “다른 이름으로 저장” 클릭
    3. 인코딩을 UTF8로 선택 후 저장

Fixtures 파일을 직접 만들지 말 것

반드시 dumpdata 명령어를 사용하여 생성

Improve query

query 개선하기

⇒ 같은 결과를 얻기 위해 DB 측에 보내는 query 개수를 점차 줄여 조회하기

사전 준비

  • fixtures 데이터
    • 게시글 10개/ 댓글 100개/ 유저 5개
  • 모델 관계
    • N:1 - Article:User / Comment:Article / Comment:Article
    • N:M - Article:User
$ python manage.py migrate
$ python manage.py loaddata users.json articles.json comments.json

annotate

SQL의 GROUP BY를 사용

문제 상황

  • “11 queries including 10 similar”
  • 각 게시글마다 댓글 갯수를 반복 평가 하기 때문
<!-- index_1.html -->
<p> 댓글 개수 : {{article.comment_set.count }} <p>

문제 해결

  • 게시글을 조회하면서 댓글 개수까지 한번에 조회해서 가져오기
  • “1 query”
# views.py
def index_1(request):
articles = Article.objects.order_by('-pk')
=> articles = Article.objects.annotate(Count('comment')).order_by('-pk')
<!-- index_1.html -->
<p> 댓글 개수 : {{article.comment__count }} <p>

select_related

  • SQL의 INNER JOIN를 사용
  • 1:1 또는 N:1 참조 관계에서 사용

문제 상황

  • “11 queries including 10 simillar and 8 duplicates”
  • 각 게시글마다 작성한 유저명까지 반복 평가 하기 때문
<!-- index_2.html -->
{% for article in articles %}
  <h3>작성자 : {{ article.user.username }}</h3> <--------- 이 부분은 유지
  <hr>
{% endfor %}

문제 해결

  • 게시글을 조회하면서 유저 정보까지 한번에 조회해서 가져오기
  • “1 query”
# views.py
def index_2(request):
    articles = Article.objects.order_by('-pk')
    => articles = Article.objects.select_related('user').order_by('-pk')

prefetch_related

  • M:N 또는 N:1 역참조 관계에서 사용
  • SQL 이 아닌 Python을 사용한 JOIN을 진행

문제 상황

  • “11 queries including 10 simmilar”
  • 각 게시글 출력 후 각 게시글의 댓글 목록까지 개별적으로 모두 평가
<!-- index_3.html -->
<p>댓글 목록</p>
{% for comment in article.comment_set.all %}
  <p>{{ comment.content }}</p>
{% endfor %}

문제 해결

  • 게시글을 조회하면서 참조된 댓글까지 한번에 조회해서 가져오기
  • “2 queries”
# views.py
def index_3(request):
    articles = Article.objects.order_by('-pk')
    => articles = Article.objects.prefetch_related('comment_set').order_by('-pk')

select_related & prefetch_related

문제 상황

  • “111 queries including 110 similar and 100duplicates”
  • “게시글” + “각 게시글의 댓글 목록” + “댓글의 작성자”를 단계적으로 평가

문제 해결

  1. prefetch_related 적용
    • 문제 해결 1단계
      • 게시글을 조회하면서 참조된 댓글까지 한번에 조회
      articles = Article.objects.order_by('-pk')
          => articles = Article.objects.prefetch_related('comment_set').order_by('-pk')
      
    • 102 queries including 100 similar and 100 duplicates
  2. select_related & prefetch_related 적용
    • 문제 해결 2단계
      • 게시글 + 각 게시글의 댓글 목록 + 댓글의 작성자를 한번에 조회
      articles = Article.objects.order_by('-pk')
      
      => articles = Article.objects.prefetch_related('comment_set').order_by('-pk')
      
      =>
      articles = Article.objects.prefetch_related(
          Prefetch('comment_set', queryset=Comment.objects.select_related('user'))
      ).order_by('-pk')
      
    • 2 queries

섣부른 최적화는 악의 근원

위의 내용들은 작은 효율성에 속한다. 섣부른 최적화는 모든 악의 근원이다.

'TIl' 카테고리의 다른 글

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