[Django DRF] project (3)
Product Media
admin 에서 이미지 관리하기
Build : Implementing the Product Image Model
pip install pillow
models.py
alternative_text
: 대체 텍스트로, 이미지의 대체 텍스트를 나타내는 문자열 필드다.
또한, ProductImage
모델은 clean()
메서드를 오버라이드하여 데이터 유효성을 검사한다. clean()
메서드에서는 동일한 productline
에 속하고 order
필드가 중복된 값을 가지는 다른 ProductImage
객체가 있는지 확인합니다. 중복된 값이 있는 경우 ValidationError
을 발생시킨다.
save()
메서드도 오버라이드되어 있으며, 모델 저장 전에 full_clean()
메서드를 호출하여 데이터 유효성을 검사한다. 그 후 원래의 save()
메서드를 호출하여 모델을 저장한다.
Build : Admin Stite Reverse Link inline URLs
admin.py
from django.contrib import admin
from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import Brand, Category, Product, ProductImage, ProductLine
class EditLinkInline(object):
def edit(self, instance):
url = reverse(
f"admin:{instance._meta.app_label}_{instance._meta.model_name}_change",
args=[instance.pk],
)
if instance.pk:
link = mark_safe('<a href="{u}">edit</a>'.format(u=url))
return link
else:
return ""
class ProductImageInline(admin.TabularInline):
model = ProductImage
class ProductLineInline(EditLinkInline, admin.TabularInline):
model = ProductLine
readonly_fields = ("edit",)
class ProductAdmin(admin.ModelAdmin):
inlines = [
ProductLineInline,
]
class ProductLineAdmin(admin.ModelAdmin):
inlines = [
ProductImageInline,
]
admin.site.register(ProductLine, ProductLineAdmin)
admin.site.register(Product, ProductAdmin)
admin.site.register(Category)
admin.site.register(Brand)
-
EditLinkInline
클래스는 인라인(admin inline)에서 사용되며, 인스턴스의 편집 링크를 제공한다.edit
메서드는 인스턴스의 편집 링크를 생성하고 반환한다. -
ProductImageInline
클래스는Product
모델의 관리자 인라인으로,ProductImage
모델과 연결된다.TabularInline
을 상속받아 테이블 형태의 인라인으로 표시한다. -
ProductLineInline
클래스는ProductLine
모델의 관리자 인라인으로,ProductLine
모델과 연결된다.EditLinkInline
을 상속받아edit
메서드를 사용할 수 있으며,TabularInline
을 상속받아 테이블 형태의 인라인으로 표시된다.readonly_fields
속성을 설정하여edit
필드를 읽기 전용으로 표시한다. -
ProductAdmin
클래스는Product
모델에 대한 관리자 설정이다.inlines
속성에ProductLineInline
을 등록하여Product
모델의 관련ProductLine
객체를 인라인으로 표시한다. -
ProductLineAdmin
클래스는ProductLine
모델에 대한 관리자 설정이다.inlines
속성에ProductImageInline
을 등록하여ProductLine
모델의 관련ProductImage
객체를 인라인으로 표시한다. -
admin.site.register()
함수를 사용하여 모델을 관리자 사이트에 등록한다.ProductLine
,Product
,Category
,Brand
모델이 등록되어 있다.
위와 같이 어드민 페이지가 생성되고 product를 등록하면 아래와 같이 edit 페이지가 생긴다.
edit 페이지를 연결하면 아래와 같이 productline 으로 연결된다.
Build : Product Image Serializer
serializers.py
Performance : Multiple Queries, Towards Eliminating the N+1 Query Problem
장고의 prefetch_related()
는 데이터베이스에서 관련된 객체를 사전에 가져오는 기능을 제공을 한다. 이를 통해 데이터베이스 쿼리 수를 최적화하고 성능을 향상시킬 수 있다.
예시를 통해 prefetch_related()
의 동작 방식을 살펴보자. 현재 당신이 블로그 애플리케이션을 개발 중이며, 다음과 같은 두 개의 모델이 있다고 가정해보자:
class Blog(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
class Comment(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name='comments')
author = models.CharField(max_length=50)
text = models.TextField()
Blog
모델과 Comment
모델은 일대다 관계로 연결되어 있다. 한 블로그에는 여러 개의 댓글이 있을 수 있다. 이제 블로그 목록을 가져오는 뷰를 만든다고 가정해보자:
def blog_list(request):
blogs = Blog.objects.all()
return render(request, 'blog/list.html', {'blogs': blogs})
여기서 blogs
쿼리셋은 각 블로그의 댓글을 가져오기 위해 추가적인 쿼리를 실행해야 한다. 이는 블로그 수가 많아질수록 성능에 부정적인 영향을 미칠 수 있다.
이제 prefetch_related()
를 사용하여 성능을 최적화해 보자:
def blog_list(request):
blogs = Blog.objects.prefetch_related('comments').all()
return render(request, 'blog/list.html', {'blogs': blogs})
prefetch_related('comments')
메서드를 호출하여 Blog
모델에 대한 쿼리셋을 가져올 때 각 블로그의 댓글도 함께 가져온다. 이렇게 함으로써 추가적인 쿼리를 실행할 필요가 없어지므로 성능이 향상된다.
prefetch_related()
는 단일 관계뿐만 아니라 다대다 관계에도 사용할 수 있다. 예를 들어, Blog
모델이 Tag
모델과 다대다 관계를 가진다면 prefetch_related('tags')
와 같이 호출하여 태그도 함께 사전에 가져올 수 있다.
이렇게 prefetch_related()
를 사용하여 관련된 객체를 미리 가져오면 데이터베이스 쿼리의 수를 줄이고 애플리케이션의 성능을 개선할 수 있다.
views.py
현재 있는 상태에서 쿼리문을 실행 시키면 아래와 같다.
SELECT "django_session"."session_key",
"django_session"."session_data",
"django_session"."expire_date"
FROM "django_session"
WHERE ("django_session"."expire_date" > '2023-07-08 12:15:05.996839'
AND "django_session"."session_key" = '2f39ep2nr7dqww6dqh0ujti5kyhzc5zs')
LIMIT 21
SELECT "auth_user"."id",
"auth_user"."password",
"auth_user"."last_login",
"auth_user"."is_superuser",
"auth_user"."username",
"auth_user"."first_name",
"auth_user"."last_name",
"auth_user"."email",
"auth_user"."is_staff",
"auth_user"."is_active",
"auth_user"."date_joined"
FROM "auth_user"
WHERE "auth_user"."id" = 1
LIMIT 21
SELECT "product_product"."id",
"product_product"."name",
"product_product"."slug",
"product_product"."description",
"product_product"."is_digital",
"product_product"."brand_id",
"product_product"."category_id",
"product_product"."is_active",
"product_brand"."id",
"product_brand"."name",
"product_brand"."is_active",
"product_category"."id",
"product_category"."name",
"product_category"."slug",
"product_category"."is_active",
"product_category"."parent_id",
"product_category"."lft",
"product_category"."rght",
"product_category"."tree_id",
"product_category"."level"
FROM "product_product"
WHERE "product_productimage"."productline_id" = 1
SELECT "product_productimage"."id",
"product_productimage"."url",
"product_productimage"."alternative_text",
"product_productimage"."productline_id",
"product_productimage"."order"
FROM "product_productimage"
WHERE "product_productimage"."productline_id" = 2
SELECT "product_productimage"."id",
"product_productimage"."url",
"product_productimage"."alternative_text",
"product_productimage"."productline_id",
"product_productimage"."order"
FROM "product_productimage"
WHERE "product_productimage"."productline_id" = 3
7개의 쿼리문이 실행된다.
views.py
prefetch 를 이용할 경우 아래와 같이 성능 향상을 볼 수 있다.
5
SELECT "django_session"."session_key",
"django_session"."session_data",
"django_session"."expire_date"
FROM "django_session"
WHERE ("django_session"."expire_date" > '2023-07-08 12:16:20.137571'
AND "django_session"."session_key" = '2f39ep2nr7dqww6dqh0ujti5kyhzc5zs')
LIMIT 21
SELECT "auth_user"."id",
"auth_user"."password",
"auth_user"."last_login",
"auth_user"."is_superuser",
"auth_user"."username",
"auth_user"."first_name",
"auth_user"."last_name",
"auth_user"."email",
"auth_user"."is_staff",
"auth_user"."is_active",
"auth_user"."date_joined"
FROM "auth_user"
WHERE "auth_user"."id" = 1
LIMIT 21
SELECT "product_product"."id",
"product_product"."name",
"product_product"."slug",
"product_product"."description",
"product_product"."is_digital",
"product_product"."brand_id",
"product_product"."category_id",
"product_product"."is_active",
"product_brand"."id",
"product_brand"."name",
"product_brand"."is_active",
"product_category"."id",
"product_category"."name",
"product_category"."slug",
"product_category"."is_active",
"product_category"."parent_id",
"product_category"."lft",
"product_category"."rght",
"product_category"."tree_id",
"product_category"."level"
FROM "product_product"
INNER JOIN "product_brand" ON ("product_product"."brand_id" = "product_brand"."id")
LEFT OUTER JOIN "product_category" ON ("product_product"."category_id" = "product_category"."id")
WHERE ("product_product"."is_active"
AND "product_product"."slug" = 'p1')
SELECT "product_productline"."id",
"product_productline"."price",
"product_productline"."sku",
"product_productline"."stock_qty",
"product_productline"."product_id",
"product_productline"."is_active",
"product_productline"."order"
FROM "product_productline"
WHERE "product_productline"."product_id" IN (1)
SELECT "product_productimage"."id",
"product_productimage"."url",
"product_productimage"."alternative_text",
"product_productimage"."productline_id",
"product_productimage"."order"
FROM "product_productimage"
WHERE "product_productimage"."productline_id" IN (1,
2,
3)
쿼리문은 5개만 실행되고 아래가 변화 되었다.
슬러그를 이용해 제품을 필터링 하고 있지만 카테고리와 브랜드 정보를 반환 하기 위해서는 두 개의 추가 쿼리가 필요하다.
이러한 문제를 해결하기 위해 select_related 를 사용하는데 select_related 는 내부적으로 SQL 코드를 조인을 포함하는 형태로 변경한다.
조인 절은 두 개 이상의 테이블에서 행을 결합하는 데 사용된다. 즉 카테고리와 브랜드 테이블을 함께 가져와 쿼리를 실행함으로서 쿼리의 수를 3개에서 1개로 줄일 수 있다.
현재 product image 테이블을 추가했으므로 이제 product를 반환할 때 카테고리와 브랜드 정보뿐만 아니라 제품 라인과 제품 이미지 데이터도 반환해야한다.
제품에 대해 제품 라인과 제품 이미지를 더 많이 추가하면 N+1 이라고 불리는 문제가 발생한다. 각 제품에 대해 제품 라인과 제품 이미지를 연결하려면 추가적인 쿼리가 필요하다.
이 경우 select_related 는 왜래키에서만 작동한다. 그러나 제품 라인과 제품 이미지를 둘 다 반환해야하므로 이는 둘 다 역방향 왜래키다.
이런 경우에 prefetch_related 를 사용하면 제품 라인과 제품 이미지를 가져오기 위해 쿼리를 실행할 필요 없이 관련된 객체를 한 번에 가져올 수 있다.
이는 역방향 왜래키를 사용하는 데이터 베이스 디자인에 사용할 수 있따.
즉 제품 테이블에서 카테고리와 브랜드에 대한 왜래키가 있는데, 여기서 왜래키가 있는 쪽을 찾을 수 있다. 그러나 product line과 product image를 참조할 때는 왜래키가 반대 테이블에 있는 것으로 참조한다. 이를 역외래키(reverse foreign key)라고 한다.
product line 에서 product image 으로 가는 왜래 키가 없다. 왜래키는 product image 테이블에 있다. 따라서 product line 에서 다시 참조하려면 여기서 왜래키는 역왜래키 관계다.
위 쿼리에서 prefetch_related 를 사용하기 전을 보면 각 product line 에 대해 개별적인 쿼리를 실행하는 것을 확인 할 수 있는데 이렇게 되면 여러개의 쿼리를 실행하기 떄문에 문제가 될 수 있다.
prefetch_related 를 사용한 후 첫 번째 쿼리는 brand 와 category 의 조인을 나태내고 두 번째 쿼리는 제품 라인과 관련 되었고 세 번째는 모든 이미지랑 관련 되어 있다.
각 제품 ID 마다 쿼리를 실행하는 대신에 이제 모두 함께 처리하는 것을 확인할 수 있다.
Testing : Product Image Factory
factories.py
Testing : Product Image Models
test_models.py
conftest.py
댓글남기기