1. Basic
가. 주요 구성요소
1) ViewSets
•
복수개의 View를 그룹화한 개념
2) Serializer
•
DRF에서는 모델의 데이터를 Json 형태로 변환 할 수 있도록 함
•
장고의 Form은 html 생성 vs DRF의 Serializer는 JSON 생성
3) Router
•
path 설정 주의사항: urlpatterns의 path 중 중복된 경로가 있으면 가장 많이 중복되는 path는 하위에 위치시킨다
•
Router 설정은 ViewSet에만 해당되며 APIView, GenericView 등은 일반 django path method 활용하여 routing 설정함
# viewset + router
router.register(r'cattles/sales/reports', CattleSaleReportViewSet)
# genericview + path
path('cattles/sales/form', FileUploadView.as_view(), name='file_upload_view'),
Python
복사
나. Pagination
1) 페이지 설정
# tutorial/settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
Python
복사
2. ViewSet 상세
가. Views
1) 상속관계
•
View → APIView
class View:
Python
복사
class APIView(View):
Python
복사
나. Generic views
1) 상속관계
•
View → APIView → GenericAPIView → GenericViewSet
class GenericAPIView(views.APIView):
Python
복사
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
Python
복사
다. Mixins
1) RetrieveModelMixin, ListModelMixin
•
이하 다른 ModelMixin과 달리 perform_* 메소드는 정의돼있지 않음
class ListModelMixin:
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class RetrieveModelMixin:
"""
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
Python
복사
2) CreateModelMixin
•
request.data 외에 request의 특정 값을 이용하여 데이터 갱신시 유용하게 사용 가능
•
perform_create 메소드를 overriding하는 방향 권장
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Python
복사
class CreateModelMixin:
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
Python
복사
3) UpdateModelMixin
•
perform_update(self, serializer) 제공
class UpdateModelMixin:
"""
Update a model instance.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
Python
복사
4) DestroyModelMixin
•
perform_destroy(self, instance) 제공
class DestroyModelMixin:
"""
Destroy a model instance.
"""
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
def perform_destroy(self, instance):
instance.delete()
Python
복사
라. Viewsets
1) 상속관계
•
View → APIView → GenericAPIView → GenericViewSet → ModelViewSet
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
Python
복사
3. Serializers 상세
가. Serializer 역할
1) Converting data
•
QuerySet, 모델 인스턴스를 파이썬 데이터 타입(dictionary 등)으로 변환
→ 결과적으로 JSON, XML 등으로 쉽게 렌더링하는 것이 목적
2) Validating the incoming data
•
외부로부터 들어오는 데이터에 대해 유효 여부를 확인
→ 유효성이 검증된 데이터에 대해 deserializing
나. Serializer 동작 방식
1) Serializing
•
모델 인스턴스를 Serializer 클래스의 인스턴스로 전환
→ 모델 데이터를 파이썬 타입 데이터(Dictionary)로 Converting
serializer = CommentSerializer(comment)
serializer.data
# {'email': 'leila@example.com', 'content': 'foo bar', 'created': '2016-01-27T15:17:10.375877'}
Python
복사
•
파이썬 타입 데이터를 JSON 형태로 렌더링
from rest_framework.renderers import JSONRenderer
json = JSONRenderer().render(serializer.data)
json
# b'{"email":"leila@example.com","content":"foo bar","created":"2016-01-27T15:17:10.375877"}'
Python
복사
2) Deserializing
•
입력 데이터를 JSON 형태로 파싱
import io
from rest_framework.parsers import JSONParser
stream = io.BytesIO(json)
data = JSONParser().parse(stream)
Python
복사
•
JSON 형태에서 파이썬 데이터 타입으로 복원
serializer = CommentSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# {'content': 'foo bar', 'email': 'leila@example.com', 'created': datetime.datetime(2012, 08, 22, 16, 20, 09, 822243)}
Python
복사
다. 유효성 검사
1) 예외 발생 방식
•
역직렬화(deserializing) 시 입력 데이터에 접근 또는 입력 데이터 저장 시 is_valid() 호출
→ 입력 데이터가 유효하지 않다면 .errors에 에러 메시지 저장
serializer = CommentSerializer(data={'email': 'foobar', 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'email': ['Enter a valid e-mail address.'], 'created': ['This field is required.']}
Python
복사
•
is_valid() 에 raise_exception 플래그를 활성화하면 HTTP 400 Bad Request 반환
# Return a 400 response if the data was invalid.
serializer.is_valid(raise_exception=True)
Python
복사
2) field level validation
•
Serializer 내 각각의 필드에 대해 유효성 검사 정의 시 활용
→ validate_<field_name> 형태로 메소드 정의
from rest_framework import serializers
class BlogPostSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)
content = serializers.CharField()
def validate_title(self, value):
"""
Check that the blog post is about Django.
"""
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return value
Python
복사
•
Serializer 내 필드 인스턴스에 대해 유효성 메소드 할당 가능
def multiple_of_ten(value):
if value % 10 != 0:
raise serializers.ValidationError('Not a multiple of ten')
class GameRecord(serializers.Serializer):
score = IntegerField(validators=[multiple_of_ten])
...
Python
복사
3) object level validation
•
Serializer 내 복수의 필드를 활용한 유효성 검사를 정의 시 활용
from rest_framework import serializers
class EventSerializer(serializers.Serializer):
description = serializers.CharField(max_length=100)
start = serializers.DateTimeField()
finish = serializers.DateTimeField()
def validate(self, data):
"""
Check that start is before finish.
"""
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return data
Python
복사
•
QuerySet 내 특정 필드를 대상으로 재사용 가능한 유효성 검사 로직 적용 가능
→ Serializer 내 inner Meta class 부분에 적용 가능
class EventSerializer(serializers.Serializer):
name = serializers.CharField()
room_number = serializers.IntegerField(choices=[101, 102, 103, 201])
date = serializers.DateField()
class Meta:
# Each room only has one event per day.
validators = [
UniqueTogetherValidator(
queryset=Event.objects.all(),
fields=['room_number', 'date']
)
]
Python
복사
4) validators in ModelSerializer
•
Model 객체에 선언된 제약사항에 따라 ModelSerializer 내 자동으로 관련 유효성 검사 생성
>>> from project.example.serializers import CustomerReportSerializer
>>> serializer = CustomerReportSerializer()
>>> print(repr(serializer))
CustomerReportSerializer():
id = IntegerField(label='ID', read_only=True)
time_raised = DateTimeField(read_only=True)
reference = CharField(max_length=20, validators=[<UniqueValidator(queryset=CustomerReportRecord.objects.all())>])
description = CharField(style={'type': 'textarea'})
### 이하와 같이 선언되면 위와 같이 관련 유효성 검사 자동 생성
class CustomerReportRecord(models.Model):
time_raised = models.DateTimeField(default=timezone.now, editable=False)
reference = models.CharField(unique=True, max_length=20)
description = models.TextField()
class CustomerReportSerializer(serializers.ModelSerializer):
class Meta:
model = CustomerReportRecord
Python
복사
•
ModelSerializer 내 자동 생성된 유효성 검사를 사용하지 않고 직접 조작하려면 Meta innner class 내 validators에 빈 리스트를 할당
class BillingRecordSerializer(serializers.ModelSerializer):
def validate(self, attrs):
# Apply custom validation either here, or in the view.
class Meta:
fields = ['client', 'date', 'amount']
extra_kwargs = {'client': {'required': False}}
validators = [] # Remove a default "unique together" constraint.
Python
복사
다. 정방향 관계 vs 역방향 관계
1) 자식 모델 호출 결과 비교
•
•
역방향 관계: 오른쪽 예시, Address(1:N=Reader:Address)
# Book
[
{
"id": 1,
"author": {
"id": 1,
"name": "MJ"
},
"name": "123",
"created_at": "2021-10-22T06:20:32.160442Z"
},
{
"id": 2,
"author": {
"id": 1,
"name": "MJ"
},
"name": "123123",
"created_at": "2021-10-22T06:20:35.061096Z"
},
{
"id": 3,
"author": {
"id": 1,
"name": "MJ"
},
"name": "123123123",
"created_at": "2021-10-22T06:20:37.859903Z"
}
]
Python
복사
# Address
[
{
"detail": "그곳이 그곳",
"city": 1.0
},
{
"detail": "저곳이 저곳",
"city": 2.0
},
{
"detail": "이런곳도 있음",
"city": 3.0
}
]
Python
복사
2) 부모 모델 호출 결과 비교
•
정방향 관계: 왼쪽, Author(1:N=Author:Book)
•
역방향 관계: 오른쪽, Reader(1:N=Reader:Address)
→ 부모 모델에 매칭되는 Serializer 내부에 Serializer Filed를 정의하고 해당 필드에 자식 모델에 매칭되는 Serializer를 맵핑
→ 코드 참고
# Author
[
{
"id": 1,
"name": "MJ"
},
{
"id": 2,
"name": "배만진"
},
{
"id": 3,
"name": "mj"
}
]
Python
복사
# Reader
[
{
"name": "MJ",
"email": "gentlemmj@gmail.com",
"addresses": [
{
"detail": "그곳이 그곳",
"city": 1.0
},
{
"detail": "저곳이 저곳",
"city": 2.0
},
{
"detail": "이런곳도 있음",
"city": 3.0
},
{
"detail": "정말 가고프다",
"city": 123.0
}
]
},
{
"name": "KK",
"email": "123@naver.com",
"addresses": [
{
"detail": "모르겠다",
"city": 4.0
}
]
}
]
Python
복사
라. SerailzierMethodFiled
1) 필드 값 수정
•
Serializer에서 특정 값 수정
→ 특정 값이 None이면 문자열 공백("")으로 반환
class FarmsSerializer(serializers.ModelSerializer):
class Meta:
model = Farms
fields = "__all__"
def get_address1(self, obj):
return obj.address1 if obj.address1 is not None else ""
def get_address2(self, obj):
return obj.address2 if obj.address2 is not None else ""
Python
복사
•
참고) Model of farms
from django.db import models
class Farms(models.Model):
no = models.IntegerField(null=True)
farmname = models.CharField(max_length=100, null=True)
owner = models.CharField(max_length=50, null=True)
farmUniqueNo = models.CharField(max_length=50, null=True)
livestockNo = models.CharField(max_length=50, null=True)
address1 = models.TextField(null=True)
address2 = models.TextField(null=True)
class Meta:
managed = True
db_table = 'farms'
Python
복사
3. S3 + Excel Data
가. Download from S3
1) Access to S3 Object with Pre-Signed URL
@api_view(['GET'])
@transaction.atomic
def download_basic_form_view(request):
object_key = 'temp/temp.xlsx'
try:
response = _get_signed_uri(object_key)
except ClientError as e:
logging.error(e)
return Response({"S3 정보가 올바르지 않습니다."}, status.HTTP_400_BAD_REQUEST)
return Response({"signature-url": response}, status.HTTP_200_OK)
def _get_signed_uri(object_key):
s3_client = boto3.client('s3',
aws_access_key_id='<AWS ACCESS KEY>',
aws_secret_access_key='<AWS SECRET KEY>',
region_name='<AWS RIGION>')
return s3_client.generate_presigned_url('get_object',
Params={'Bucket': '<버킷 이름>',
'Key': object_key},
ExpiresIn=3600)
Python
복사
나. Upload to WAS
1) Upload CSV file
•
아래와 같이 FileUploadSerialier 활용하여 form-data 내 key 값이 file인 file 객체를 가져올 수 있음
@api_view(['POST'])
def file_upload_view(request):
serializer = FileUploadSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
file = serializer.validated_data['file']
decoded_file = file.read().decode()
io_string = io.StringIO(decoded_file)
reader = csv.reader(io_string)
rows = list(reader)
for row in rows[1:]:
public_id = row[0]
date = row[1]
weight = row[2]
destination = row[3]
price = row[4]
memo = row[5]
JavaScript
복사
•
Serializer
class FileUploadSerializer(serializers.Serializer):
file = serializers.FileField()
class Meta:
fields = ('file',)
Python
복사
2) Upload xlsx file
•
Transaction in atomic
@api_view(['POST'])
@transaction.atomic
def upload_excel_view(request):
serializer = FileUploadSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
file = serializer.validated_data['file']
decoded_file = get_data(file)
rows = decoded_file['시트1']
try:
with transaction.atomic():
_save_sales_in_bulk(rows)
except IntegrityError:
raise exceptions.ParseError("엑셀 데이터 업로드에 이상이 있습니다.")
return Response({"status": "success"}, status.HTTP_201_CREATED)
Python
복사
•
Bulk Creat
def _save_in_bulk(rows):
sales = []
rows_length = len(rows[0])
for row in rows[1:]:
public_id, date, weight, destination, price, memo = refine_sale_cells(row, rows_length)
try:
dog = Dog.objects.get(public_id=public_id)
except Dog.DoesNotExist:
raise exceptions.ParseError("id가 올바르지 않습니다.")
cattle.shipment_status = "sale"
cattle.save()
sales.append(DogSale(cattle_id=cattle.id))
DogSale.objects.bulk_create(sales)
Python
복사
4. Exception Handling
가. Exception Basic
1) Globally Handling in View
•
핵심 아이디어: 예외는 결국 에러 처리를 용이하게 하는 것이므로 사용자(프런트엔드 개발자)에게 관련 예외 메시지를 효과적으로 전달할 수 있어야 한다
•
이하 컴포넌트에서 발생한 예외를 View에서 가져와서 Error 메시지로 이어짐
except KeyError:
return JsonResponse({"message": "KEY_ERROR"}, status=400)
except BlankFieldException as e:
return JsonResponse({"message": e.__str__()}, status=400)
except User.DoesNotExist:
return JsonResponse({"message": "INVALID_USER"}, status=400)
except Exception as e:
return JsonResponse({"message": "UNKNOWN_ERROR"}, status=400)
Python
복사
code from https://velog.io/@pm1100tm/PythonDjango-1%EB%B2%88%EC%A7%B8-%EC%BD%94%EB%93%9C-%EB%A6%AC%EB%B7%B0
나. Custom Exception
5. know-hows
가. Reverse relationships in serializers
1) 사용 목적
•
1:N 관계에서 부모 테이블 기준으로 연관된 자식 테이블의 값을 포함해서 전달
→ 자식 테이블에는 부모 테이블의 PK를 FK로 갖지만 부모 테이블에는 자식 테이블과의 접점이 없으므로 역방향 관계를 활용
2) 사용 방법
•
Serializer에 자식 테이블과 연관된 Serializer에 대해 Serializerㅏiled로 할당
•
1:N 관계이므로 'many=True' 설정
•
조회 요청에 따른 응답값으로 추가된 필드이므로 'read_only=True' 설정
결과
3) 활용
•
큰 주제를 중심으로 하위 주제를 묶은 형태로 Response에 담아서 전달할 때 사용
나. Read-only derived data to the response
1) 사용 목적
•
모델 내 값을 활용하여 추가적인 데이터를 Response로 전달
2) 사용 방법
•
Serializer에서 SerializerMethodField 선언
•
SerializerMethodField의 구현부에서 모델 내 값을 활용하여 필요한 데이터 연산 후 반환
•
SerializerMethodField 의 구현부에는 매개변수로 self 외 오직 해당 모델에 대한 변수만 허용된다
→ 이하에서 book 외에 author(book의 부모 모델)는 매개변수로 전달할 수 없다
def get_book_display_name(self, book):
return book.name.upper()
Python
복사
다. Serializer context
1) 목적
•
Serializer에서 데이터 조작에 필요한 추가적인 데이터를 view에서 받아오는 방법
2) 사용법
•
view 내부에 serializer에서 context를 가져갈 수 있도록 관련 메소드를 정의함
•
serializer에서 context 반환 메소드(view에서 정의)를 활용하여 필요한 데이터 추출
•
코드 참고
3) 활용
•
Serializer에 추가한 필드 갱신 시 request 정보를 view로부터 받아서 조작이 필요한 경우
•
Serializer에서 특정 값의 유효성 검사 시 view나 model에서 추가적인 데이터가 필요한 경우
Reference
•
•
SerailzierMethodFileds, https://eunjin3786.tistory.com/268
•
Dealing with nested objects, https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects
•
multi FKs in request path, https://medium.com/swlh/using-nested-routers-drf-nested-routers-in-django-rest-framework-951007d55cdc
•
drf-writable-nested
•
•
serializer context 활용, https://velog.io/@langssi/DRF-sideproject
•
•