Django 22 - 이미지 다루기 1(폼셋, 트렌젝션)
13 Jun 2019 | Django이미지 다루기 1 (인스타그램)
Django17 미디어 파일 관리 및 업로드 구현 포스팅에서 이미지(미디어)파일을 어떻게 업로드하여 관리하고, 재가공하는지에 대해 정리해보았다. 추가로, 하나의 게시글(Post)에 여러 사진을 올리고, 또한 그사진들을 횡으로 보여지도록, 마치 인스타그램 처럼 기능하게끔 내용을 확장키켜보도록 하자.
하나의 Post에 여러개의 사진을 올리기
Models.py
하나의 Post에 여러 사진을 올리기 위해서는 Post와 이미지를 1: N 관계를 형성해야함. 데이터베이스 관계를 재 설정해야하므로 models.py를 다시 수정하도록 하자.
Image
라는 새로운 클래스 생성 후 ,Post
클래스 내 image 필드의 내용을 그대로 갖고와Image
클래스 내 file 필드값으로 옮김.Post
클래스와image
클래스의 관계를 1:N로 설정post = models.ForeignKey(Post, on_delete=models.CASCADE)
- 이미지 저장 경로 변경(
{instance.content}
=>{instance.post.content}
)- 본래 image가
Post
클래스의 하위 필드인 경우,instance.content
로 접근 가능했으나, 이미지는 Image 클래스로 이동되었으므로,instance.post.content
로 한번 더 접근해야함.
- 본래 image가
def path_image_path(instance, filename):
#{instance.content} => {instance.post.content}
return f'posts/{instance.post.content}/{filename}'
class Image(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
file = processedImageField(
upload_to = path_image_path,
processors = [ResizeToFill(600,600)],
format = 'JPEG',
option = {'quality':90},
)
Forms.py (1)
PostForm
내image
필드 삭제. 이에 따라 게시글 생성 페이지에서는,PostForm
양식에는 이미지 업로드 기능이 사라졌으므로,Image
클래스를 갖고와ImageForm
을 만들어서, html에 넘겨줘야함.Image
클래스 import
#Image 클래스 추가
from .models import Post, Comment, Image
class PostForm(forms.ModelForm):
class Meta:
model = Post
# image 필드 삭제
fields = ['content',]
class ImageForm(forms.ModelForm):
class Meta:
model = Image
fields = ['file',]
Forms.py (2)
위의 코드대로 ImageForm
을 html 문서에 넘길 경우에는 하나의 이미지만 올릴 수 있음. 여러개의 이미지를 올리기위해서는 formset
을 사용해야함.
inlineformset_factory
: 이미지 폼을 갖고와 엮어서 폼셋으로 만들어주는 역할을 함.- 1번째 인자: 만들 데이터의 부모 모델(1:N에서 1을 의미). 이미지를 들고 있는 모델(
Post
) - 2번째 인자: 우리가 만들 모델(
Image
) - 3번째 인자(키워드 인자, form= 폼 명): 기본으로 들고오는 폼이미지 폼
- 4번째 인자(키워드 인자, extra= 갯수): 이미지의 갯수
- 1번째 인자: 만들 데이터의 부모 모델(1:N에서 1을 의미). 이미지를 들고 있는 모델(
- 공식문서 참조
class ImageForm(forms.ModelForm):
class Meta:
model = Image
fields = ['file',]
#추가
ImageFormSet = forms.inlineformset_factory(Post, Image, form=ImageForm, extra=3)
Views.py (1)
- 게시글을 생성할 때, 이미지를 업로드해야하므로, create 함수 코드 수정
ImageFormSet
import- GET 방식인 경우,
ImageFormSet
을 갖고와 변수(image_formset
)에 저장한 후 , 템플릿 변수로 넘김
from .forms import PostForm, CommentForm, ImageFormSet
@login_required
def create(request):
if request.method == 'POST':
post_form = PostForm(request.POST, request.FILES)
if post_form.is_valid():
post = post_form.save(commit=False)
post.user = request.user
post.save()
return redirect('posts:list')
else:
post_form = PostForm()
image_formset = ImageFormSet()
return render(request, 'posts/create.html', {
'post_form': post_form,
'image_formset':image_formset,
})
Views.py (2)
- POST 방식인 경우에도 마찬가지로
ImageFormSet
에 대한 코드를 삽입해야함. post_form
의 두번째 인자에 들어간request.FILES
은 삭제한 후,ImageFormSet
을 불러와 인자에 각각request.POST
와request.FILES
을 입력.image_formset
또한 유효성 검증을 통과해야하므로 조건문에 추가.Post
와Image
는 1:N 관계로, 게시글이 존재해야 이미지를 업로드 할 수 있으므로 순서가 매우 중요하다. 따라서post.save()
로 게시글을 먼저 생성한 후 id 값이 부여 되면post
가 외래키로 사용가능하게되고Image
모델에 따라 이미지를 저장 시킬 수 있게 된다.- 또한,
image_formset
은 모델(클래스) => 모델폼 으로 래핑이 되어있는데, 이는 일종의 외부에 껍데기가 씌인 개념이다. 따라서image_formset.instance
로 접근하여 껍데기를 벗긴 후, 부모 모델의 인스턴스를 저장시켜줘야 한다. (만약 래핑이 되어 있지 않다면image_formset.post = post
로 접근이 가능할 것이다.)
@login_required
def create(request):
if request.method == 'POST':
post_form = PostForm(request.POST)
image_formset = ImageFormSet(request.POST, request.FILES)
if post_form.is_valid() and image_formset.is_valid():
post = post_form.save(commit=False)
post.user = request.user
post.save()
image_formset.instance = post
image_formset.save()
return redirect('posts:list')
Views.py (3) - transaction
위의 코드대로 실행을 시킬 경우, 작동이 될 수도 있으나, 이는 완벽한 코드가 아니며 많은 에러가 발생 할 수 있다.
.save()
는 데이터를 실제 DB에 저장시킬 때 사용하는 코드로, 이 코드를 통해 Django는 DB에 데이터를 저장해달라고 요청을 보내게 된다. 데이터는 db.SQLITE3 라는 파일에 저장되는데, 이 때 DB인 db.SQLITE3는 최상단 디렉토리에 위치하고 있다 (Django 밖에 위치)
Django ORM이 DB에 요청을 보내 놓고, DB가 역으로 응답을 다시 보내지 않는 이상, Django 는 응답을 기다리지 않고 바로 밑의 코드를 실행시키게 된다. (이를 비동기로 동작한다고 한다)
post.save()
코드의 경우, DB가 따로 응답을 보내지 않기 때문에 DB에 실제로 저장이 완료되었는지를 알 수 없다. 예를 들어, 병목 현상으로 인해 DB에 저장되는 시간이 많이 소요될 수 도 있을 것이다. 따라서, post
가 DB에 저장되지 않았는데, image_formset.instance = post
의 코드가 실행되는 상황이 발생 할 수 있다.
DB의 입장에서는 순서가 보장되지 않기 때문에(ex, 이미지 저장 후, post 저장) Django에서는 위와 같은 상황이 발생하는 것을 막아줘야할 필요가 있음. 이러한 역할을 하는 Django 의 transaction이며 아래와 같이 코드를 작성할 수 있다.
with transaction.atomic()
from django.db import transaction
사용을 위해 관련 코드 import
transaction은 간단히 말해 작업단위(쪼개질 수 없는 업무처리 단위) 라 할 수 있는데, 여러개의 프로세스가 묶여져 마치 하나처럼 동작하는 방식이라 할 수 있음. 데이터 베이스 충돌을 해결하기 위해서, 둘또는 그이상의 데이터베이스 업데이트를 하나의 작업으로 처리하는 기법을 의미한다.
그렇기 때문에 성공 아니면 실패 두가지 결과밖에 존재하지 않음. - Transaction의 원자성(Atomicity)
관련 하기 reference를 추가로 참조해보자!
from django.db import transaction
@login_required
def create(request):
if request.method == 'POST':
post_form = PostForm(request.POST)
image_formset = ImageFormSet(request.POST, request.FILES)
if post_form.is_valid() and image_formset.is_valid():
post = post_form.save(commit=False)
post.user = request.user
# from django.db import transaction
with transaction.atomic():
# 첫번째: 실제 DB에 저장
post.save()
# 두번째
image_formset.instance = post
image_formset.save() #실제 DB에 저장
return redirect('posts:list')
views.py 최종 version
- 최종적으로 정리된 코드는 다음과 같음.
from django.db import transaction
@login_required
def create(request):
if request.method == 'POST':
post_form = PostForm(request.POST)
image_formset = ImageFormSet(request.POST, request.FILES)
if post_form.is_valid() and image_formset.is_valid():
post = post_form.save(commit=False)
post.user = request.user
with transaction.atomic():
post.save()
image_formset.instance = post
image_formset.save()
return redirect('posts:list')
else:
post_form = PostForm()
image_formset = ImageFormSet()
return render(request, 'posts/create.html', {
'post_form': post_form,
'image_formset':image_formset,
})
create.html
- 템플릿 변수로 넘긴 모델 폼셋(
image_formset
)을 진자 템플릿 문법을 통하여 입력
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block container %}
<h1>New Post</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form post_form %}
<!--추가된 부분 -->
{% image_formset.as_p %}
<!--추가된 부분 -->
<input type="submit" value="Submit"/>
</form>
{% endblock %}
_post.html
- 업로드한 이미지 표시. 1:N 관계 중, 1(
post
)에서 N(image
) 으로 접근할 때는 항상_set.all
을 사용하였음.- 예)
post.image_set
- 예)
post.image_set
에는 여러장의 이미지가 저장되어있으므로 반복문을 돌려서 표시.img
태그의src
속성에는 반드시 url까지 입력하도록 하자.
<div class="card" style="width: 18rem;">
<div class="card-header">
<span> <a href="{% url 'accounts:people' post.user.username %}">{{ post.user.username }} </a></span>
</div>
{% for image in post.image_set.all %}
<img src="{{ image.file.url }}" class="card-img-top" alt="{{ image.file.image }}">
{% endfor %}
Comments