Django Serializer Nested Creation: How to avoid N+1 queries on relations












4















There are dozens of posts about n+1 queries in nested relations in Django, but I can't seem to find the answer to my question. Here's the context:



The Models



class Book(models.Model):
title = models.CharField(max_length=255)

class Tag(models.Model):
book = models.ForeignKey('app.Book', on_delete=models.CASCADE, related_name='tags')
category = models.ForeignKey('app.TagCategory', on_delete=models.PROTECT)
page = models.PositiveIntegerField()

class TagCategory(models.Model):
title = models.CharField(max_length=255)
key = models.CharField(max_length=255)


A book has many tags, each tag belongs to a tag category.



The Serializers



class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
exclude = ['id', 'book']

class BookSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, required=False)

class Meta:
model = Book
fields = ['title', 'tags']

def create(self, validated_data):
with transaction.atomic():
tags = validated_data.pop('tags')
book = Book.objects.create(**validated_data)
Tag.objects.bulk_create([Tag(book=book, **tag) for tag in tags])
return book


The Problem



I am trying to POST to the BookViewSet with the following example data:



{ 
"title": "The Jungle Book"
"tags": [
{ "page": 1, "category": 36 }, // plot intro
{ "page": 2, "category": 37 }, // character intro
{ "page": 4, "category": 37 }, // character intro
// ... up to 1000 tags
]
}


This all works, however, during the post, the serializer proceeds to make a call for each tag to check if the category_id is a valid one:



enter image description here



With up to 1000 nested tags in a call, I can't afford this.

How do I "prefetch" for the validation?

If this is impossible, how do I turn off the validation that checks if a foreign_key id is in the database?



EDIT: Additional Info



Here is the view:



class BookViewSet(views.APIView):

queryset = Book.objects.all().select_related('tags', 'tags__category')
permission_classes = [IsAdminUser]

def post(self, request, format=None):
serializer = BookSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)









share|improve this question

























  • Use the .prefetch_related() function to solve

    – Flavio Milan
    Nov 24 '18 at 23:24
















4















There are dozens of posts about n+1 queries in nested relations in Django, but I can't seem to find the answer to my question. Here's the context:



The Models



class Book(models.Model):
title = models.CharField(max_length=255)

class Tag(models.Model):
book = models.ForeignKey('app.Book', on_delete=models.CASCADE, related_name='tags')
category = models.ForeignKey('app.TagCategory', on_delete=models.PROTECT)
page = models.PositiveIntegerField()

class TagCategory(models.Model):
title = models.CharField(max_length=255)
key = models.CharField(max_length=255)


A book has many tags, each tag belongs to a tag category.



The Serializers



class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
exclude = ['id', 'book']

class BookSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, required=False)

class Meta:
model = Book
fields = ['title', 'tags']

def create(self, validated_data):
with transaction.atomic():
tags = validated_data.pop('tags')
book = Book.objects.create(**validated_data)
Tag.objects.bulk_create([Tag(book=book, **tag) for tag in tags])
return book


The Problem



I am trying to POST to the BookViewSet with the following example data:



{ 
"title": "The Jungle Book"
"tags": [
{ "page": 1, "category": 36 }, // plot intro
{ "page": 2, "category": 37 }, // character intro
{ "page": 4, "category": 37 }, // character intro
// ... up to 1000 tags
]
}


This all works, however, during the post, the serializer proceeds to make a call for each tag to check if the category_id is a valid one:



enter image description here



With up to 1000 nested tags in a call, I can't afford this.

How do I "prefetch" for the validation?

If this is impossible, how do I turn off the validation that checks if a foreign_key id is in the database?



EDIT: Additional Info



Here is the view:



class BookViewSet(views.APIView):

queryset = Book.objects.all().select_related('tags', 'tags__category')
permission_classes = [IsAdminUser]

def post(self, request, format=None):
serializer = BookSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)









share|improve this question

























  • Use the .prefetch_related() function to solve

    – Flavio Milan
    Nov 24 '18 at 23:24














4












4








4








There are dozens of posts about n+1 queries in nested relations in Django, but I can't seem to find the answer to my question. Here's the context:



The Models



class Book(models.Model):
title = models.CharField(max_length=255)

class Tag(models.Model):
book = models.ForeignKey('app.Book', on_delete=models.CASCADE, related_name='tags')
category = models.ForeignKey('app.TagCategory', on_delete=models.PROTECT)
page = models.PositiveIntegerField()

class TagCategory(models.Model):
title = models.CharField(max_length=255)
key = models.CharField(max_length=255)


A book has many tags, each tag belongs to a tag category.



The Serializers



class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
exclude = ['id', 'book']

class BookSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, required=False)

class Meta:
model = Book
fields = ['title', 'tags']

def create(self, validated_data):
with transaction.atomic():
tags = validated_data.pop('tags')
book = Book.objects.create(**validated_data)
Tag.objects.bulk_create([Tag(book=book, **tag) for tag in tags])
return book


The Problem



I am trying to POST to the BookViewSet with the following example data:



{ 
"title": "The Jungle Book"
"tags": [
{ "page": 1, "category": 36 }, // plot intro
{ "page": 2, "category": 37 }, // character intro
{ "page": 4, "category": 37 }, // character intro
// ... up to 1000 tags
]
}


This all works, however, during the post, the serializer proceeds to make a call for each tag to check if the category_id is a valid one:



enter image description here



With up to 1000 nested tags in a call, I can't afford this.

How do I "prefetch" for the validation?

If this is impossible, how do I turn off the validation that checks if a foreign_key id is in the database?



EDIT: Additional Info



Here is the view:



class BookViewSet(views.APIView):

queryset = Book.objects.all().select_related('tags', 'tags__category')
permission_classes = [IsAdminUser]

def post(self, request, format=None):
serializer = BookSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)









share|improve this question
















There are dozens of posts about n+1 queries in nested relations in Django, but I can't seem to find the answer to my question. Here's the context:



The Models



class Book(models.Model):
title = models.CharField(max_length=255)

class Tag(models.Model):
book = models.ForeignKey('app.Book', on_delete=models.CASCADE, related_name='tags')
category = models.ForeignKey('app.TagCategory', on_delete=models.PROTECT)
page = models.PositiveIntegerField()

class TagCategory(models.Model):
title = models.CharField(max_length=255)
key = models.CharField(max_length=255)


A book has many tags, each tag belongs to a tag category.



The Serializers



class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
exclude = ['id', 'book']

class BookSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, required=False)

class Meta:
model = Book
fields = ['title', 'tags']

def create(self, validated_data):
with transaction.atomic():
tags = validated_data.pop('tags')
book = Book.objects.create(**validated_data)
Tag.objects.bulk_create([Tag(book=book, **tag) for tag in tags])
return book


The Problem



I am trying to POST to the BookViewSet with the following example data:



{ 
"title": "The Jungle Book"
"tags": [
{ "page": 1, "category": 36 }, // plot intro
{ "page": 2, "category": 37 }, // character intro
{ "page": 4, "category": 37 }, // character intro
// ... up to 1000 tags
]
}


This all works, however, during the post, the serializer proceeds to make a call for each tag to check if the category_id is a valid one:



enter image description here



With up to 1000 nested tags in a call, I can't afford this.

How do I "prefetch" for the validation?

If this is impossible, how do I turn off the validation that checks if a foreign_key id is in the database?



EDIT: Additional Info



Here is the view:



class BookViewSet(views.APIView):

queryset = Book.objects.all().select_related('tags', 'tags__category')
permission_classes = [IsAdminUser]

def post(self, request, format=None):
serializer = BookSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)






python django django-rest-framework django-serializer






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Nov 26 '18 at 17:01







jbodily

















asked Nov 24 '18 at 23:16









jbodilyjbodily

5251513




5251513













  • Use the .prefetch_related() function to solve

    – Flavio Milan
    Nov 24 '18 at 23:24



















  • Use the .prefetch_related() function to solve

    – Flavio Milan
    Nov 24 '18 at 23:24

















Use the .prefetch_related() function to solve

– Flavio Milan
Nov 24 '18 at 23:24





Use the .prefetch_related() function to solve

– Flavio Milan
Nov 24 '18 at 23:24












5 Answers
5






active

oldest

votes


















1














The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:




  1. Serialize and check the validity of input data.

  2. Serialize output data.


Therefore the correct place to optimize your query is the corresponding view.

We will use the select_related method that:




Returns a QuerySet that will “follow” foreign-key relationships, selecting additional related-object data when it executes its query. This is a performance booster which results in a single more complex query but means later use of foreign-key relationships won’t require database queries.
to avoid the N+1 database queries.




You will need to modify the part of your view code that creates the corresponding queryset, in order to include a select_related call.

You will also need to add a related_name to the Tag.category field definition.



Example:



# In your Tag model:
category = models.ForeignKey(
'app.TagCategory', on_delete=models.PROTECT, related_name='categories'
)

# In your queryset defining part of your View:
class BookViewSet(views.APIView):

queryset = Book.objects.all().select_related(
'tags', 'tags__categories'
) # We are using the related_name of the ForeignKey relationships.




If you want to test something different that uses also the serializer to cut down the number of queries, you can check this article.






share|improve this answer


























  • I have added the view to the question. I did as you recommended and added the prefetch_related to the view to get tags and tags__category (it's one-to-many), but on create it is still querying once per tag for the tag_category.

    – jbodily
    Nov 25 '18 at 2:13








  • 1





    This should be done via a select_related, not prefetch. (Because it's 1 to Many and not Many to Many).

    – yrekkehs
    Nov 25 '18 at 3:33








  • 1





    @yrekkehs You are absolutely right and that is the reason why one shouldn't try answering 2 in the morning :P. Thanks for the catch!

    – John Moutafis
    Nov 25 '18 at 12:42






  • 1





    @jbodily As yrekkehs caught my error, the correct method you should use is the select_related. I edited my answer and you should try it as it is now. Sorry for the inconvenience!

    – John Moutafis
    Nov 25 '18 at 12:43











  • I appreciate the due diligence, but unfortunately it doesn't seem to help on creation. Let me clarify the relations again: A book has many tags, a tag has one book. A tag has one tag_category, a tag_category has many tags. So two one-to-many relations. There is no tags__categories. But secondly, I've updated my question to reflect your solution but still find that on create, the db is queried once per category.

    – jbodily
    Nov 26 '18 at 17:00



















0














I think the issue here is that the Tag constructor is automatically converting the category id that you pass in as category into a TagCategory instance by looking it up from the database. The way to avoid that is by doing something like the following if you know that all of the category ids are valid:




def create(self, validated_data):
with transaction.atomic():
tags = validated_data.pop('tags')
book = Book.objects.create(**validated_data)
tag_instances = [ Tag(book_id=book.id, page=x['page'], category_id=x['category']) for x in tags ]
Tag.objects.bulk_create(tag_instances)
return book





share|improve this answer
























  • This feels like it should be right, but check out the update I added: In the view I call serializer.is_valid(), then serializer.save(). The problem is on the is_valid check, if the category_id is set and not the category, it says category: This field may not be null.

    – jbodily
    Nov 25 '18 at 2:16





















0














I've come up with an answer that gets things working (but that I'm not thrilled about): Modify the Tag Serializer like this:



class TagSerializer(serializers.ModelSerializer):

category_id = serializers.IntegerField()

class Meta:
model = Tag
exclude = ['id', 'book', 'category']


This allows me to read/write a category_id without having the overhead of validations. Adding category to exclude does mean that the serializer will ignore category if it's set on the instance.






share|improve this answer































    0














    Problem is that you don't set created tags to the book instance so serializer try to get this while returning.



    You need to set it to the book as a list:



    def create(self, validated_data):
    with transaction.atomic():
    book = Book.objects.create(**validated_data)

    # Add None as a default and check that tags are provided
    # If you don't do that, serializer will raise error if request don't have 'tags'

    tags = validated_data.pop('tags', None)
    tags_to_create =

    if tags:
    tags_to_create = [Tag(book=book, **tag) for tag in tags]
    Tag.objects.bulk_create(tags_to_create)

    # Here I set tags to the book instance
    setattr(book, 'tags', tags_to_create)

    return book


    Provide Meta.fields tuple for TagSerializer (it's weird that this serializer don't raise error saying that fields tuple is required)



    class TagSerializer(serializers.ModelSerializer):
    class Meta:
    model = Tag
    fields = ('category', 'page',)


    Prefetching tag.category should be NOT necessary in this case because it's just id.



    You will need prefetching Book.tags for GET method. The simplest solution is to create static method for serializer and use it in viewset get_queryset method like this:



    class BookSerializer(serializers.ModelSerializer):
    ...
    @staticmethod
    def setup_eager_loading(queryset): # It can be named any name you like
    queryset = queryset.prefetch_related('tags')

    return queryset

    class BookViewSet(views.APIView):
    ...
    def get_queryset(self):
    self.queryset = BookSerializer.setup_eager_loading(self.queryset)
    # Every GET request will prefetch 'tags' for every book by default

    return super(BookViewSet, self).get_queryset()





    share|improve this answer































      0














      select_related function will check ForeignKey in the first time.
      Actually,this is a ForeignKey check in the relational database and you can use SET FOREIGN_KEY_CHECKS=0; in database to close inspection.






      share|improve this answer

























        Your Answer






        StackExchange.ifUsing("editor", function () {
        StackExchange.using("externalEditor", function () {
        StackExchange.using("snippets", function () {
        StackExchange.snippets.init();
        });
        });
        }, "code-snippets");

        StackExchange.ready(function() {
        var channelOptions = {
        tags: "".split(" "),
        id: "1"
        };
        initTagRenderer("".split(" "), "".split(" "), channelOptions);

        StackExchange.using("externalEditor", function() {
        // Have to fire editor after snippets, if snippets enabled
        if (StackExchange.settings.snippets.snippetsEnabled) {
        StackExchange.using("snippets", function() {
        createEditor();
        });
        }
        else {
        createEditor();
        }
        });

        function createEditor() {
        StackExchange.prepareEditor({
        heartbeatType: 'answer',
        autoActivateHeartbeat: false,
        convertImagesToLinks: true,
        noModals: true,
        showLowRepImageUploadWarning: true,
        reputationToPostImages: 10,
        bindNavPrevention: true,
        postfix: "",
        imageUploader: {
        brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
        contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
        allowUrls: true
        },
        onDemand: true,
        discardSelector: ".discard-answer"
        ,immediatelyShowMarkdownHelp:true
        });


        }
        });














        draft saved

        draft discarded


















        StackExchange.ready(
        function () {
        StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53463217%2fdjango-serializer-nested-creation-how-to-avoid-n1-queries-on-relations%23new-answer', 'question_page');
        }
        );

        Post as a guest















        Required, but never shown

























        5 Answers
        5






        active

        oldest

        votes








        5 Answers
        5






        active

        oldest

        votes









        active

        oldest

        votes






        active

        oldest

        votes









        1














        The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:




        1. Serialize and check the validity of input data.

        2. Serialize output data.


        Therefore the correct place to optimize your query is the corresponding view.

        We will use the select_related method that:




        Returns a QuerySet that will “follow” foreign-key relationships, selecting additional related-object data when it executes its query. This is a performance booster which results in a single more complex query but means later use of foreign-key relationships won’t require database queries.
        to avoid the N+1 database queries.




        You will need to modify the part of your view code that creates the corresponding queryset, in order to include a select_related call.

        You will also need to add a related_name to the Tag.category field definition.



        Example:



        # In your Tag model:
        category = models.ForeignKey(
        'app.TagCategory', on_delete=models.PROTECT, related_name='categories'
        )

        # In your queryset defining part of your View:
        class BookViewSet(views.APIView):

        queryset = Book.objects.all().select_related(
        'tags', 'tags__categories'
        ) # We are using the related_name of the ForeignKey relationships.




        If you want to test something different that uses also the serializer to cut down the number of queries, you can check this article.






        share|improve this answer


























        • I have added the view to the question. I did as you recommended and added the prefetch_related to the view to get tags and tags__category (it's one-to-many), but on create it is still querying once per tag for the tag_category.

          – jbodily
          Nov 25 '18 at 2:13








        • 1





          This should be done via a select_related, not prefetch. (Because it's 1 to Many and not Many to Many).

          – yrekkehs
          Nov 25 '18 at 3:33








        • 1





          @yrekkehs You are absolutely right and that is the reason why one shouldn't try answering 2 in the morning :P. Thanks for the catch!

          – John Moutafis
          Nov 25 '18 at 12:42






        • 1





          @jbodily As yrekkehs caught my error, the correct method you should use is the select_related. I edited my answer and you should try it as it is now. Sorry for the inconvenience!

          – John Moutafis
          Nov 25 '18 at 12:43











        • I appreciate the due diligence, but unfortunately it doesn't seem to help on creation. Let me clarify the relations again: A book has many tags, a tag has one book. A tag has one tag_category, a tag_category has many tags. So two one-to-many relations. There is no tags__categories. But secondly, I've updated my question to reflect your solution but still find that on create, the db is queried once per category.

          – jbodily
          Nov 26 '18 at 17:00
















        1














        The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:




        1. Serialize and check the validity of input data.

        2. Serialize output data.


        Therefore the correct place to optimize your query is the corresponding view.

        We will use the select_related method that:




        Returns a QuerySet that will “follow” foreign-key relationships, selecting additional related-object data when it executes its query. This is a performance booster which results in a single more complex query but means later use of foreign-key relationships won’t require database queries.
        to avoid the N+1 database queries.




        You will need to modify the part of your view code that creates the corresponding queryset, in order to include a select_related call.

        You will also need to add a related_name to the Tag.category field definition.



        Example:



        # In your Tag model:
        category = models.ForeignKey(
        'app.TagCategory', on_delete=models.PROTECT, related_name='categories'
        )

        # In your queryset defining part of your View:
        class BookViewSet(views.APIView):

        queryset = Book.objects.all().select_related(
        'tags', 'tags__categories'
        ) # We are using the related_name of the ForeignKey relationships.




        If you want to test something different that uses also the serializer to cut down the number of queries, you can check this article.






        share|improve this answer


























        • I have added the view to the question. I did as you recommended and added the prefetch_related to the view to get tags and tags__category (it's one-to-many), but on create it is still querying once per tag for the tag_category.

          – jbodily
          Nov 25 '18 at 2:13








        • 1





          This should be done via a select_related, not prefetch. (Because it's 1 to Many and not Many to Many).

          – yrekkehs
          Nov 25 '18 at 3:33








        • 1





          @yrekkehs You are absolutely right and that is the reason why one shouldn't try answering 2 in the morning :P. Thanks for the catch!

          – John Moutafis
          Nov 25 '18 at 12:42






        • 1





          @jbodily As yrekkehs caught my error, the correct method you should use is the select_related. I edited my answer and you should try it as it is now. Sorry for the inconvenience!

          – John Moutafis
          Nov 25 '18 at 12:43











        • I appreciate the due diligence, but unfortunately it doesn't seem to help on creation. Let me clarify the relations again: A book has many tags, a tag has one book. A tag has one tag_category, a tag_category has many tags. So two one-to-many relations. There is no tags__categories. But secondly, I've updated my question to reflect your solution but still find that on create, the db is queried once per category.

          – jbodily
          Nov 26 '18 at 17:00














        1












        1








        1







        The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:




        1. Serialize and check the validity of input data.

        2. Serialize output data.


        Therefore the correct place to optimize your query is the corresponding view.

        We will use the select_related method that:




        Returns a QuerySet that will “follow” foreign-key relationships, selecting additional related-object data when it executes its query. This is a performance booster which results in a single more complex query but means later use of foreign-key relationships won’t require database queries.
        to avoid the N+1 database queries.




        You will need to modify the part of your view code that creates the corresponding queryset, in order to include a select_related call.

        You will also need to add a related_name to the Tag.category field definition.



        Example:



        # In your Tag model:
        category = models.ForeignKey(
        'app.TagCategory', on_delete=models.PROTECT, related_name='categories'
        )

        # In your queryset defining part of your View:
        class BookViewSet(views.APIView):

        queryset = Book.objects.all().select_related(
        'tags', 'tags__categories'
        ) # We are using the related_name of the ForeignKey relationships.




        If you want to test something different that uses also the serializer to cut down the number of queries, you can check this article.






        share|improve this answer















        The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:




        1. Serialize and check the validity of input data.

        2. Serialize output data.


        Therefore the correct place to optimize your query is the corresponding view.

        We will use the select_related method that:




        Returns a QuerySet that will “follow” foreign-key relationships, selecting additional related-object data when it executes its query. This is a performance booster which results in a single more complex query but means later use of foreign-key relationships won’t require database queries.
        to avoid the N+1 database queries.




        You will need to modify the part of your view code that creates the corresponding queryset, in order to include a select_related call.

        You will also need to add a related_name to the Tag.category field definition.



        Example:



        # In your Tag model:
        category = models.ForeignKey(
        'app.TagCategory', on_delete=models.PROTECT, related_name='categories'
        )

        # In your queryset defining part of your View:
        class BookViewSet(views.APIView):

        queryset = Book.objects.all().select_related(
        'tags', 'tags__categories'
        ) # We are using the related_name of the ForeignKey relationships.




        If you want to test something different that uses also the serializer to cut down the number of queries, you can check this article.







        share|improve this answer














        share|improve this answer



        share|improve this answer








        edited Nov 25 '18 at 12:41

























        answered Nov 25 '18 at 0:30









        John MoutafisJohn Moutafis

        12.2k43259




        12.2k43259













        • I have added the view to the question. I did as you recommended and added the prefetch_related to the view to get tags and tags__category (it's one-to-many), but on create it is still querying once per tag for the tag_category.

          – jbodily
          Nov 25 '18 at 2:13








        • 1





          This should be done via a select_related, not prefetch. (Because it's 1 to Many and not Many to Many).

          – yrekkehs
          Nov 25 '18 at 3:33








        • 1





          @yrekkehs You are absolutely right and that is the reason why one shouldn't try answering 2 in the morning :P. Thanks for the catch!

          – John Moutafis
          Nov 25 '18 at 12:42






        • 1





          @jbodily As yrekkehs caught my error, the correct method you should use is the select_related. I edited my answer and you should try it as it is now. Sorry for the inconvenience!

          – John Moutafis
          Nov 25 '18 at 12:43











        • I appreciate the due diligence, but unfortunately it doesn't seem to help on creation. Let me clarify the relations again: A book has many tags, a tag has one book. A tag has one tag_category, a tag_category has many tags. So two one-to-many relations. There is no tags__categories. But secondly, I've updated my question to reflect your solution but still find that on create, the db is queried once per category.

          – jbodily
          Nov 26 '18 at 17:00



















        • I have added the view to the question. I did as you recommended and added the prefetch_related to the view to get tags and tags__category (it's one-to-many), but on create it is still querying once per tag for the tag_category.

          – jbodily
          Nov 25 '18 at 2:13








        • 1





          This should be done via a select_related, not prefetch. (Because it's 1 to Many and not Many to Many).

          – yrekkehs
          Nov 25 '18 at 3:33








        • 1





          @yrekkehs You are absolutely right and that is the reason why one shouldn't try answering 2 in the morning :P. Thanks for the catch!

          – John Moutafis
          Nov 25 '18 at 12:42






        • 1





          @jbodily As yrekkehs caught my error, the correct method you should use is the select_related. I edited my answer and you should try it as it is now. Sorry for the inconvenience!

          – John Moutafis
          Nov 25 '18 at 12:43











        • I appreciate the due diligence, but unfortunately it doesn't seem to help on creation. Let me clarify the relations again: A book has many tags, a tag has one book. A tag has one tag_category, a tag_category has many tags. So two one-to-many relations. There is no tags__categories. But secondly, I've updated my question to reflect your solution but still find that on create, the db is queried once per category.

          – jbodily
          Nov 26 '18 at 17:00

















        I have added the view to the question. I did as you recommended and added the prefetch_related to the view to get tags and tags__category (it's one-to-many), but on create it is still querying once per tag for the tag_category.

        – jbodily
        Nov 25 '18 at 2:13







        I have added the view to the question. I did as you recommended and added the prefetch_related to the view to get tags and tags__category (it's one-to-many), but on create it is still querying once per tag for the tag_category.

        – jbodily
        Nov 25 '18 at 2:13






        1




        1





        This should be done via a select_related, not prefetch. (Because it's 1 to Many and not Many to Many).

        – yrekkehs
        Nov 25 '18 at 3:33







        This should be done via a select_related, not prefetch. (Because it's 1 to Many and not Many to Many).

        – yrekkehs
        Nov 25 '18 at 3:33






        1




        1





        @yrekkehs You are absolutely right and that is the reason why one shouldn't try answering 2 in the morning :P. Thanks for the catch!

        – John Moutafis
        Nov 25 '18 at 12:42





        @yrekkehs You are absolutely right and that is the reason why one shouldn't try answering 2 in the morning :P. Thanks for the catch!

        – John Moutafis
        Nov 25 '18 at 12:42




        1




        1





        @jbodily As yrekkehs caught my error, the correct method you should use is the select_related. I edited my answer and you should try it as it is now. Sorry for the inconvenience!

        – John Moutafis
        Nov 25 '18 at 12:43





        @jbodily As yrekkehs caught my error, the correct method you should use is the select_related. I edited my answer and you should try it as it is now. Sorry for the inconvenience!

        – John Moutafis
        Nov 25 '18 at 12:43













        I appreciate the due diligence, but unfortunately it doesn't seem to help on creation. Let me clarify the relations again: A book has many tags, a tag has one book. A tag has one tag_category, a tag_category has many tags. So two one-to-many relations. There is no tags__categories. But secondly, I've updated my question to reflect your solution but still find that on create, the db is queried once per category.

        – jbodily
        Nov 26 '18 at 17:00





        I appreciate the due diligence, but unfortunately it doesn't seem to help on creation. Let me clarify the relations again: A book has many tags, a tag has one book. A tag has one tag_category, a tag_category has many tags. So two one-to-many relations. There is no tags__categories. But secondly, I've updated my question to reflect your solution but still find that on create, the db is queried once per category.

        – jbodily
        Nov 26 '18 at 17:00













        0














        I think the issue here is that the Tag constructor is automatically converting the category id that you pass in as category into a TagCategory instance by looking it up from the database. The way to avoid that is by doing something like the following if you know that all of the category ids are valid:




        def create(self, validated_data):
        with transaction.atomic():
        tags = validated_data.pop('tags')
        book = Book.objects.create(**validated_data)
        tag_instances = [ Tag(book_id=book.id, page=x['page'], category_id=x['category']) for x in tags ]
        Tag.objects.bulk_create(tag_instances)
        return book





        share|improve this answer
























        • This feels like it should be right, but check out the update I added: In the view I call serializer.is_valid(), then serializer.save(). The problem is on the is_valid check, if the category_id is set and not the category, it says category: This field may not be null.

          – jbodily
          Nov 25 '18 at 2:16


















        0














        I think the issue here is that the Tag constructor is automatically converting the category id that you pass in as category into a TagCategory instance by looking it up from the database. The way to avoid that is by doing something like the following if you know that all of the category ids are valid:




        def create(self, validated_data):
        with transaction.atomic():
        tags = validated_data.pop('tags')
        book = Book.objects.create(**validated_data)
        tag_instances = [ Tag(book_id=book.id, page=x['page'], category_id=x['category']) for x in tags ]
        Tag.objects.bulk_create(tag_instances)
        return book





        share|improve this answer
























        • This feels like it should be right, but check out the update I added: In the view I call serializer.is_valid(), then serializer.save(). The problem is on the is_valid check, if the category_id is set and not the category, it says category: This field may not be null.

          – jbodily
          Nov 25 '18 at 2:16
















        0












        0








        0







        I think the issue here is that the Tag constructor is automatically converting the category id that you pass in as category into a TagCategory instance by looking it up from the database. The way to avoid that is by doing something like the following if you know that all of the category ids are valid:




        def create(self, validated_data):
        with transaction.atomic():
        tags = validated_data.pop('tags')
        book = Book.objects.create(**validated_data)
        tag_instances = [ Tag(book_id=book.id, page=x['page'], category_id=x['category']) for x in tags ]
        Tag.objects.bulk_create(tag_instances)
        return book





        share|improve this answer













        I think the issue here is that the Tag constructor is automatically converting the category id that you pass in as category into a TagCategory instance by looking it up from the database. The way to avoid that is by doing something like the following if you know that all of the category ids are valid:




        def create(self, validated_data):
        with transaction.atomic():
        tags = validated_data.pop('tags')
        book = Book.objects.create(**validated_data)
        tag_instances = [ Tag(book_id=book.id, page=x['page'], category_id=x['category']) for x in tags ]
        Tag.objects.bulk_create(tag_instances)
        return book






        share|improve this answer












        share|improve this answer



        share|improve this answer










        answered Nov 25 '18 at 0:43









        2ps2ps

        8,05221031




        8,05221031













        • This feels like it should be right, but check out the update I added: In the view I call serializer.is_valid(), then serializer.save(). The problem is on the is_valid check, if the category_id is set and not the category, it says category: This field may not be null.

          – jbodily
          Nov 25 '18 at 2:16





















        • This feels like it should be right, but check out the update I added: In the view I call serializer.is_valid(), then serializer.save(). The problem is on the is_valid check, if the category_id is set and not the category, it says category: This field may not be null.

          – jbodily
          Nov 25 '18 at 2:16



















        This feels like it should be right, but check out the update I added: In the view I call serializer.is_valid(), then serializer.save(). The problem is on the is_valid check, if the category_id is set and not the category, it says category: This field may not be null.

        – jbodily
        Nov 25 '18 at 2:16







        This feels like it should be right, but check out the update I added: In the view I call serializer.is_valid(), then serializer.save(). The problem is on the is_valid check, if the category_id is set and not the category, it says category: This field may not be null.

        – jbodily
        Nov 25 '18 at 2:16













        0














        I've come up with an answer that gets things working (but that I'm not thrilled about): Modify the Tag Serializer like this:



        class TagSerializer(serializers.ModelSerializer):

        category_id = serializers.IntegerField()

        class Meta:
        model = Tag
        exclude = ['id', 'book', 'category']


        This allows me to read/write a category_id without having the overhead of validations. Adding category to exclude does mean that the serializer will ignore category if it's set on the instance.






        share|improve this answer




























          0














          I've come up with an answer that gets things working (but that I'm not thrilled about): Modify the Tag Serializer like this:



          class TagSerializer(serializers.ModelSerializer):

          category_id = serializers.IntegerField()

          class Meta:
          model = Tag
          exclude = ['id', 'book', 'category']


          This allows me to read/write a category_id without having the overhead of validations. Adding category to exclude does mean that the serializer will ignore category if it's set on the instance.






          share|improve this answer


























            0












            0








            0







            I've come up with an answer that gets things working (but that I'm not thrilled about): Modify the Tag Serializer like this:



            class TagSerializer(serializers.ModelSerializer):

            category_id = serializers.IntegerField()

            class Meta:
            model = Tag
            exclude = ['id', 'book', 'category']


            This allows me to read/write a category_id without having the overhead of validations. Adding category to exclude does mean that the serializer will ignore category if it's set on the instance.






            share|improve this answer













            I've come up with an answer that gets things working (but that I'm not thrilled about): Modify the Tag Serializer like this:



            class TagSerializer(serializers.ModelSerializer):

            category_id = serializers.IntegerField()

            class Meta:
            model = Tag
            exclude = ['id', 'book', 'category']


            This allows me to read/write a category_id without having the overhead of validations. Adding category to exclude does mean that the serializer will ignore category if it's set on the instance.







            share|improve this answer












            share|improve this answer



            share|improve this answer










            answered Nov 25 '18 at 3:45









            jbodilyjbodily

            5251513




            5251513























                0














                Problem is that you don't set created tags to the book instance so serializer try to get this while returning.



                You need to set it to the book as a list:



                def create(self, validated_data):
                with transaction.atomic():
                book = Book.objects.create(**validated_data)

                # Add None as a default and check that tags are provided
                # If you don't do that, serializer will raise error if request don't have 'tags'

                tags = validated_data.pop('tags', None)
                tags_to_create =

                if tags:
                tags_to_create = [Tag(book=book, **tag) for tag in tags]
                Tag.objects.bulk_create(tags_to_create)

                # Here I set tags to the book instance
                setattr(book, 'tags', tags_to_create)

                return book


                Provide Meta.fields tuple for TagSerializer (it's weird that this serializer don't raise error saying that fields tuple is required)



                class TagSerializer(serializers.ModelSerializer):
                class Meta:
                model = Tag
                fields = ('category', 'page',)


                Prefetching tag.category should be NOT necessary in this case because it's just id.



                You will need prefetching Book.tags for GET method. The simplest solution is to create static method for serializer and use it in viewset get_queryset method like this:



                class BookSerializer(serializers.ModelSerializer):
                ...
                @staticmethod
                def setup_eager_loading(queryset): # It can be named any name you like
                queryset = queryset.prefetch_related('tags')

                return queryset

                class BookViewSet(views.APIView):
                ...
                def get_queryset(self):
                self.queryset = BookSerializer.setup_eager_loading(self.queryset)
                # Every GET request will prefetch 'tags' for every book by default

                return super(BookViewSet, self).get_queryset()





                share|improve this answer




























                  0














                  Problem is that you don't set created tags to the book instance so serializer try to get this while returning.



                  You need to set it to the book as a list:



                  def create(self, validated_data):
                  with transaction.atomic():
                  book = Book.objects.create(**validated_data)

                  # Add None as a default and check that tags are provided
                  # If you don't do that, serializer will raise error if request don't have 'tags'

                  tags = validated_data.pop('tags', None)
                  tags_to_create =

                  if tags:
                  tags_to_create = [Tag(book=book, **tag) for tag in tags]
                  Tag.objects.bulk_create(tags_to_create)

                  # Here I set tags to the book instance
                  setattr(book, 'tags', tags_to_create)

                  return book


                  Provide Meta.fields tuple for TagSerializer (it's weird that this serializer don't raise error saying that fields tuple is required)



                  class TagSerializer(serializers.ModelSerializer):
                  class Meta:
                  model = Tag
                  fields = ('category', 'page',)


                  Prefetching tag.category should be NOT necessary in this case because it's just id.



                  You will need prefetching Book.tags for GET method. The simplest solution is to create static method for serializer and use it in viewset get_queryset method like this:



                  class BookSerializer(serializers.ModelSerializer):
                  ...
                  @staticmethod
                  def setup_eager_loading(queryset): # It can be named any name you like
                  queryset = queryset.prefetch_related('tags')

                  return queryset

                  class BookViewSet(views.APIView):
                  ...
                  def get_queryset(self):
                  self.queryset = BookSerializer.setup_eager_loading(self.queryset)
                  # Every GET request will prefetch 'tags' for every book by default

                  return super(BookViewSet, self).get_queryset()





                  share|improve this answer


























                    0












                    0








                    0







                    Problem is that you don't set created tags to the book instance so serializer try to get this while returning.



                    You need to set it to the book as a list:



                    def create(self, validated_data):
                    with transaction.atomic():
                    book = Book.objects.create(**validated_data)

                    # Add None as a default and check that tags are provided
                    # If you don't do that, serializer will raise error if request don't have 'tags'

                    tags = validated_data.pop('tags', None)
                    tags_to_create =

                    if tags:
                    tags_to_create = [Tag(book=book, **tag) for tag in tags]
                    Tag.objects.bulk_create(tags_to_create)

                    # Here I set tags to the book instance
                    setattr(book, 'tags', tags_to_create)

                    return book


                    Provide Meta.fields tuple for TagSerializer (it's weird that this serializer don't raise error saying that fields tuple is required)



                    class TagSerializer(serializers.ModelSerializer):
                    class Meta:
                    model = Tag
                    fields = ('category', 'page',)


                    Prefetching tag.category should be NOT necessary in this case because it's just id.



                    You will need prefetching Book.tags for GET method. The simplest solution is to create static method for serializer and use it in viewset get_queryset method like this:



                    class BookSerializer(serializers.ModelSerializer):
                    ...
                    @staticmethod
                    def setup_eager_loading(queryset): # It can be named any name you like
                    queryset = queryset.prefetch_related('tags')

                    return queryset

                    class BookViewSet(views.APIView):
                    ...
                    def get_queryset(self):
                    self.queryset = BookSerializer.setup_eager_loading(self.queryset)
                    # Every GET request will prefetch 'tags' for every book by default

                    return super(BookViewSet, self).get_queryset()





                    share|improve this answer













                    Problem is that you don't set created tags to the book instance so serializer try to get this while returning.



                    You need to set it to the book as a list:



                    def create(self, validated_data):
                    with transaction.atomic():
                    book = Book.objects.create(**validated_data)

                    # Add None as a default and check that tags are provided
                    # If you don't do that, serializer will raise error if request don't have 'tags'

                    tags = validated_data.pop('tags', None)
                    tags_to_create =

                    if tags:
                    tags_to_create = [Tag(book=book, **tag) for tag in tags]
                    Tag.objects.bulk_create(tags_to_create)

                    # Here I set tags to the book instance
                    setattr(book, 'tags', tags_to_create)

                    return book


                    Provide Meta.fields tuple for TagSerializer (it's weird that this serializer don't raise error saying that fields tuple is required)



                    class TagSerializer(serializers.ModelSerializer):
                    class Meta:
                    model = Tag
                    fields = ('category', 'page',)


                    Prefetching tag.category should be NOT necessary in this case because it's just id.



                    You will need prefetching Book.tags for GET method. The simplest solution is to create static method for serializer and use it in viewset get_queryset method like this:



                    class BookSerializer(serializers.ModelSerializer):
                    ...
                    @staticmethod
                    def setup_eager_loading(queryset): # It can be named any name you like
                    queryset = queryset.prefetch_related('tags')

                    return queryset

                    class BookViewSet(views.APIView):
                    ...
                    def get_queryset(self):
                    self.queryset = BookSerializer.setup_eager_loading(self.queryset)
                    # Every GET request will prefetch 'tags' for every book by default

                    return super(BookViewSet, self).get_queryset()






                    share|improve this answer












                    share|improve this answer



                    share|improve this answer










                    answered Nov 25 '18 at 10:58









                    mon iomon io

                    2464




                    2464























                        0














                        select_related function will check ForeignKey in the first time.
                        Actually,this is a ForeignKey check in the relational database and you can use SET FOREIGN_KEY_CHECKS=0; in database to close inspection.






                        share|improve this answer






























                          0














                          select_related function will check ForeignKey in the first time.
                          Actually,this is a ForeignKey check in the relational database and you can use SET FOREIGN_KEY_CHECKS=0; in database to close inspection.






                          share|improve this answer




























                            0












                            0








                            0







                            select_related function will check ForeignKey in the first time.
                            Actually,this is a ForeignKey check in the relational database and you can use SET FOREIGN_KEY_CHECKS=0; in database to close inspection.






                            share|improve this answer















                            select_related function will check ForeignKey in the first time.
                            Actually,this is a ForeignKey check in the relational database and you can use SET FOREIGN_KEY_CHECKS=0; in database to close inspection.







                            share|improve this answer














                            share|improve this answer



                            share|improve this answer








                            edited Dec 4 '18 at 13:43

























                            answered Dec 4 '18 at 10:45









                            mengxunmengxun

                            414




                            414






























                                draft saved

                                draft discarded




















































                                Thanks for contributing an answer to Stack Overflow!


                                • Please be sure to answer the question. Provide details and share your research!

                                But avoid



                                • Asking for help, clarification, or responding to other answers.

                                • Making statements based on opinion; back them up with references or personal experience.


                                To learn more, see our tips on writing great answers.




                                draft saved


                                draft discarded














                                StackExchange.ready(
                                function () {
                                StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53463217%2fdjango-serializer-nested-creation-how-to-avoid-n1-queries-on-relations%23new-answer', 'question_page');
                                }
                                );

                                Post as a guest















                                Required, but never shown





















































                                Required, but never shown














                                Required, but never shown












                                Required, but never shown







                                Required, but never shown

































                                Required, but never shown














                                Required, but never shown












                                Required, but never shown







                                Required, but never shown







                                Popular posts from this blog

                                Costa Masnaga

                                Fotorealismo

                                Sidney Franklin