I'm try to decrease query counts for using prefetch_related and select_related. However, it seems doesn't work.
in Match Model have 5 ForeignKey fields, so when i get the query counts it will return 5. Also when i delete def get_queryset method in MatchDetailAPIView. The Api still work. ( e.g 127.0.0.1:8000/game/match/match-1 is working whether or not the get_queryset method.
I can't find where I'm doing wrong.
Models.py
class Game(models.Model):
name = models.CharField(max_length=255)
...
class Match(models.Model):
name = models.TextField(blank=False, null=False)
game = models.ForeignKey(Game, on_delete=models.SET_NULL, null=True)
tournament = models.ForeignKey(Tournament, on_delete=models.SET_NULL, null=True, blank=True)
....
serializers.py
class MatchSerializer(serializers.ModelSerializer):
class Meta:
model = Match
fields = '__all__'
#exclude = ['participant', ]
views.py
class MatchDetailAPIView(RetrieveAPIView):
serializer_class = MatchSerializer
def get_queryset(self):
queryset =Match.objects.all().prefetch_related('game_id')
return queryset
def get_object(self):
gameslug = self.kwargs.get('gameslug')
slug = self.kwargs.get('slug')
# find the user
game = Game.objects.get(slug=gameslug)
return Match.objects.get(slug=slug, game__slug=game.slug)
def get_serilizer_context(self, *args, **kwargs):
return {'request': self.request}
You should use select_related for ForeignKey fields, since prefetch_related does the joining in Python, and select_related creates an SQL join.
You should also refer to the relationship by it's name, not the id of the ForeignKey ('game_id' vs. 'game'):
def get_queryset(self):
queryset =Match.objects.all().select_related('game')
return queryset
As mentioned in django docs, select_related should be used for foeign-key relationships. Also for RetrieveAPIView override the get method.
def get(self, request, *args, **kwargs):
gameslug = self.kwargs.get('gameslug')
slug = self.kwargs.get('slug')
game = Game.objects.get(slug=gameslug)
return Match.objects.filter(slug=slug, game__slug=gameslug).select_related('game').first()
Related
I have a comment field for every blog post. I want to pass Comment.objects.all() from Views.py to ModelSerializer def get_comments(self, obj) to reduce the number of sql queries. As I am serializing a list of blog posts
Views.py
class BlogViewSet(ModelViewSet):
queryset = Blog.objects.all().annotate(
author_name=F('author__username')
)
serializer_class = BlogSerializer
permission_classes = [IsOwnerOrReadOnly]
def list(self, request):
return Response({'blogs': BlogSerializer(self.queryset, many=True).data})
Serializers.py
class BlogSerializer(ModelSerializer):
author_name = serializers.CharField(read_only=True)
comments = SerializerMethodField()
class Meta:
model = Blog
fields = ('title_text', 'main_text', 'datepublished', 'author_name', 'id', 'comments')
def get_comments(self, obj):
# filter comment
comment_object = Comment.objects.filter(post_id=obj.id)
comments = CommentSerializer(comment_object, many=True).data
return comments
You don't have to pass anything from the View.
First, you have to change the comments field in your BlogSerializer.
class CommentSerializer(ModelSerializer):
# This serializer should have all the details of your comments.
....
class Meta:
model = Comment
fields = "__all__" # Or whatever fields you want to set.
class BlogSerializer(ModelSerializer):
author_name = serializers.CharField(read_only=True)
comments = CommentSerializer(many=True, read_only=True) # I am not sure of the source of your comment reverse manager name
class Meta:
model = Blog
fields = ('title_text', 'main_text', 'datepublished', 'author_name', 'id', 'comments')
Second, you have to make a small change to your view's queryset in order to reduce the number of queries sent to the database by using prefetch_related
class BlogViewSet(ModelViewSet):
queryset = Blog.objects.prefetch_related('comments').all().annotate(
author_name=F('author__username')
)
serializer_class = BlogSerializer
permission_classes = [IsOwnerOrReadOnly]
def list(self, request):
return Response({'blogs': BlogSerializer(self.get_queryset(), many=True).data})
I assumed in the code snippets that you didn't set a related_name on your Blog ForeignKey for the Comment model, so by default, its related manager on Blog will be comment_set
Update
Your models should look like this, in order for this solution to work
class Comment(models.Model):
...
blog = models.ForeignKey('Blog', on_delete=models.CASCADE, related_name='comments')
...
class Blog(models.Model):
# comment = models.ForeignKey(Comment, on_delete=models.CASCADE, null=True)
# this foreign key shouldn't be here, remove it.
....
Do the changes on Serializer and View
# in BlogSerializer
comments = CommentSerializer(many=True, read_only=True)
# In BlogViewSet
queryset = Blog.objects.prefetch_related('comments').all().annotate(
author_name=F('author__username')
)
In my Django app, when I add the host_count field to my serializer to get the number of hosts for each domain, the performance of the API response suffers dramatically.
Without host_count: 300ms
With host_count: 15s
I tried adding 'host_set' to the prefetch_related method but it did not help.
Would an annotation using Count help me here? How can I optimize the fetching of this value?
serializers.py
class DomainSerializer(serializers.Serializer):
name = serializers.CharField(read_only=True)
org_name = serializers.CharField(source='org.name', read_only=True)
created = serializers.DateTimeField(read_only=True)
last_host_search = serializers.DateTimeField(read_only=True)
host_count = serializers.SerializerMethodField()
def get_host_count(self, obj):
return Host.objects.filter(domain=obj).count()
views.py
class DomainList(generics.ListAPIView):
def get(self, request, format=None):
domains = Domain.objects.prefetch_related('org').all()
serializer = DomainSerializer(domains, many=True)
return Response(serializer.data)
models.py
class Domain(models.Model):
created = models.DateTimeField(auto_now_add=True)
last_host_search = models.DateTimeField(auto_now=True)
name = models.CharField(unique=True, max_length=settings.MAX_CHAR_COUNT, blank=False, null=False)
org = models.ForeignKey(Org, on_delete=models.CASCADE, blank=True, null=True)
You can work with .annotate(…) [Django-doc] to count the related objects in the same query:
from django.db.models import Count
class DomainList(generics.ListAPIView):
def get(self, request, format=None):
domains = Domain.objects.prefetch_related('org').annotate(
host_count=Count('host')
)
serializer = DomainSerializer(domains, many=True)
return Response(serializer.data)
In the serializer, you then simply retrieve the corresponding attribute:
class DomainSerializer(serializers.Serializer):
name = serializers.CharField(read_only=True)
org_name = serializers.CharField(source='org.name', read_only=True)
created = serializers.DateTimeField(read_only=True)
last_host_search = serializers.DateTimeField(read_only=True)
host_count = serializers.IntegerField(read_only=True)
This will make a query that looks like:
SELECT domain.*, org.*, COUNT(host.id)
FROM domain
LEFT OUTER JOIN org ON domain.org_id = org.id
LEFT OUTER JOIN host ON host.domain_id = domain.id
GROUP BY domain.id, org.id
I have a django project with a django rest framework. I am trying to use the Retreive API View to return the members details. I want to grab all the member records based on a parameter that is passed in teh url with the groups name. I tried using the .get() and .filter(). The .get() returned an error of returning more than 2 items. The .filter() is not part of the dict.
I tried .list and .retrieve
How can I retreive the data objects based with more than 1 item. Here is my View that I am calling.
class MemberDetailView(RetrieveAPIView):
queryset = Member.objects.all()
serializer_class = MemberSerializer
def get_object(self):
return self.queryset.filter(group__name=self.kwargs.filter('name'))
model
class Member(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
host = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.group.name + ' - ' + self.user.username
urls
path('members/', MemberListView.as_view()),
path('members/<name>', MemberDetailView.as_view()),
_________________________________________-
UPDATE:
so i am getting the error when i override the list:
TypeError at /api/groups/members/homiez
list() got an unexpected keyword argument 'name'
when i dont override the list I get an empty results object.
Here is the code I have right now...
class MemberGroupListView(ListAPIView):
serializer_class = MemberSerializer
def get_queryset(self):
return Member.objects.filter(group__name=self.request.query_params.get('name'))
def list(self, request):
# Note the use of `get_queryset()` instead of `self.queryset`
queryset = self.get_queryset()
serializer = MemberSerializer(queryset, many=True)
return Response(serializer)
models
class Group(models.Model):
name = models.CharField(max_length=42)
description = models.CharField(max_length=220)
user_count = models.IntegerField()
status = models.CharField(max_length=12)
image = models.ImageField(upload_to='group_images/')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name + ' - ' + self.created_by.username
class Member(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
host = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.group.name + ' - ' + self.user.username
urls:
path('members/<name>', MemberGroupListView.as_view()),
RetrieveAPIView is designed for retrieving single instance. You should use ListAPIView instead. And use get_queryset method instead of get_object
ListAPIView calls serializer with many=True param, which returns a list of objects instead of one.
Django ORM get is used to retrieve only one object. filter maybe used to query for multiple objects in a queryset. Hence for retrieving multiple stuff you have to use filter.
In your View I think you need to override the list method.
class MemberDetailView(RetrieveAPIView):
serializer_class = MemberSerializer
def get_queryset(self):
"""
This view should return a list of all the purchases for
the user as determined by the username portion of the URL.
"""
username = self.kwargs['username']
return Purchase.objects.filter(group__name=self.request.query_params.get('name'))
def list(self, request):
# Note the use of `get_queryset()` instead of `self.queryset`
queryset = self.get_queryset()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
Note: self.request.query_params.get() is how you retrieve data from request object. It is about a dictionary operations rather than ORM operations. So don't do .filter() on self.kwargs.
In addition to #Alex's answer, you need to implement the filter method or typicallu add the filter backend to be able to filter querysets based on url filter parameters.
I would go with a filter backend using django-filter as it is quite effective, flexiblle and has become quite a standard for Django applications.
You need something like this:
from django_filters import rest_framework as filters
class MemberDetailView(RetrieveAPIView):
queryset = Member.objects.all()
serializer_class = MemberSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('user', 'host', 'group')
You can read the Django-filter docs for more details on how to use it.
I'm trying to add a nested serializer to an existing serializer based on some criteria of the parent model, not a foreign key. The use case is to return a 'Research' object with an array of 'ResearchTemplate' objects that are identified by filtering on a Postgres ArrayField.
Models
class Research(TimeStampedModel):
category = models.CharField(max_length=100, choices=RESEARCH_TEMPLATE_CATEGORIES, default='quote')
body = models.CharField(max_length=1000, blank=True, default='') #The body of text comprising the nugget
additionaldata = JSONField(null=True) # all the varying values to be merged into a wrapper
def __str__(self):
return self.body
class Meta:
ordering = ('created',)
class ResearchTemplate(TimeStampedModel):
template = models.TextField(blank=True, default='')
category = models.CharField(max_length=100, choices=RESEARCH_TEMPLATE_CATEGORIES, default='quote')
mergefields = ArrayField(models.CharField(max_length=200), blank=True)
def save(self, *args, **kwargs):
merges = re.findall("{{(.*?)}}", self.template) #get all the template names from within the mustaches
self.mergefields = list(set(merges)) #TODO: Make Unique
super(TimeStampedModel, self).save(*args, **kwargs)
def __str__(self):
return self.wrapper
class Meta:
ordering = ('created',)
Serializers
class ResearchSerializer(serializers.ModelSerializer):
templates = ResearchTemplateSerializer(many=True)
class Meta:
model = Research
fields = ('id', 'created', 'speaker', 'body', 'templates')
class ResearchTemplateSerializer(serializers.RelatedField):
def get_queryset(self, values):
return ResearchTemplate.objects.filter(mergefields__contained_by=['django']) #This must an array of keys from the Research object's JSON field
class Meta:
model = ResearchTemplate
fields = ('id', 'template')
I've been able to nest serializers when there is a foreign key mapping them, however I am unable to do so with a custom queryset. Perhaps I'm not thinking about this properly, and I require some form of 'relationship' field on the Research model.
How can I nest a serialized list of all rows that are returned from a filter with values specified from the parent model?
You can use DRF's SerializerMethodField.
Define your ResearchTemplateSerializer as a normal ModelSerializer, not as a RelatedField.
Then replace your ResearchSerializer with this:
class ResearchSerializer(serializers.ModelSerializer):
templates = serializers.SerializerMethodField()
class Meta:
model = Research
fields = ('id', 'created', 'speaker', 'body', 'templates')
def get_templates(self, obj):
values = obj.get_values() # whatever your filter values are. obj is the Research instance
templates = ResearchTemplate.objects.filter(mergefields__contained_by=values) # Or whatever queryset filter
return ResearchTemplateSerializer(templates, many=True).data
I'm developing RESTFul services with DRF and I have multiple databases depending on the country (see my last question here)
I'm having a problem now with relationships, I have two models: Category and SubCategory:
class SubCategory(models.Model):
objects = CountryQuerySet.as_manager()
id = models.AutoField(primary_key=True,db_column='sub_category_id')
name = models.TextField()
female_items_in_category = models.BooleanField()
male_items_in_category = models.BooleanField()
kids_items_in_category = models.BooleanField()
category = models.ForeignKey('Category')
class Meta:
managed = True
db_table = Constants().SUBCATEGORY
And the serializer is:
class SubCategorySerializer(serializers.ModelSerializer):
category = PrimaryKeyRelatedField(queryset=Category.objects.using('es').all())
class Meta:
model = SubCategory
fields = ('id', 'name','female_items_in_category','male_items_in_category','kids_items_in_category','category')
If I don't set the queryset with the proper country it fails, because it doesn't know where to get the category.
Here the problem
I already set the country in the serializer context (in the ModelViewSet):
def get_serializer_context(self):
return {Constants().COUNTRY: self.kwargs.get(Constants().COUNTRY)}
But I can not find the proper way to get the self.context.get(Constants().COUNTRY) in the serializer.
Do you any have an idea to solve this? Thanks!
Well, I found a solution to my problem: I overwrite the method get_fields in the serializer:
def get_fields(self, *args, **kwargs):
fields = super(SubCategorySerializer, self).get_fields()
country = self.context.get(Constants().COUNTRY)
qs = Category.objects.using(country).all()
fields['category'].queryset = qs
return fields
And that works!