Django Serializer Nested Creation: How to avoid N+1 queries on relations
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:
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
add a comment |
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:
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
Use the .prefetch_related() function to solve
– Flavio Milan
Nov 24 '18 at 23:24
add a comment |
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:
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
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:
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
python django django-rest-framework django-serializer
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
add a comment |
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
add a comment |
5 Answers
5
active
oldest
votes
The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:
- Serialize and check the validity of input data.
- 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.
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 theselect_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 onebook
. Atag
has onetag_category
, atag_category
has many tags. So two one-to-many relations. There is notags__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
add a comment |
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
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 sayscategory: This field may not be null.
– jbodily
Nov 25 '18 at 2:16
add a comment |
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.
add a comment |
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()
add a comment |
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.
add a comment |
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
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:
- Serialize and check the validity of input data.
- 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.
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 theselect_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 onebook
. Atag
has onetag_category
, atag_category
has many tags. So two one-to-many relations. There is notags__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
add a comment |
The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:
- Serialize and check the validity of input data.
- 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.
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 theselect_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 onebook
. Atag
has onetag_category
, atag_category
has many tags. So two one-to-many relations. There is notags__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
add a comment |
The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:
- Serialize and check the validity of input data.
- 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.
The DRF serializer is not the place (in my own opinion) to optimize a DB query. Serializer has 2 jobs:
- Serialize and check the validity of input data.
- 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.
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 theselect_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 onebook
. Atag
has onetag_category
, atag_category
has many tags. So two one-to-many relations. There is notags__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
add a comment |
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 theselect_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 onebook
. Atag
has onetag_category
, atag_category
has many tags. So two one-to-many relations. There is notags__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
add a comment |
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
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 sayscategory: This field may not be null.
– jbodily
Nov 25 '18 at 2:16
add a comment |
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
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 sayscategory: This field may not be null.
– jbodily
Nov 25 '18 at 2:16
add a comment |
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
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
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 sayscategory: This field may not be null.
– jbodily
Nov 25 '18 at 2:16
add a comment |
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 sayscategory: 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
add a comment |
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.
add a comment |
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.
add a comment |
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.
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.
answered Nov 25 '18 at 3:45
jbodilyjbodily
5251513
5251513
add a comment |
add a comment |
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()
add a comment |
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()
add a comment |
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()
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()
answered Nov 25 '18 at 10:58
mon iomon io
2464
2464
add a comment |
add a comment |
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.
add a comment |
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.
add a comment |
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.
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.
edited Dec 4 '18 at 13:43
answered Dec 4 '18 at 10:45
mengxunmengxun
414
414
add a comment |
add a comment |
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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
Use the .prefetch_related() function to solve
– Flavio Milan
Nov 24 '18 at 23:24