팔로우 기능 구현
프로필 페이지
- 각 회원의 개인 프로필 페이지에 팔로우 기능을 구현하기 위해 프로필 페이지를 먼저 구현하기
# 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가 있다고 생각해보기
- A가 먼저 프로젝트를 작업 후 원격 저장소에 push 진행
- gitignore로 인해 DB는 업로드하지 않기 때문에 A가 생성한 데이터도 업로드 X
- B가 원격 저장소에서 A가 push한 프로젝트를 pull (혹은 clone)
- 결과적으로 B는 DB가 없는 프로젝트를 받게 됨
- A가 먼저 프로젝트를 작업 후 원격 저장소에 push 진행
- 이처럼 프로젝트의 앱을 처음 설정할 때 동일하게 준비 된 데이터로 데이터베이스를 미리 채우는 것이 필요한 순간이 있음
⇒ 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
- dumpdata 시 추가 옵션 작성
$ python -Xutf8 manage.py dumpdata [생략]
- 메모장 활용
- 메모장으로 json 파일 열기
- “다른 이름으로 저장” 클릭
- 인코딩을 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”
- “게시글” + “각 게시글의 댓글 목록” + “댓글의 작성자”를 단계적으로 평가
문제 해결
- 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
- 문제 해결 1단계
- 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
- 문제 해결 2단계
섣부른 최적화는 악의 근원
위의 내용들은 작은 효율성에 속한다. 섣부른 최적화는 모든 악의 근원이다.