인프런 커뮤니티 질문&답변

김동혁님의 프로필 이미지
김동혁

작성한 질문수

파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트

관계를 표현하는 모델 필드 (ForeignKey)

외래키 설정을 다르게 하는 경우

작성

·

450

0

방식 1.
class NoticeBoard(models.Model):
title = models.CharField(verbose_name='제목')
files = models.ForeignKey(Files, on_delete=models.CASCADE)
class Files(models.Model):
file = models.FileField(upload_to='files/%Y/%m/%d')
 
방식 2.
class NoticeBoard(models.Model):
title = models.CharField(verbose_name='제목')

class Files(models.Model):
post = models.ForeignKey(Files, on_delete=models.CASCADE)
file = models.FileField(upload_to='files/%Y/%m/%d')
 
 
저는 오히려 방식1처럼
주체가 되는 곳에 외래키 설정을 해줘야
나중에 보기도 쉬운거같은데
 
방식2를 추천하는 이유가 있을까요?
 

답변 7

1

이진석님의 프로필 이미지
이진석
지식공유자

안녕하세요. 쓰신 코드처럼 create 를 직접 구현하셔서 처리를 해보실 수 있으실텐데, validated_data에는 "file"이 없죠. 왜냐하면 "file" 이름의 시리얼라이저 필드가 없기 때문입니다. 디버거를 활용해서 validated_data 값을 확인해보시면 좋구요.

그러면 파일 데이터는 어떻게 참조할 수 있느냐면. request 객체를 통해서 Raw 파일 데이터를 참조하실 수 있습니다.

아래와 같이 코드를 써볼 수 있습니다. 모델은 다르지만 주석으로 설명도 써뒀으니 참고해보세요. 이미지만 새창으로 열어서 보시면 코드를 보시는 것이 보다 수월하실 겁니다.

화이팅입니다. :-)

1

이진석님의 프로필 이미지
이진석
지식공유자

안녕하세요.

1:N 관계를 외래키로 지정할 수 있습니다. 이 관계는 1측이 아니라 N측에 세팅을 하여, 어떤 1에 속하는 지를 저장하여야 합니다. 그것이 외래키 관계인 것이죠.

그러니 장고에서는 외래키 관계는 1번 방법은 맞지 않으며, 2번 방법을 써야만 합니다.

화이팅입니다.

0

김동혁님의 프로필 이미지
김동혁
질문자

덕분에 원하는 기능을 구현했어요

한개의 게시물이 있고 

게시물에는 원하는대로 첨부파일을 넣을수  있는거죠

그리고  원하는대로 삭제하거나 중간만 업데이트 하거나 등등..

근데 이게 업데이트 할때랑 create할때랑 좀 로직이 바뀌어야되는거같아요

 DRF 강력하긴 한데, 너무 강력해서 이게 커스터마이징하는데 투자시간이 너무 많이 드네요 ㅠ

from rest_framework import serializers
from rest_framework.serializers import raise_errors_on_nested_writes
from rest_framework.utils import model_meta

from .models import NoticeBoard, NoticeBoardFile, NoticeBoardComment


class NoticeBoardFileSerializers(serializers.ModelSerializer):
file = serializers.FileField(use_url=True)

class Meta:
model = NoticeBoardFile
fields = '__all__'

def to_internal_value(self, data):
data['name'] = data['file'].name

return super().to_internal_value(data)

def to_representation(self, instance):
representation = super().to_representation(instance)
representation.update(
{
"size": instance.file.size,
}
)
return representation


class NoticeBoardListSerializers(serializers.ModelSerializer):
author = serializers.CharField(source='author.fullname')

class Meta:
model = NoticeBoard
fields = ['id', 'title', 'is_public', 'updated_at', 'author', 'views']


class NoticeBoardRetrieveSerializers(serializers.ModelSerializer):
author = serializers.CharField(source='author.fullname')
noticeboardfile_set = NoticeBoardFileSerializers(many=True)

class Meta:
model = NoticeBoard
fields = '__all__'

def validate(self, attrs):
attrs = super().validate(attrs)
request = self.context['request']
uploaded_files = []
if request.data.getlist('file'):
for uploaded_file in request.data.getlist('file'):
print(type(uploaded_file))
# print(uploaded_file.name)
# serializer = NoticeBoardFileSerializers(data={'file': uploaded_file})
# serializer.is_valid(raise_exception=True)
uploaded_files.append(uploaded_file)
attrs['file'] = uploaded_files
print(attrs)
return attrs
else:
return attrs

def update(self, instance, validated_data):
raise_errors_on_nested_writes('update', self, validated_data)
info = model_meta.get_field_info(instance)

uploaded_files = []
file_object_list = []
str_list = []
already_uploaded_file_list = []
already_uploaded_file_list_temp = NoticeBoardFile.objects.filter(post=instance).values_list('id', flat=True)
for temp in already_uploaded_file_list_temp:
already_uploaded_file_list.append(temp)

print('start', already_uploaded_file_list)
if 'file' in validated_data:
uploaded_files = validated_data.pop('file')

if len(uploaded_files) > 0:
for uploaded_file in uploaded_files:
print(uploaded_file, type(uploaded_file))
if type(uploaded_file) == str:
str_list.append(uploaded_file)
continue
else:
file_object_list.append(
NoticeBoardFile(post=instance, file=uploaded_file, name=uploaded_file.name))
NoticeBoardFile.objects.bulk_create(file_object_list)
else:
NoticeBoardFile.objects.filter(post=instance).delete()

print(str_list, 'str_list')
print('already_uploaded_file_list', already_uploaded_file_list)
for already_uploaded_file in already_uploaded_file_list:
print(already_uploaded_file)
if str(already_uploaded_file) in str_list:
continue
if not str(already_uploaded_file) in str_list:
print(already_uploaded_file, '는 없다.')
NoticeBoardFile.objects.get(id=already_uploaded_file).delete()

# Simply set each attribute on the instance, and then save it.
# Note that unlike `.create()` we don't need to treat many-to-many
# relationships as being a special case. During updates we already
# have an instance pk for the relationships to be associated with.
m2m_fields = []
for attr, value in validated_data.items():
if attr in info.relations and info.relations[attr].to_many:
m2m_fields.append((attr, value))
else:
setattr(instance, attr, value)

instance.save()

# Note that many-to-many fields are set after updating instance.
# Setting m2m fields triggers signals which could potentially change
# updated instance and we do not want it to collide with .update()
for attr, value in m2m_fields:
field = getattr(instance, attr)
field.set(value)

return instance


class NoticeBoardCreateSerializers(serializers.ModelSerializer):
class Meta:
model = NoticeBoard
fields = ['title', 'content', 'author']

def validate(self, attrs):
attrs = super().validate(attrs)
request = self.context['request']
uploaded_files = []
if request.data.getlist('file'):
for uploaded_file in request.data.getlist('file'):
print(type(uploaded_file))
print(uploaded_file.name)
# serializer = NoticeBoardFileSerializers(data={'file': uploaded_file})
# serializer.is_valid(raise_exception=True)
uploaded_files.append(uploaded_file)
attrs['file'] = uploaded_files
return attrs
else:
return attrs

def create(self, validated_data):
uploaded_files = []
file_object_list = []

if 'file' in validated_data:
uploaded_files = validated_data.pop('file')

post = super().create(validated_data)

if len(uploaded_files) > 0:
for uploaded_file in uploaded_files:
file_object_list.append(NoticeBoardFile(post=post, file=uploaded_file, name=uploaded_file.name))
NoticeBoardFile.objects.bulk_create(file_object_list)

return post

def to_representation(self, instance):
representation = super().to_representation(instance)
representation.update(
{
"id": instance.id,
}
)
return representation

이진석님의 프로필 이미지
이진석
지식공유자

DRF의 철학대로 API를 설계하시고 개발하시면 보다 수월하게 개발할 수 있을 것이구요. 그 철학에서 벗어나면 나름의 고충이 있을 수는 있습니다. // DRF의 많은 기능들을 과하게 수정하고 있다라고 느끼신다면, 설계에서 뭔가 개선할 점이 있지는 않을까 고민해보시는 것은 어떨까요?

하지만 DRF없이 구현을 해보시면 DRF를 활용하시는 것이 지금에도, 장기적으로도 DRF의 도움으로 훨씬 수월하게 개발할 수 있음을 느끼실 수 있으실 것입니다.

그리고, NoticeBoard를 create 시에 NoticeBoardFile들을 같이 생성하시더라도
각각의 NoticeBoardFile에 대한 수정 및 삭제는
NoticeBoard API를 통하지 않고,
NoticeBoardFile API를 통해 수정/삭제하시는 방법이 훨씬 간결하실 수 있습니다.
NoticeBoardFile은 별도의 리소스로 보는 거죠. 비슷한 기능이더라도 관점에 따라 API가 달라지고 일이 훨씬 간결해질 수 있습니다.

화이팅입니다. :-)

김동혁님의 프로필 이미지
김동혁
질문자

생각해보니 게시글을 올릴 때 두번의 호출을 통해 수정할 수도 있었네요 ㅠ  게시글과 내용은 NoticeBoardAPI 호출

첨부파일은 NoticeBoardFileAPI 호출... ㅠ 

 

아니면 NoticeBoardAPI 호출 시 NoticeBoardFileAPI를 호출하는건가요??

 

강사님 의견이 궁금해요 ㅠ 

이진석님의 프로필 이미지
이진석
지식공유자

정답은 없고 구현하기 나름입니다. :-)

유명 회사들의 api들을 분석해보시는 것도 도움이 되겠죠.

다양한 접근이 있음을 인지하시고, 모두 구현해보시면 실력향상에 도움이 되실 것입니다.

화이팅입니다.

김동혁님의 프로필 이미지
김동혁
질문자

감사합니다. (__) 

0

김동혁님의 프로필 이미지
김동혁
질문자

답변 감사합니다.

원하는대로 구현했는데 문제가 있습니다.

model.py

class NoticeBoard(models.Model):
title = models.CharField(verbose_name='제목', max_length=512)
content = models.TextField(verbose_name='내용')
views = models.PositiveIntegerField(default=0, verbose_name='조회수')
author = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.PROTECT)
is_public = models.BooleanField(verbose_name='공개 여부', default=True)
created_at = models.DateTimeField(verbose_name='작성일자', auto_now_add=True)
updated_at = models.DateTimeField(verbose_name='변경일자', auto_now=True)

def __str__(self):
return f'{self.title}'


class NoticeBoardFile(models.Model):
post = models.ForeignKey(NoticeBoard, on_delete=models.CASCADE)
filename = models.CharField(verbose_name='파일명', max_length=128, blank=True)
file = models.FileField(upload_to='files/%Y/%m/%d', blank=True)

def delete(self, *args, **kwargs):
if self.file:
os.remove(os.path.join(settings.MEDIA_ROOT, self.file.path))
super(NoticeBoardFile, self).delete(*args, **kwargs)

serializers.py

class NoticeBoardFileSerializers(serializers.ModelSerializer):
file = serializers.FileField(use_url=True)

class Meta:
model = NoticeBoardFile
fields = '__all__'

def to_internal_value(self, data):
data['filename'] = data['file'].name

return super().to_internal_value(data)

def to_representation(self, instance):
representation = super().to_representation(instance)
representation.update(
{
"size": instance.file.size,
}
)
return representation
 
class NoticeBoardCreateSerializers(serializers.ModelSerializer):
class Meta:
model = NoticeBoard
fields = ['title', 'content', 'author']

def validate(self, attrs):
attrs = super().validate(attrs)
request = self.context['request']
uploaded_files = []
if request.data.getlist('file'):
for uploaded_file in request.data.getlist('file'):
print(type(uploaded_file))
print(uploaded_file.name)
# serializer = NoticeBoardFileSerializers(data={'file': uploaded_file})
# serializer.is_valid(raise_exception=True)
uploaded_files.append(uploaded_file)
attrs['file'] = uploaded_files
return attrs
else:
return attrs

def create(self, validated_data):
uploaded_files = []
file_object_list = []

if 'file' in validated_data:
uploaded_files = validated_data.pop('file')

post = super().create(validated_data)

if len(uploaded_files) > 0:
for uploaded_file in uploaded_files:
file_object_list.append(NoticeBoardFile(post=post, file=uploaded_file))
NoticeBoardFile.objects.bulk_create(file_object_list)

return post

마지막 시리얼라이즈에서 

 # serializer = NoticeBoardFileSerializers(data={'file': uploaded_file})
# serializer.is_valid(raise_exception=True)

이 부분을 주석처리 해제하면 에러가 뜨는데 도저히 해결책을 모르겠어요

원하는 기능은 구현했는데 이럴 때 뭐가 문제인지 궁금합니다.

장고 시리얼라이즈 너무 어려워요 ㅠㅠㅠㅠㅠㅠㅠㅠ

 

아울러 프론트엔드에서 보낼 때는 

fileList를 보내면 str값으로 보내져서

그냥 

formData.append('title', title_value)
formData.append('content', value)
formData.append('author', '0000')
if(files.length > 0){
files.forEach(file => {
formData.append('file', file)
})
}

이런 식으로 보내야되는거 같아요 

 

이진석님의 프로필 이미지
이진석
지식공유자

장고 Form과 Serializer의 유효성 검사 메커니즘은 기본적으로 동일합니다. 조바심 가지지마시고, 차근차근 정리해보시며, 유효성 검사에 대해서 차근 차근 이해해보세요. Form과 시리얼라이저 강의 부분을 여러 번 반복해서 학습해보시길 권해드립니다. 그리고 실습의 코드와 얼추 비슷하게 새로운 앱과 모델을 만들어서 연습해보시고, 조금씩 변경해보시며 이해도를 올려가세요.

장고가 제공해주는 기능이 많지만, 그냥 얻어지는 것은 없습니다.

에러가 발생하신다면 어떤 에러가 발생하는 지 알려주셔야 합니다. 제게 질문을 남기시는 과정도 학습의 과정입니다. 질문을 하기위해 정리하시면서 오히려 답이 찾아가시기도 합니다.

500 에러가 아니라 유효성 검사 에러가 뜬다는 말씀이실까요?

구현하신 NoticeBoardFileSerializers 에 보시면 Meta의 fields 속성이 "__all__"로 되어있습니다. 제가 예시 코드로 보여드린 시리얼라이저와 다를 것입니다. "__all__" 로 지정하시면 지정 모델의 모든 필드에 대해서 유효성 검사를 수행할 텐데, NoticeBoardFileSerializers(data={'file': uploaded_file}) 코드에서는 file 필드만 지정하니 당연히 다음에 serializer.is_valid(raise_exception=True) 에서는 다른 필드값이 주어지지 않았으니 항상 유효성 검사에 실패하게 될 것입니다.

위 유효성 검사의 의도는 file 필드에 대해서만 유효성 검사를 수행할 의도였습니다.

차근차근 화이팅입니다. :-)

0

김동혁님의 프로필 이미지
김동혁
질문자

def validate(self, attrs):
attrs = super().validate(attrs)

질문 1 

이 validate는 상속받아서 구현하는건 알겠는데

attrs = super().validate(attrs)

이부분은 왜 하는걸까요? ㅠ 진짜 파이썬에 대해서 하나도 모른다는걸 실감하네요 ㅠ

 

질문 2

self.context['request']

이 부분인데 의미를 더 알고 싶습니다. self.context 이부분이요

 

질문 3

그럼 로직이 일단 시리얼라이즈가 뷰에서 호출되면 

validate 함수가 발동이 되면서 이걸 호출해서 검증을 해주면 되고 

이 attrs 결과가 validated_data로 들어가는거죠?

 

질문4

drf에 

serializers.ImageField가 있는데 모델에서 이미지필드가 있다면 따로 적용안해도 되는부분인가요.??

이진석님의 프로필 이미지
이진석
지식공유자

1) 클래스 상속에서 부모 클래스의 validate 메서드를 호출하여 리턴값을 받습니다. 부모의 validate구현을 무시한다면 호출하지 않아도 되지만, 부모의 validate구현에 덧붙여 자식의 validate 구현을 추가할 목적으로 작성한 코드이기에 super를 호출하였습니다. 파이썬의 상속 문법을 이 기회에 차근 차근 정리해보세요. 많이 사용하는 문법입니다.

2) DRF의 GenericAPIView에서는 serializer 인스턴스를 생성 시에 get_serializer 메서드를 호출하여 인스턴스를 생성합니다.

관련 코드 : https://github.com/encode/django-rest-framework/blob/master/rest_framework/generics.py#L109

 이때 get_serializer_class로 Serializer 클래스를 획득하고, get_serializer_context를 통해 serializer 인스턴스 생성 시에 context 이름의 키워드 인자로 지정할 dict를 획득합니다. 대강 Signature는 PostSerializer(context=get_serializer_context()) 와 같은 형태가 되는 거죠. context 지정은 GenericAPIView가 디폴트 동작으로 지정해주고 있습니다.

아래 코드를 보시면 get_serializer_context에서는 'request' 등의 키를 지정하고 있음을 확인하실 수 있습니다.

관련 코드 : https://github.com/encode/django-rest-framework/blob/master/rest_framework/generics.py#L130

Serializer의 생성자에 conext로 지정된 값은 Serializer 인스턴스 내부에서 self.context를 통해 참조할 수 있도록 되어있습니다. 그러니 Serializer 내부에서는 self.context['request'] 로 현재 요청 내역을 참조할 수 있게 되는 것입니다.

3) Serializer의 validate는 장고 Form의 clean과 메커니즘이 동일합니다. DRF Serializer가 장고 Form 설계를 그대로 따라서 개발했기 때문입니다. 장고 Form의 clean 에피도스에서 유효성 검사에 대해서 깊게 설명드리니 참고해보세요. serializer.is_valid() 호출 시에 모든 유효성 검사 로직을 수행합니다. 이때 serializer.validate 메서드도 그 중 하나인 것이죠. 

Form의 cleaned_data와 Serializer의 validated_data가 역할이 동일합니다. 유효성 검사가 끝난 값들을 참조할 수 있습니다.

화이팅입니다. :-)

0

김동혁님의 프로필 이미지
김동혁
질문자

models.py

class NoticeBoard(models.Model):
title = models.CharField(verbose_name='제목', max_length=512)
content = models.TextField(verbose_name='내용')
author = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.PROTECT)
is_public = models.BooleanField(verbose_name='공개 여부', default=True)
created_at = models.DateTimeField(verbose_name='작성일자', auto_now_add=True)
updated_at = models.DateTimeField(verbose_name='변경일자', auto_now=True)

def __str__(self):
return f'{self.title}'


class NoticeBoardFile(models.Model):
post = models.ForeignKey(NoticeBoard, on_delete=models.CASCADE, related_name='noticeboardfile')
file = models.FileField(upload_to='files/%Y/%m/%d')

serializers.py

class NoticeBoardFileSerializers(serializers.HyperlinkedModelSerializer):
file = serializers.FileField(use_url=True)

class Meta:
model = NoticeBoardFile
fields = ['post', 'file']


class NoticeBoardSerializers(serializers.ModelSerializer):
noticeboardfile = NoticeBoardFileSerializers(many=True)

class Meta:
model = NoticeBoard
fields = ['title', 'content', 'author', 'is_public', 'created_at', 'updated_at', 'noticeboardfile']

def create(self, validated_data):
files_data = validated_data.pop('file')
noticeboard = NoticeBoardFile.objects.create(**validated_data)
for file_data in files_data:
NoticeBoardFile.objects.create(post_id=noticeboard, **file_data)
return noticeboard

현재 이렇게 직렬화를 해서 성공은 했는데(모델은 본문의 2와 동일합니다.)

생각해보니 게시판에 게시글을 이렇게 올릴 때 

게시글이 만들어지고나서, 게시글에 첨부파일들을 넣어줘야 되는데

drf로 구현할려니 어렵네요.

 

프론트엔드에서

사용자가 게시글 1개와 게시글1에 대해 첨부파일 5개를 넣어서 작성버튼을 누르면

 

백엔드에 요청보낼 때는 NoticeBoard모델에 대해 요청을 보내서

백엔드에서 NoticeBoard모델에 대한 객체를 만들고 응답값으로 NoticeBoard의 주소를 보내주면

다시 프론트엔드에서 NoticeBoard주소 + 파일 데이터(파일 5개)에 대해서 요청을 다시 보내줘야되는가요?

 

동시에 보내면 파일데이터는 참조할 수 있는 모델이 없기 때문에 처리가 안될거같은데요

 

제가 생각하는 방식이 맞는지 아니면 동시에 보낼 수 있는지 궁금하네요 보통 어떻게 처리하는지도 궁금하구요 ㅠ

0

김동혁님의 프로필 이미지
김동혁
질문자

감사합니다. (__)

이진석님의 프로필 이미지
이진석
지식공유자

2번 방법으로 지정하시면, NoticeBoard 인스턴스에서는 files_set 속성을 가지게 됩니다.

그럼 notice_board.files_set.all() 코드로 notice_board에 속한 Files를 조회할 수 있습니다.

"files_set" 이름은 FK 지정 시에 related_name 키워드 인자를 통해 다른 이름으로 변경하실 수도 있구요.

화이팅입니다. :-)

김동혁님의 프로필 이미지
김동혁

작성한 질문수

질문하기