DRF 특강
역참조
😠 정의
- 외래키를 사용해 참조하는 object를 역으로 찾을 수 있다.
- 외래키 지정 시 아래와 같이
related_name
을 설정하게 되면, 역참조 시 해당 이름을 사용할 수 있다.
hobby = models.ManyToManyField(Hobby, verbose_name="취미", related_name="user_hobby")
hobby.user_hobby
related_name
을 설정하지 않았다면 테이블명_set
을 통해 역참조를 진행하면 된다.
hobby = models.ManyToManyField(Hobby, verbose_name="취미")
hobby.userprofile_set
😠 역참조 미사용 시 코드
- 헷갈림을 방지하기 위해 테이블의 구조를 먼저 살펴보자.
class User(AbstractBaseUser):
username = models.CharField("사용자 계정", max_length=50, unique=True)
password = models.CharField("비밀번호", max_length=128)
email = models.EmailField("이메일", max_length=100)
name = models.CharField("이름", max_length=20)
join_data = models.DateTimeField("가입일자", auto_now_add=True)
class Hobby(models.Model):
name = models.CharField("취미 이름", max_length=50)
def __str__(self):
return self.name
class UserProfile(models.Model):
user = models.OneToOneField(User, verbose_name="사용자", on_delete=models.CASCADE)
introduction = models.TextField("자기소개")
birth = models.DateField("생일")
age = models.IntegerField("나이")
hobby = models.ManyToManyField(Hobby, verbose_name="취미")
def __str__(self):
return f'{self.user.username} 님의 프로필'
- 사용자 정보를 조회할 때 역참조를 사용하지 않는다면 이전에 했던 것과 같이 찾아와야 한다.
class UserView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
user_profile = UserProfile.objects.get(user=user)
hobbys = user_profile.hobby.all()
hobbys = str(hobbys)
return Response({"hobbys": hobbys})
- 그러나 역참조를 사용하게 되면 아래와 같이 간단하게 작성할 수 있다.
class UserView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
hobbys = user.userprofile.hobby.all()
hobbys = str(hobbys)
return Response({"hobbys": hobbys})
😠 dir 사용
- 위의 코드를 치고 서버를 돌리면 아래의 빨간 박스에 보이는 에러가 나게 된다.
- 에러를 고쳐보기 위해
dir
를 사용해 보았다.
- dir을 사용하게 되면 내가 user라는 모델 안에서 사용할 수 있는 기능을 전부 볼 수 있다.
class UserView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
print(dir(user))
return Response({})
- 결과
- 우리가 모델을 만들면서 사용했던 친숙한 이름, 에러 이름, 그리고 우리가 사용하려 했던
userprofile
또한 잘 보인다. 즉 코드에는 큰 문제는 없어 보인다.
- admin 페이지에 혹시나 해서 들어가 보았다. 역시나 내가 일반 유저의 프로필은 작성했는데, 현재 로그인되어 있는
admin user의 계정으로 된 프로필을 작성하지 않았었다.
따라서 프로필을 작성해 주었고, 다시 포스트맨을 돌려보았다.
- 내가 작성한데로 취미가 잘 불러와진 것을 알 수 있다.
😠 역참조 다시 한 번 뜯어보기
- 위의 사진처럼 우리는 이미 성공했다. 그러나 다시 한 번 코드를 리마인드하는 겸해서 조금씩 뜯어서 포스트맨에게 보내기로 해보자.
- 첫 번째
class UserView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
hobbys = user.userprofile
hobbys = str(hobbys)
return Response({"hobbys": hobbys})
- 위의 코드를 실행해보면 아래의 결과가 나온다. 우리의 의도대로 역참조가 잘 된 것을 볼 수 있다. 우리는 지금 로그인한 유저의 프로필을 가져오기 위해 위의 코드를 작성한 것이고, 지금 로그인되어 있는
geun
의 프로필이 잘 실려온 것을 볼 수 있다.
- 여기서 궁금해할 수도 있다.
도대체 왜 'geun 님의 프로필'이지?
. 이에 대한 해답은 내가 맨 위에 올려놓은 모델에서 찾을 수 있다.
__str__
부분에 적어놓은 것 때문에 위와 같이 나오게 된다.
class UserProfile(models.Model):
user = models.OneToOneField(User, verbose_name="사용자", on_delete=models.CASCADE)
introduction = models.TextField("자기소개")
birth = models.DateField("생일")
age = models.IntegerField("나이")
hobby = models.ManyToManyField(Hobby, verbose_name="취미")
def __str__(self):
return f'{self.user.username} 님의 프로필'
- 주의해야할 점은 User 모델에는 UserProfile의 내용이 전혀 없다.
OneToOneField
이기 때문에 역참조로 내용을 가져올 수 있는 것이며, OneToOneField
이기 때문에 _set
을 붙이지 않아도 되는 것이다.
- 두 번째
class UserView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
hobbys = user.userprofile.hobby
hobbys = str(hobbys)
print(dir(hobbys))
return Response({"hobbys": hobbys })
- 위의 결과는 아래의 사진과 같다.
- 역참조 시 아무런 조건을 달지 않으면 아래와 같은 메세지가 나온다. 역참조가 실패한 것이 아니다!
- dir의 결과는 아래와 같다.
- 우리가 쓰려하는 all이 보이는 것을 볼 수 있다!
hobby
에서 우리가 .
을 찍고 쓸 수 있는 것들이다.
- dir를 통해 나온 값들에는 여러 값들이 섞여 있다. 앞에 던더가 붙거나 언더바가 하나 붙은 것들을 다 지우고, 나머지 값들을 한 번 살펴보자. 여기에는 함수 또는 변수들이 들어있다. 여기서 어떤 것을 우리가 쓸 수 있고, 그것의 결과가 어떻게 나오는지 아래의 코드를 통해 프린트해 볼 수 있을 것이다.
def get(self, request):
user = request.user
hobbys = user.userprofile.hobby
hobbys = str(hobbys)
print(dir(hobbys))
for command in dir(hobbys):
try:
print(f"command : {command} / ", eval(f"hobbys.{command}()")
print(f"command : {command} / ", eval(f"hobbys.{command}")
except:
pass
return Response({"hobbys": hobbys })
- 이제 위에서 우리가 확인한
all()
을 뒤에 붙여 쓰게 되면 우리가 코드를 뜯어보기 전에 얻었던 결과에 도달할 수 있다.
😠 역참조를 통해 나와 비슷한 취미를 가진 유저 찾기
- 위의 코드에서 추가적으로 나와 비슷한 취미를 가진 유저를 찾는 코드를 더해보도록 하자.
- for문을 돌리기 전에 내가 뭘 쓸 수 있을지 궁금하다 싶으면 어떤 것을 해야하는가??
print(dir(hobby))
를 하면 된다!
print(dir(hobby))
를 해보면 userprofile_set
가 존재한다. 그러므로 뒤에 붙여다가 써도 된다.
- hobby에서 userprofile을 역참조 하였고, 그 결과에서
exclude(user=user)
를 사용해 현재 로그인해 있는 user는 결과값에서 제외하였다.
class UserView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
hobbys = user.userprofile.hobby.all()
for hobby in hobbys:
hobby_members = hobby.userprofile_set.exclude(user=user).annotate(username=F('user__username')).values_list('username', flat=True)
hobby_members = list(hobby_members)
print(f"hobby : {hobby.name} / hobby members : {hobby_members}")
return Response({"hobbys": str(hobbys)})
- 아래와 같이 겹치는 취미를 가진 user를 뽑아낼 수 있다.
- 위의
hobby_members
를 하나씩 뜯어보겠다.
1
: 뒤에 get이나 all, filter 등의 쿼리를 붙여주면 해당하는 결과값이 나오게 된다. 아무런 값이 붙지 않아 None이 붙어있는 것!
2
: 현재 로그인한 사용자인 admin을 제외한 모든 사용자의 프로필 값들이 쿼리셋으로 불러와진다.
3
: 현재 값에서는 annotate로 결과가 달라지지는 않는다.
4
: values.list
를 사용해서 원하는 값들을 뽑아올 수 있다. 우리는 user profile 안에 있는 user 안에 있는 username의 오브젝트값을 불러왔고, values.list
안에 있는 username 값을 출력한 것이다.
5
: flat=True
를 통해 해당 값들을 쿼리셋 안에서 리스트로 뽑은 것이다.
6
: 위의 값을 쿼리셋이 아닌 리스트의 형태로 뽑았다.
for hobby in hobbys:
print(f'1 : {hobby.userprofile_set.all()}')
print(f'2 : {hobby.userprofile_set.exclude(user=user)}')
print(f"3 : {hobby.userprofile_set.exclude(user=user).annotate(username=F('user__username'))}")
print(f"4 : {hobby.userprofile_set.exclude(user=user).annotate(username=F('user__username')).values_list('username')}")
print(f"5 : {hobby.userprofile_set.exclude(user=user).annotate(username=F('user__username')).values_list('username', flat=True)}")
print(f"6 : {list(hobby.userprofile_set.exclude(user=user).annotate(username=F('user__username')).values_list('username', flat=True))}")
break
Serializer
😠 사용 방법
- 먼저,
user/serializers.py
를 생성한다.
- 그리고 아래와 같이 import를 진행해야 한다.
from rest_framework import serializers
- 이제 UserSerializer를 먼저 만들어보도록하겠다. serializer에서 제일 중요한 것은
Meta class
이다. 일단 아래와 같이 세팅한다.
from rest_framework import serializers
from .models import User as UserModel
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = UserModel
fields = "__all__"
- 그리고
user/views.py
로 가서 serializer를 사용해보자.
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import permissions
from django.db.models import F
from django.contrib.auth import login, authenticate, logout
from .serializers import UserSerializer
def get(self, request):
return Response(UserSerializer(request.user).data)
- 이렇게 작성하고 포스트맨으로 돌아가 실행해보도록 하겠다.
- 해당 코드의 의미는
fields = "__all__"
이기 때문의 User 모델의 모든 값을 가져다 쓸 수 있는 것이다. 그런데 모든 값이 나오기 때문에 fields = "__all__"
은 잘 사용하지 않는다.
- 대신 가져오고 싶은 값을 리스트로 작성해 가져오게 된다. 그래서
user/serializers.py
의 코드를 수정해보았다.
from rest_framework import serializers
from .models import User as UserModel
from .models import UserProfile as UserprofileModel
from .models import Hobby as HobbyModel
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = UserModel
fields = ["username", "email", "name", "join_data"]
- 위의 결과값이 잘 불러와지는 것을 볼 수 있다.
- 그런데 우리는 좀 더 자세한 값을 알고 싶다. 그래서 프로필에서 정보를 가져와보자.
fields
에는 아무 값이나 담을 수 없다. 모델에서 가지고 올 수 있는 값만 적을 수 있다. 역참조도 담아서 활용할 수 있다.
- 유저 프로필의 시리얼라이저도 생성은 했으나, 가져다 쓰려면 언급을 해주어야 한다.
- 우리는 views.py에 언급하는 대신, 아래에 있는
UserSerializer
에서 역참조를 활용해 사용해보자.
- 아래와 같이 fields에 역참조에서 불러왔던 것처럼 userprofile을 적어주고, 해당 시리얼라이저 안에서 userprofile을 선언해주면 된다.
from rest_framework import serializers
from .models import User as UserModel
from .models import UserProfile as UserProfileModel
from .models import Hobby as HobbyModel
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfileModel
fields = ["introduction", "birth", "age"]
class UserSerializer(serializers.ModelSerializer):
userprofile = UserProfileSerializer()
class Meta:
model = UserModel
fields = ["username", "email", "name", "join_data", "userprofile"]
- 포스트맨을 실행해보니 결과값이 너무 이쁘다.
- 그런데 또 보다보니 hobby 값도 가져다가 보고 싶은 욕심이 생긴다.
from rest_framework import serializers
from .models import User as UserModel
from .models import UserProfile as UserProfileModel
from .models import Hobby as HobbyModel
class HobbySerializer(serializers.ModelSerializer):
class Meta:
model = HobbyModel
fields = ["name"]
class UserProfileSerializer(serializers.ModelSerializer):
hobby = HobbySerializer()
class Meta:
model = UserProfileModel
fields = ["introduction", "birth", "age", "hobby"]
class UserSerializer(serializers.ModelSerializer):
userprofile = UserProfileSerializer()
class Meta:
model = UserModel
fields = ["username", "email", "name", "join_data", "userprofile"]
- 부푼 마음을 가지고 포스트맨을 보면 결과가 참담하다. 이름값이 나오지 않는다.
- 안되는 이유를 알아보자.
UserProfile
은 User
와 OneToOne
관계이기 때문에 object
로 반환된다. 왜냐면 어짜피 결과가 하나이기 때문이다.
- 반면,
UserProfile
과 Hobby
는 ManyToMany
관계라 QuerySet
으로 반환된다. 관계의 이름에서 유추할 수 있듯이 여러 개의 결과가 object로 생성되게 되며, 이를 쿼리셋으로 묶어서 보내준다.
- 그렇다면 어떻게 해결할 수 있을까?
- 아래와 같이 다대다 관계를 가진 시리얼라이저의 괄호 안에
many=True
를 넣어주면 된다.
from rest_framework import serializers
from .models import User as UserModel
from .models import UserProfile as UserProfileModel
from .models import Hobby as HobbyModel
class HobbySerializer(serializers.ModelSerializer):
class Meta:
model = HobbyModel
fields = ["name"]
class UserProfileSerializer(serializers.ModelSerializer):
hobby = HobbySerializer(many=True)
class Meta:
model = UserProfileModel
fields = ["introduction", "birth", "age", "hobby"]
class UserSerializer(serializers.ModelSerializer):
userprofile = UserProfileSerializer()
class Meta:
model = UserModel
fields = ["username", "email", "name", "join_data", "userprofile"]
- 또다시 포스트맨으로 돌아가보자. 아주 이쁘다.
- 취미를 이쁘게 잘 뽑았으니, 이제
누가 나와 같은 취미를 가졌는가
에 대해서도 보여주고 싶어졌다.
- 해당 작업을 하려면
HobbySerializer
를 수정해주면 된다.
- 앞에서 나는 fields 안에는 모델 안에 존재하는 요소만 작성해야 한다고 했다. 그러나 이 작업은 모델에 있는 내용으로는 마무리할 수 없다.
- 따라서 나는
serializerMethodField()
를 사용할 것이다. 우선 아래와 같이 작성해 포스트맨으로 정상적인지 확인해보자.
serializerMethodField()
를 사용하게 된다면, 꼭 get_same_hobby_user
와 같이 get_변수명
을 함수로 만들어주어야 한다. 이렇게하지 않으면 에러가 난다.
- 그리고
get_same_hobby_user
의 인자로 들어가는hobby model의 obj의 값과 타입을 찍어보았다.
from rest_framework import serializers
from .models import User as UserModel
from .models import UserProfile as UserProfileModel
from .models import Hobby as HobbyModel
class HobbySerializer(serializers.ModelSerializer):
same_hobby_user = serializers.SerializerMethodField()
def get_same_hobby_user(self, obj):
user_list = []
print(obj, type(obj))
return "Test!!"
class Meta:
model = HobbyModel
fields = ["name", "same_hobby_user"]
class UserProfileSerializer(serializers.ModelSerializer):
hobby = HobbySerializer(many=True)
class Meta:
model = UserProfileModel
fields = ["introduction", "birth", "age", "hobby"]
class UserSerializer(serializers.ModelSerializer):
userprofile = UserProfileSerializer()
class Meta:
model = UserModel
fields = ["username", "email", "name", "join_data", "userprofile"]
- obj의 값과 타입이 올바르게 찍혀있는 것을 볼 수 있다. 이제 나는 obj를 가지고 무엇을 할 수 있는지 알아내기 위해
print(dir(obj))
를 해보도록 하겠다.
- 그리고 우리는 아주 값진 녀석을 건졌다. 바로
userprofile_set
이다. 즉, hobby의 obj에서 역참조로 userprofile의 값을 가져올 수 있는 것이고, 이는 바로 해당 hobby를 가지고 있는 user의 정보를 가져올 수 있다는 뜻이다.
- 위에서 알아낸 내용을 바탕으로 코드를 수정하였다.
from rest_framework import serializers
from .models import User as UserModel
from .models import UserProfile as UserProfileModel
from .models import Hobby as HobbyModel
class HobbySerializer(serializers.ModelSerializer):
same_hobby_user = serializers.SerializerMethodField()
def get_same_hobby_user(self, obj):
user_list = []
for user_profile in obj.userprofile_set.all():
user_list.append(user_profile.user.username)
return user_list
class Meta:
model = HobbyModel
fields = ["name", "same_hobby_user"]
class UserProfileSerializer(serializers.ModelSerializer):
hobby = HobbySerializer(many=True)
class Meta:
model = UserProfileModel
fields = ["introduction", "birth", "age", "hobby"]
class UserSerializer(serializers.ModelSerializer):
userprofile = UserProfileSerializer()
class Meta:
model = UserModel
fields = ["username", "email", "name", "join_data", "userprofile"]
- 포스트맨을 확인해보자. 아주 좋다. 여기의 단점은 admin 유저, 즉 지금 로그인해 있는 user를 exclude를 통해 제거하지 않은 점이다.
- 여기서 로그인한 사용자와 취미가 똑같은 사람을 보는 것이 아닌, 모든 사용자에 대한 해당 값을 가져오고 싶다면 views.py를 어떻게 바꿔야 할까?
- 아래와 같이 진행하면 되고, 리턴값의
.data
가 결과값을 딕셔너리 형태로 바꿔주고, Response
가 변경된 딕셔너리 값을 JSON
형태로 바꿔서 반환해 주는 것이다.
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import permissions
from django.db.models import F
from django.contrib.auth import login, authenticate, logout
from .serializers import UserSerializer
from .models import User as UserModel
from .models import UserProfile as UserProfileModel
from .models import Hobby as HobbyModel
class UserView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
all_user = UserModel.objects.all()
return Response(UserSerializer(all_user, many=True).data)
- 포스트맨을 확인해보면 다른 유저들에 대한 값도 나온 것을 확인할 수 있다. 나는 user3의 값을 확인해보았다.
😠 같은 의미 다른 방법
- 만약 원래
userprofile
이었던 변수명을 user_detail
로 바꿔서 사용하고 싶다면, 아래와 같이 설정해주면 된다.
class UserSerializer(serializers.ModelSerializer):
user_detail = UserProfileSerializer(source="userprofile")
class Meta:
model = UserModel
fields = ["username", "email", "name", "join_data", "user_detail"]
- 포스트맨에서도 이름이 바뀐 것을 볼 수 있다.
- HobbySerializer에서 우리는 취미가 같은 유저를 뽑아서 보여줬다. 그런데 이 방법 말고도 바로 역참조를 필드에 넣어서 활용 가능하며, 이것을 가공해 사용할 수도 있다.
class HobbySerializer(serializers.ModelSerializer):
class Meta:
model = HobbyModel
fields = ["name", "userprofile_set"]
- 포스트맨에서 확인해보자.
Permissions
😠 종류
- permissions는 종류가 아주 많다. 어제 공부할 때는 주로 사용했던 3개의 permissions에 대해서만 봤었다.
- 이제 모든 친구들을 보러가보자.
- permissions를 찾아서
⌘
+ 클릭
해보자.
- 어제 봤던 3개는 위로 올리고 그 아래의 녀석들을 캡쳐해봤다. 코드가 총 300줄이니 아래 더 많이 남아있다.
😠 커스텀 permissions
- 이제 나는 유저 모델과 동일하게 permissions 모델도 커스텀화해보도록 하겠다.
- 먼저 위에서 확인했던 여러 퍼미션들 중에서 마음에 드는 친구를 하나 골라온다. 나는 이 녀석을 선택했다.
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)
- 위의 내용을
django_rest_framework/permissions.py
파일을 생성한 뒤 복붙해주고, BasePermission
을 사용할 수 있도록 import를 해주면 된다. 여기서 나는 class의 이름을 변경해 주었다.
from rest_framework.permissions import BasePermission
class MyAuthenticate(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)
- 이제 이 permission을 사용해보자.
- 나는 이 permission을
user/views.py
에서 사용할 것이다. 따라서 import를 진행해보도록 하겠다. 그리고 이전에 permission을 사용하던 것과 같은 폼으로 사용을 선언해주면 된다.
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import permissions
from django.db.models import F
from django.contrib.auth import login, authenticate, logout
from .serializers import UserSerializer
from .models import User as UserModel
from .models import UserProfile as UserProfileModel
from .models import Hobby as HobbyModel
from django_rest_framework.permissions import MyAuthenticate
class UserView(APIView):
permission_classes = [MyAuthenticate]
def get(self, request):
return Response(UserSerializer(request.user).data)
- 어떻게 적용하는지 알아냈으니 permission의 내용을 수정하도록 하겠다. 나는 가입한지 일주일이 넘는 사용자만 활동할 수 있도록 권한을 부여하고 싶다.
django_rest_framework/permissions.py
from rest_framework.permissions import BasePermission
from datetime import datetime, timedelta
class MyAuthenticateOverWeek(BasePermission):
def has_permission(self, request, view):
user = request.user
if not user or not user.is_authenticated:
return False
print(f'user join date : {user.join_data}')
print(f'now date : {datetime.now().date()}')
print(f'a week ago date : {datetime.now().date() - timedelta(days=7)}')
return bool(user.join_data < (datetime.now().date() - timedelta(days=7)))
- 위의 코드를 테스트할 때는
days=7
을 minutes=5
와 같이 바꿔서 테스트를 꼭 진행해봐야 한다.
- 위의 코드는 가입일자를 DateField로 생성했을 때의 이야기이다. 만약 나처럼 DateTimeField로 생성했다면 아래와 같이 진행하면 된다.
from rest_framework.permissions import BasePermission
from datetime import timedelta
from django.utils import timezone
class MyAuthenticateOverWeek(BasePermission):
def has_permission(self, request, view):
user = request.user
if not user or not user.is_authenticated:
return False
print(f'user join date : {user.join_data}')
print(f'now date : {timezone.now()}')
print(f'a week ago date : {timezone.now() - timedelta(days=7)}')
return bool(user.join_data < (timezone.now() - timedelta(days=7)))