create_aiTool을 통해 aiTool을 생성요청할 때, categories에 [id1, id2, id3] 같은 모습의 value를 넣어서 저장하고 있었다.
하지만 실제 저장될 땐, ManyToMany 테이블의 관계로 저장되어 배열에 넣어준 순서가 유지된 채 저장되지 않았다.
아래는 기존 aiTool과 aiToolCategory 사이 N-M관계를 매핑하는 중간테이블이다. 보이다시피, 따로 카테고리 입력 순서가 정의돼있지 않다.
[N-M 매핑 테이블]
클라이언트의 메인페이지 코어섹션에서 대표 카테고리 하나씩을 식별하여 카테고리 색상, 뱃지를 스타일로 적용해주도록 설계했기 때문에, 대표 카테고리를 식별하기 위해 배열의 맨 앞에 들어갈 id와 카테고리 순서를 신경써서 넣어줘야 하며 이를 유지하여 저장해야할 필요가 있다.
아래는 한계를 맞이한 기존의 장고 core 서브앱의 핵심 코드임.
#models.py
from django.db import models
# from accounts.models import User
class AiToolCategory(models.Model):
id = models.AutoField(primary_key=True)
name_set = models.JSONField(default=dict) # name_set: {ko: string[], en: string[] }
class Meta:
managed = True #db에 테이블을 추가 및 삭제한다.
db_table = 'core_aitool_category'
class AiTool(models.Model):
id = models.AutoField(primary_key=True)
imgUrl = models.CharField(max_length=255)
name_set = models.JSONField(default=dict) # name_set: {ko: string[], en: string[]}
summary = models.TextField(default='Unknown')
redirectUrl = models.CharField(max_length=255, default='https://cmd8.vercel.app/')
categories = models.ManyToManyField(AiToolCategory, related_name='ai_tools') #N-M관계, 역참조이름 설정
class Meta:
managed = True #db에 테이블을 추가 및 삭제한다.
db_table = 'core_aitool'
# serializers.py
from rest_framework import serializers
from .models import AiTool, AiToolCategory
### Abstracts
class AbstractBinNameSerializer(serializers.Serializer):
ko = serializers.ListField(child=serializers.CharField())
en = serializers.ListField(child=serializers.CharField())
class Meta:
abstract = True
### Serializers
#
class ToolNameSerializer(AbstractBinNameSerializer):
pass
class CategoryNameSerializer(AbstractBinNameSerializer):
pass
class AiToolCategorySerializer(serializers.ModelSerializer):
name_set = CategoryNameSerializer()
class Meta:
model = AiToolCategory
fields = ('id', 'name_set')
class AiToolSerializer(serializers.ModelSerializer):
name_set = ToolNameSerializer()
categories = serializers.PrimaryKeyRelatedField(queryset=AiToolCategory.objects.all(), many=True)
class Meta:
model = AiTool
fields = ('id', 'imgUrl', 'name_set', 'summary', 'redirectUrl', 'categories')
# views.py
@api_view(['POST'])
@csrf_exempt
def create_aiTool(request):
try:
data = json.loads(request.POST['data'])
image = request.FILES['image']
image_extension = os.path.splitext(image.name)[1]
image_name = data['name_set']['en'][0] + image_extension
# 이미지를 S3에 업로드하고 이미지 URL 가져오기
default_storage.save(f'ai_tools/{image_name}', image)
image_url = default_storage.url(f'ai_tools/{image_name}')
# 이미지 URL을 요청 데이터에 추가하기
data['imgUrl'] = image_url
serializer = AiToolSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=status.HTTP_201_CREATED)
return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@csrf_exempt
def get_aiTool(request, pk):
try:
ai_tool = AiTool.objects.prefetch_related('categories').get(pk=pk)
serializer = AiToolSerializer(ai_tool)
return JsonResponse(serializer.data, status=status.HTTP_200_OK)
except AiTool.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
#### safe=True(=default)였기에 딕셔너리가 아닌 직렬화 오류가 발생했었다.
#### err: In order to allow non-dict objects to be serialized set the safe parameter to False.
#### 해결법2 적용 해결했음 ( django 권장 해결법), 1번 대안은 safe=False 설정이었음
@api_view(['GET'])
@csrf_exempt
def get_all_aiTools(request):
try:
ai_tools = AiTool.objects.prefetch_related('categories').all() #prefetch사용해서 성능향상 도모: 어차피 메인페이지 항상 함께씀
if not ai_tools:
raise AiTool.DoesNotExist
serializer = AiToolSerializer(ai_tools, many=True)#직렬화
serialized_data = serializer.data
return JsonResponse({'aiTools': serialized_data}, status=status.HTTP_200_OK)
except AiTool.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@csrf_exempt
def get_all_aiTools_combinedCats(request):
try:
ai_tools = AiTool.objects.prefetch_related('categories').all()
if not ai_tools:
raise AiTool.DoesNotExist
serializer = AiToolSerializer(ai_tools, many=True)
serialized_data = serializer.data
# AiToolCategory를 한번에 가져와 ID를 기준으로 사전에 저장 : 성능개선용 (DB쿼리 수 감소)
all_categories = AiToolCategory.objects.all()
category_dict = {category.id: category for category in all_categories}
# 프론트-메인코어 인터페이스 맞게 가공
transformed_data = []
for ai_tool in serialized_data:
category_ids = ai_tool['categories']
categories_ko = []
categories_en = []
for category_id in category_ids:
category = category_dict[category_id]
categories_ko.append(category.name_set['ko'][0])
categories_en.append(category.name_set['en'][0])
transformed_data.append({
'id': ai_tool['id'],
'imgUrl': ai_tool['imgUrl'],
'ko': {
'name': ai_tool['name_set']['ko'],
'category': categories_ko,
},
'en': {
'name': ai_tool['name_set']['en'],
'category': categories_en,
},
'summary': ai_tool['summary'],
'redirectUrl': ai_tool['redirectUrl'],
'derived': {
'score': {
'avg': 4.5, # 여기에 실제 평균 점수를 적용
'cnt': 10, # 여기에 실제 평가 수를 적용
},
'favoriteCnt': 1000, # 여기에 실제 즐겨찾기 수를 적용
},
})
return JsonResponse({'aiTools': transformed_data}, status=status.HTTP_200_OK)
except AiTool.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
####################
##### CATEGORY #####
@api_view(['POST'])
@csrf_exempt
def create_aiTool_category(request):
try:
data = request.data
serializer = AiToolCategorySerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=status.HTTP_201_CREATED)
return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@csrf_exempt
def get_aiTool_category(request, pk):
try:
ai_tool_category = AiToolCategory.objects.get(pk=pk)
serializer = AiToolCategorySerializer(ai_tool_category)
return JsonResponse(serializer.data, status=status.HTTP_200_OK)
except AiToolCategory.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@csrf_exempt
def get_all_aiTool_categories(request):
try:
ai_tool_categories = AiToolCategory.objects.all()
if not ai_tool_categories:
raise AiToolCategory.DoesNotExist
serializer = AiToolCategorySerializer(ai_tool_categories, many=True)
serialized_data = serializer.data
return JsonResponse({'aiToolCategories': serialized_data}, status=status.HTTP_200_OK)
except AiToolCategory.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
기존에 단순히 ManyToManyField를 사용해 단순한 다대다 매핑을 통해 자동생성했던 매핑테이블을 아래처럼 order를 함께 저장하는 테이중간매핑 테이블로 정의해 매핑해주었다.
그리고 아래는 그 중간테이블의 모습이며
실제로 관련 테이블을 활용한 get Query시, 입력 시 categories id 배열의 순서를 지켜서 응답을 받을 수 있음을 확인했다.