์ƒˆ์†Œ์‹

ISSUE/Django

[ISSUE] ManytoMany ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง„ ๋‘ ๋ชจ๋ธ Bulk Create ์ž‘์—…

  • -

[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๊ด€๊ณ„๋ฅผ ๋งค์นญํ•ด ์ค€๋‹ค.

 

Contents

ํฌ์ŠคํŒ… ์ฃผ์†Œ๋ฅผ ๋ณต์‚ฌํ–ˆ์Šต๋‹ˆ๋‹ค

์ด ๊ธ€์ด ๋„์›€์ด ๋˜์—ˆ๋‹ค๋ฉด ๊ณต๊ฐ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.