- [ISSUE] ManytoMany 관계를 가진 두 모델 Bulk Create 작업2023년 05월 06일
- 조별하
- 작성자
- 2023.05.06.:01
[22.11.26]
🍃 ManytoMany 관계를 가진 두 모델 Bulk Create 작업
✔️ 현재 위와 같이 등록된 항목에 대해 태그를 추가하는 기능을 개발 구현 중이다.
- 등록된 항목은 Scrap Parsing을 통해 특정 사이트에 관련한 url, 썸네일, 제목 등을 저장하여 접근할 수 있게 구현한 현황 화면이다.
- 특정 사이트를 선택하여 bulk(다중)로 태그를 등록할 수 있게 스크립트 구현
- 선택한 각 Site와 Tag Model이 ManytoMany 관계로 데이터 모델링
1. 모델 코드
📌 Tag Model
class Site(models.Model): """ 항목에 관한 데이터 모델 """ title = models.CharField(verbose_name='타이틀', max_length=100) user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='유저') url = models.CharField(verbose_name='url', max_length=2000, null=True) thumbnail_url = models.CharField(verbose_name='썸네일주소', max_length=2000) host_name = models.CharField(verbose_name='호스트명', max_length=500) content = models.TextField(verbose_name='컨텐츠') # category choices CATEGORY_CHOICES = [(1, 'python'), (2, 'django'), (3, 'javascript'), (4, 'orm'), (5, 'mysql'), (6, 'drf'), (7, 'docker'), (8, 'os'), (9, 'aws'), (10, 'html'), (11, 'css'), (12, 'git'), (13, 'linux')] category = models.IntegerField(verbose_name='카테고리', choices=CATEGORY_CHOICES) favorite = models.BooleanField(verbose_name='즐겨찾기', default=False) video = models.BooleanField(verbose_name='비디오', default=False) 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} ({self.get_category_display()})" class Meta: verbose_name = '항목' verbose_name_plural = '항목 목록'
📌 Tag Model
class Tag(models.Model): """ 웹 항목 태그 목록 모델 """ name = models.CharField(verbose_name='이름', max_length=20) site = models.ManyToManyField(Site, verbose_name='리스트') def __str__(self): return f"{self.name}" class Meta: verbose_name = '태그' verbose_name_plural = '태그 목록'
2. js에서 선택한 항목의 id값과 입력된 Tag값 전달
function setFechData(method, body){ /* Fetch data 셋팅 */ let csrftoken = getCookie('csrftoken'); const data = { method: method, headers: { 'content-type': 'application/json', 'X-CSRFToken' : csrftoken, }, body: JSON.stringify(body) } return data } function bulkTag() { /* 벌크 태그 이벤트 */ const data = setFechData("POST",{ pk_ids: selected_articles, tags: added_tags, user: "User Id" }) fetch(`/api/sites/tags`, data) .then(response => { let status = response.status }) .then(() => getSiteList()) .then(() => changeSelected()) .catch(error => console.log(error) }
✔️ ‘저장’ 버튼 Click
1. bulkTag() 실행 및 fetch에 담아 전송할 data 할당
2. data는 setFechData() 함수를 이용
- 다른 api요청 시에도 재사용할 수 있도록 공통함수 작성
- method는 새로 저장되는 api이기 때문에 POST로 요청
3. pk_ids에는 선택한 항목의 pk(id) 값을 배열 형식으로 할당 ex) [1, 2]
4. tags에는 선택한 항목에 등록할 태그 명을 배열 형식으로 할당 ex) [’python’, ‘django’]
5. fetch함수를 이용하여
/api/sites/tags
api에 data 담아 전송3. api url 설정
from pocket.views import ( # api_view SiteBulkAPIView **SiteTagsAPIView,** ) api_patterns = [ path('sites/bulk', SiteBulkAPIView.as_view()), **path('sites/tags', SiteTagsAPIView.as_view()),** ] urlpatterns = [ # api **path('api/', include(api_patterns)),** ]
- 화면 이동이 아닌 api 관련은 include 하여 api_patterns로 연결되게 설정
- js에서 전송한 request를 SiteTagsAPIView 뷰단으로 이동할 수 있게 맴핑
4. view 코드 작성
class SiteTagsAPIView(APIView): """ 벌크 항목 태그 api """ # 2_b def get_list(self): pk_ids: list = self.request.data.get('pk_ids') return get_list_or_404(Site, id__in=pk_ids) # 2_a def validate_ids(self): pk_ids: list = self.request.data.get('pk_ids') for id in pk_ids: get_object_or_404(Site,id=id) return self.get_list() # 1 def post(self, request): """ Site 태그 추가 """ # 2,3 sites = validate_ids() tags = self.request.data.get('tags') with transaction.atomic(): '''트랜젝션 시작''' # 4_a bulk_tags = [Tag(name=tag) for tag in tags] # 4_b created_tags = Tag.objects.bulk_create(bulk_tags) # 5 [tag.site.add(site.id) for tag in created_tags for site in sites] return Response({'msg':'Add Tag successfully'}, status=status.HTTP_200_OK)
1. js에서 요청한 method와 동일하게 함수명을 post로 지정
2. 전달받은 pk(id)가 담긴 배열을 이용하여 Site list를 조회
- validate_ids() 호출 후 존재하는 id값들인 경우 get_list() 호출
- get_list()를 호출하여
get_list_or_404(Site, id__in=pk_ids)
와 같이 pk_ids값을 포함한 Site들 조회 (단, 항목이 존재하지 않을 경우 404 에러 반환)
3. 전달받은 태그가 담긴 배열 변수 할당
4. 태그 생성을 위한 Tag 객체 생성
- list comprehesion을 이용하여 Tag가 담긴 배열 생성
- bulk_create() 함수를 이용하여 이전 단계에서 담아둔 Tag객체를 매개변수로하여 create_tags 할당
5. 생성된 Tags 리스트와 Sites 리스트 ManytoMany 매칭
- list comprehesion을 이용하여 Tag 모델의 site 칼럼에 전달받는 site들을 넣어주는 작업 진행
bulk_create()를 사용한 이유
✔️ 벌크 작업 시 일반 create로 작업을 하게 되면 하나하나 create를 할 때마다 DB에 connection을 요청하기 때문에 오래 걸리기도 하고 과부하가 걸린다는 단점이 있다.
✔️ 그래서 Django에서 제공하는 bulk_create()를 사용한 것이다. 이 함수는 DB에 connection을 한 번만 요청하여 생성을 한 번에 처리해 주는 함수이기 때문에 효율적인 면에서 좋다.5. Issue1
Internal Server Error: /api/sites/tags Traceback (most recent call last): File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/django/core/handlers/exception.py", line 47, in inner response = get_response(request) File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/django/core/handlers/base.py", line 181, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view return view_func(*args, **kwargs) File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/django/views/generic/base.py", line 70, in view return self.dispatch(request, *args, **kwargs) File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/rest_framework/views.py", line 509, in dispatch response = self.handle_exception(exc) File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/rest_framework/views.py", line 469, in handle_exception self.raise_uncaught_exception(exc) File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception raise exc File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/rest_framework/views.py", line 506, in dispatch response = handler(request, *args, **kwargs) File "/Users/cjy/Lecture/Devket/pocket/decorator.py", line 18, in exec_func return func(self, request, sites=sites) File "/Users/cjy/Lecture/Devket/pocket/views.py", line 189, in post [tag.site.add(site.id) for tag in created_tags for site in sites] File "/Users/cjy/Lecture/Devket/pocket/views.py", line 189, in <listcomp> [tag.site.add(site.id) for tag in created_tags for site in sites] File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/django/db/models/fields/related_descriptors.py", line 536, in __get__ return self.related_manager_cls(instance) File "/Users/cjy/Lecture/Devket/env-devket/lib/python3.10/site-packages/django/db/models/fields/related_descriptors.py", line 851, in __init__ raise ValueError('"%r" needs to have a value for field "%s" before ' **ValueError: "<Tag: 자바>" needs to have a value for field "id" before this many-to-many relationship can be used.**
✔️ 위와 동일하게 코드를 작성하여 이벤트를 실행시키면 제일 에러가 발생한다.
- Error 내용
- Tag라는 객체는 many-to-many 작업을 하기 위해 id 필드가 필요하다.라는 내용이 담겨 있다.
bulk_create() 사용으로 에러가 발생한 이유
✔️ 여기서 에러를 생각보다 오래 잡지 못했던 이유가 작업을 하면서 당연히 bulk_create() 함수를 사용하면
✔️ 리턴되는 객체가 Tag를 생성하였으니 그 생성한 id와 pk값을 가지고 있다고 판단하고 위와 같이 코드를 작성하니 id값이 존재하지 않다고 에러가 발생한 것이다.
✔️ 결론적으로 bulk_create() 함수는 여러 생성 작업만 가지고는 효율적이지만 재사용성 면에서는 좋지 않다는 것이다. 그 이유에 Tag를 Many-to-Many로 추가해야 하는 상황에서 후처리 작업을 하지 못한다.6. 해결 방법
class SiteTagsAPIView(APIView): """ 벌크 항목 태그 api """ def get_list(self): pk_ids: list = self.request.data.get('pk_ids') return get_list_or_404(Site, id__in=pk_ids) def validate_ids(self): pk_ids: list = self.request.data.get('pk_ids') for id in pk_ids: get_object_or_404(Site,id=id) return self.get_list() def post(self, request, **kwards): """ Site 태그 추가 """ sites = validate_ids() tags = self.request.data.get('tags') with transaction.atomic(): '''트랜젝션 시작''' created_tags = [Tag.objects.get_or_create(name=tag)[0] for tag in tags] [tag.site.add(site.id) for tag in created_tags for site in sites] return Response({'msg':'Updated successfully'}, status=status.HTTP_200_OK)
- Tag 객체가 담긴 리스트를 bulk_create()로 생성해 주는 로직에서 list comprehesion을 이용하여 Tag를 생성하며 반환되는 Tag객체를 배열에 할당해 준다.
- 할당해 준 Tags 리스트와 Sites를 반복문을 통해서 tag.site.add(site.id)하여 Many-to-Many관계를 매칭해 준다.
'ISSUE > Django' 카테고리의 다른 글
[ISSUE] Tag 포함된 Site 모델의 serialize 처리 (0) 2023.05.06 [ISSUE]decorator를 이용한 중복작업 전처리 (0) 2023.05.05 [ISSUE] signup AbstractBaseUser class (0) 2023.05.05 다음글이전글이전 글이 없습니다.댓글
스킨 업데이트 안내
현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)