DRF (Django Rest Frame Work)

이민기·2022년 6월 20일
0
post-thumbnail

외래 키에 대한 이해

  • 외래 키 종류
    • ForeignKey : many-to-one 형태로 특정 테이블에서 다른 테이블을 참조 할 수 있다,
      • 영화관과 시청자의 관계를 나타 낼 때, 시청자 테이블에서 영화관 테이블을 Foreign Key를 사용해 관계를 맺을 수 있다.
    • OneToOneField : one-to-one 형태로 ForeignKey와 동일하지만, 1:1 관계만 가능하다.
      • 사용자 계정 테이블과 사용자 프로필 테이블이 별도로 존재 할 때, 계정 테이블을 프로필에서 1:1로 관계를 맺을 수 있다.
    • ManyToManyField : many-to-many 형태로 한 개의 필드에서 여러개의 테이블을 참조 할 수 있다.
      • 영화라는 테이블에서 카테고리 테이블의 object를 참조하고 싶을 때, many to many 관계를 사용해 2개 이상의 object를 참조할 수 있다.
      • Many-to-many 관계로 생성 된 필드는 db에 값이 2개 이상 저장되는 것이 아닌, 중간 테이블이 생성된다.
        • 예시)
          • [models.py](http://models.py) 구조
            class UserProfile(models.Model):
                user = models.OneToOneField(to=User, verbose_name="사용자", on_delete=models.CASCADE, primary_key=True)
                hobby = models.ManyToManyField("Hobby", verbose_name="취미")
                introduction = models.TextField("소개")
                birthday = models.DateField("생일")
                age = models.IntegerField("나이")
                
                def __str__(self):
                    return f"{self.user.username} 님의 프로필"
            
            class Hobby(models.Model):
                name = models.CharField("취미", max_length=50)
                
                def __str__(self):
                    return self.name
          • 실제 DB 테이블 구조
  • 역참조에 대한 이해
    • 외래 키를 사용해 참조하는 object를 역으로 찾을 수 있다.
      • 왜래 키 지정 시 related_name 옵션을 사용해 역참조 시 사용될 이름을 지정할 수 있다.
      • releated_name을 지정하지 않는다면 기본적으로 tablename_set 형태로 지정된다.
      • ex1) user_profile.hobby → 정참조
      • ex2) hobby.userprofile_set → hobby를 참조하고 있는 UserProfile 테이블의 object를 져옴
        • models.py에서 releated_name을 user_hobby 로 지정했다면 hobby.user_hobby와 같이 사용

    TIP dir

# python에는 객체에 존재하는 변수, 메소드 등을 출력해주는 dir()이라는 함수가 존재
# 특정 object를 dir(object) 형태로 사용 시 역참조 확인 가능한 항목들을 불러올 수 있음

# models.py
class User(AbstractBaseUser):
    ...

class UserProfile(models.Model):
    user = models.OneToOneField(to=User)
    hobby = models.ManyToManyField("Hobby", verbose_name="취미")
    ...

class Hobby(models.Model):
    ...

# views.py
user = User.objects.get(id=obj_id)
hobby = Hobby.objects.get(id=obj_id)

print(dir(user))
print(dir(hobby))

# result dir(user) print
"""
[..., userprofile, ...]
"""

# result dir(hobby) print
"""
[..., userprofile_set, ...]
"""

# user를 바라보고 있는 UserProfile은 One-to-one 관계기 때문에 _set이 붙지 않음
# hobby를 바라보고 있는 UserProfile은 Many-to-many 관계기 때문에 _set이 붙음
# ForeignKey를 사용했을 떄 또한 _set이 붙게 됨  

역참조 활용하기

def get(self, request):
		user = request.user
		hobbys = user.userprofile.hobby.all()
		for hobby in hobbys:
		    # exclde : 매칭 된 쿼리만 제외, filter와 반대
		    # annotate : 필드 이름을 변경해주기 위해 사용, 이외에도 원하는 필드를 추가하는 등 다양하게 활용 가능
		    # values / values_list : 지정한 필드만 리턴 할 수 있음. values는 dict로 return, values_list는 tuple로 ruturn
		    # F() : 객체에 해당되는 쿼리를 생성함
		    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}")

# result print
"""
hobby : 산책 / hobby members : ['user1']
hobby : 음악감상 / hobby members : ['user1', 'user2']
hobby : 스쿠버다이빙 / hobby members : ['user2']
hobby : 여행 / hobby members : ['user2']
"""

drf의 꽃, serializer 활용

  • serializer란?
    • django의 object, queryset 인스턴스 등 복잡한 테이터들을 json같은 다른 콘텐츠 유형으로 쉽게 변환 할 수 있다.
    • create, update 시 validation 기능을 제공한다.
  • serializer Meta class
    • serializer에서 사용되는 설정 파일이다.
    • model에 사용 될 테이블을 적어주고, field에 사용될 필드를 적어준다.
    • extra_kwargs, read_only_fields와 같은 옵션을 통해 다양한 설정이 가능하다.
      • 자세한 내용은 serializer 심화에서 다룰 예정
  • 기본적인 serializer 사용법

serializers.py

from rest_framework import serializers

class UserSerializer(serializers.ModelSerializer):
   class Meta:
        # serializer에 사용될 model, field지정
        model = User
        # 모든 필드를 사용하고 싶을 경우 fields = "__all__"로 사용
        fields = ["username", "password", "fullname", "email"]

views.py

from rest_framework.response import Response
from rest_framework import status

from user.serializers import UserSerializer

def get(self, request):
    user = request.user
    # serializer에 queryset을 인자로 줄 경우 many=True 옵션을 사용해야 한다.
    serialized_user_data = UserSerializer(user).data
    return Response(serialized_user_data, status=status.HTTP_200_OK)

# return data
"""
{
    "username": "user",
    "password": "pbkdf2_sha256$320000$u5YnmKo9luab9csqWpzRsa$pKfqHnBiF5Rgdo1Mj9nxNOdhpAl9AhPVXFPXkbPz7Mg=",
    "fullname": "user's name",
    "email": "user@email.com"
}
"""
  • 외래 키 관계에 있는 테이블이 있을 경우, 해당 테이블의 serializer를 생성해 함께 사용할 수 있다.
class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserProfile
        fields = "__all__"

class UserSerializer(serializers.ModelSerializer):
    """
    외래 키는 UserProfile에서 User 테이블로 설정되어 있지만
    one to one 필드기 때문에 userprofile이라는 명칭으로 역참조가 가능하다.
    """
    userprofile = UserProfileSerializer()
    class Meta:
        model = User
        fields = ["username", "password", "fullname", "email", "userprofile"]
  • SerializerMethodField 를 활용해 원하는 필드를 추가하고, 더 나아가서 여러 serializer들을 함께 사용할 수 있다.
class HobbySerializer(serializers.ModelSerializer):
    # serializers.SerializerMethodField()를 사용해 원하는 필드를 생성한다.
    same_hobby_users = serializers.SerializerMethodField()
    def get_same_hobby_users(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 = Hobby
        fields = ["name", "same_hobby_users"]

class UserProfileSerializer(serializers.ModelSerializer):
    # 외래 키 관계로 이어져 있는 필드는 Serializer를 바로 호출할 수 있다.
    hobby = HobbySerializer(many=True)

    class Meta:
        model = UserProfile
        fields = "__all__"

class UserSerializer(serializers.ModelSerializer):
    # One-to-one 관계에서는 fk처럼 사용 가능하다.
    userprofile = UserProfileSerializer()

    class Meta:
        model = User
        fields = ["username", "password", "fullname", "email", "userprofile"]

# views.py
...
class UserView(APIView)
    def get(self, request):
        user = request.user
        return Response(UserSerializer(user).data, status=status.HTTP_200_OK)

# response data
"""
{
    "username": "admin",
    "password": "pbkdf2_sha256$320000$u5YnmKo9luab9csqWpzRsa$pKfqHnBiF5Rgdo1Mj9nxNOdhpAl9AhPVXFPXkbPz7Mg=",
    "fullname": "zxcv",
    "email": "zxv@asd.com",
    "userprofile": {
        "birthday": "2022-06-08",
        "age": 1,
        "introduction": "asdac",
        "hobby": [
            {
                "name": "독서",
                "same_hobby_users": [
                    "user1",
                    "user2",
                    "user3"
                ]
            }
        ]
    }
}
"""

permission_classes를 활용한 접근 권한 설정

  • view에 접근 할 수 있는 요청을 drf의 permission_classes를 활용해 관리 할 수 있다.
    • permissions.AllowAny : 모든 사용자를 대상으로 접근 허용
    • permissions.IsAuthenticated : 로그인 된 사용자를 대상으로 접근 허용
    • permissions.AllowAny : 모든 사용자를 대상으로 접근 허용
    • 이외에도 다양한 permission class들이 존재
  • permission class 커스텀하기

permission.py
가입일이 1주일 이상 된 사용자만 접근 가능하도록 설정하기

from rest_framework.permissions import BasePermission
from datetime import timedelta
from django.utils import timezone

class RegistedMoreThanAWeekUser(BasePermission):
    """
    가입일 기준 1주일 이상 지난 사용자만 접근 가능
    """
    message = '가입 후 1주일 이상 지난 사용자만 사용하실 수 있습니다.'
    
    def has_permission(self, request, view):
        return bool(request.user and request.user.join_date < (timezone.now() - timedelta(days=7)))

admin은 모든 권한이 있고, 인증 된 사용자는 조회만 가능하도록 설정하기

from rest_framework.permissions import BasePermission
from rest_framework.exceptions import APIException
from rest_framework import status

class GenericAPIException(APIException):
    def __init__(self, status_code, detail=None, code=None):
        self.status_code=status_code
        super().__init__(detail=detail, code=code)

class IsAdminOrIsAuthenticatedReadOnly(BasePermission):
    """
    admin 사용자는 모두 가능, 로그인 사용자는 조회만 가능
    """
    SAFE_METHODS = ('GET', )
    message = '접근 권한이 없습니다.'

    def has_permission(self, request, view):
        user = request.user

        if not user.is_authenticated:
            response ={
                    "detail": "서비스를 이용하기 위해 로그인 해주세요.",
                }
            raise GenericAPIException(status_code=status.HTTP_401_UNAUTHORIZED, detail=response)

        if user.is_authenticated and user.is_admin:
            return True
            
        elif user.is_authenticated and request.method in self.SAFE_METHODS:
            return True
        
        return False

django admin 심화

  • list_display / object 목록에 띄워줄 필드를 지정한다.
    list_display = ('id', 'username', 'fullname')
  • list_display_links / object 목록에서 클릭 시 상세 페이지로 들어갈 수 있는 필드를 지정한다.
    list_display_links = ('username', )
  • list_filter / filter를 걸 수 있는 필드를 생성한다.
    list_filter = ('name', )
  • search_fields / 검색에 사용될 필드를 지정한다.
    search_fields = ('username', )
  • readonly_fields / 상세페이지에서 읽기 전용 필드를 설정할 때 사용된다.
    readonly_fields = ('username', 'join_date', )
  • fieldsets / 상세페이지에서 필드를 분류하는데 사용된다.
    fieldsets = (
            ("info", {'fields': ('username', 'fullname', 'join_date')}),
            ('permissions', {'fields': ('is_admin', 'is_active', )}),
        )
  • Tabulainline / Stackinline 설정
    from django.contrib import admin
    from user.models import User, UserProfile, Hobby
    
    # 사용 방법은 TabulaInline과 StackedInline 모두 동일
    # 둘 다 사용해보고 뭐가 좋은지 비교해보기
    # class UserProfileInline(admin.TabulaInline):
    class UserProfileInline(admin.StackedInline):
        model = UserProfile
    
        def formfield_for_manytomany(self, db_field, request, **kwargs):
            if db_field.name == 'hobby':
                kwargs['queryset'] = Hobby.objects.filter(id__lte=7)
    
            return super().formfield_for_foreignkey(db_field, request, **kwargs)
    
    class UserAdmin(admin.ModelAdmin):
        inlines = (
                UserProfileInline,
            )
    
    admin.site.register(User, UserAdmin)
  • 추가 / 삭제 / 수정 권한 설정
from django.contrib import admin

class UserAdmin(admin.ModelAdmin):
    def has_add_permission(self, request, obj=None): # 추가 권한
        return False

    def has_delete_permission(self, request, obj=None): # 삭제 권한
        return False

    def has_change_permission(self, request, obj=None): # 수정 권한
        return False

admin.site.register(User, UserAdmin)
profile
지나가는사람

0개의 댓글