• 티스토리 홈
  • 프로필사진
    조별하
  • 방명록
  • 공지사항
  • 태그
  • 블로그 관리
  • 글 작성
조별하
  • 프로필사진
    조별하
    • 분류 전체보기 (124)
      • 1. 공부 (0)
        • 기술 면접 (0)
      • 2. 웹개발 (7)
        • Java (1)
        • IT 정보 (5)
        • Error 모음 (1)
      • 3. 개인관심 (1)
        • 전자 (1)
      • GITHUB (5)
      • IT 기술 면접 (3)
      • COMPUTER (2)
      • TASK (1)
      • LANGUAGES (20)
        • Python (3)
        • Django (6)
        • Java (7)
        • Node (1)
        • Jsp (2)
        • R (1)
      • 데이터과학 (2)
        • 머신러닝 및 딥러닝 (2)
      • DATABASE (5)
        • Oracle (2)
      • ISSUE (10)
        • Django (4)
        • Drf (2)
        • Javascript (1)
        • Git (1)
      • JAVA 교육 (65)
        • Java (3)
        • Jquery (2)
        • Sql (16)
        • Jdbc (1)
        • Db (2)
        • Jsp (9)
        • myWeb (15)
        • Servlet (4)
        • Spring (12)
        • Crawling (0)
        • Hosting (1)
      • 정보처리산업기사 (1)
      • EDUCATION (2)
        • IoT 서비스 (2)
  • 방문자 수
    • 전체:
    • 오늘:
    • 어제:
  • 최근 댓글
      등록된 댓글이 없습니다.
    • 최근 공지
        등록된 공지가 없습니다.
      # Home
      # 공지사항
      #
      # 태그
      # 검색결과
      # 방명록
      • [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일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)
      목차
      표시할 목차가 없습니다.
        • 안녕하세요
        • 감사해요
        • 잘있어요

        티스토리툴바