I have the following models:
class User(models.Model):
name = models.Charfield()
email = models.EmailField()
class Friendship(models.Model):
from_friend = models.ForeignKey(User)
to_friend = models.ForeignKey(User)
And those models are used in the following view and serializer:
class GetAllUsers(generics.ListAPIView):
authentication_classes = (SessionAuthentication, TokenAuthentication)
permission_classes = (permissions.IsAuthenticated,)
serializer_class = GetAllUsersSerializer
model = User
def get_queryset(self):
return User.objects.all()
class GetAllUsersSerializer(serializers.ModelSerializer):
is_friend_already = serializers.SerializerMethodField('get_is_friend_already')
class Meta:
model = User
fields = ('id', 'name', 'email', 'is_friend_already',)
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
if request.user != obj and Friendship.objects.filter(from_friend = user):
return True
else:
return False
So basically, for each user returned by the GetAllUsers view, I want to print out whether the user is a friend with the requester (actually I should check both from_ and to_friend, but does not matter for the question in point)
What I see is that for N users in database, there is 1 query for getting all N users, and then 1xN queries in the serializer's get_is_friend_already
Is there a way to avoid this in the rest-framework way? Maybe something like passing a select_related included query to the serializer that has the relevant Friendship rows?
Django REST Framework cannot automatically optimize queries for you, in the same way that Django itself won't. There are places you can look at for tips, including the Django documentation. It has been mentioned that Django REST Framework should automatically, though there are some challenges associated with that.
This question is very specific to your case, where you are using a custom SerializerMethodField that makes a request for each object that is returned. Because you are making a new request (using the Friends.objects manager), it is very difficult to optimize the query.
You can make the problem better though, by not creating a new queryset and instead getting the friend count from other places. This will require a backwards relation to be created on the Friendship model, most likely through the related_name parameter on the field, so you can prefetch all of the Friendship objects. But this is only useful if you need the full objects, and not just a count of the objects.
This would result in a view and serializer similar to the following:
class Friendship(models.Model):
from_friend = models.ForeignKey(User, related_name="friends")
to_friend = models.ForeignKey(User)
class GetAllUsers(generics.ListAPIView):
...
def get_queryset(self):
return User.objects.all().prefetch_related("friends")
class GetAllUsersSerializer(serializers.ModelSerializer):
...
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
friends = set(friend.from_friend_id for friend in obj.friends)
if request.user != obj and request.user.id in friends:
return True
else:
return False
If you just need a count of the objects (similar to using queryset.count() or queryset.exists()), you can include annotate the rows in the queryset with the counts of reverse relationships. This would be done in your get_queryset method, by adding .annotate(friends_count=Count("friends")) to the end (if the related_name was friends), which will set the friends_count attribute on each object to the number of friends.
This would result in a view and serializer similar to the following:
class Friendship(models.Model):
from_friend = models.ForeignKey(User, related_name="friends")
to_friend = models.ForeignKey(User)
class GetAllUsers(generics.ListAPIView):
...
def get_queryset(self):
from django.db.models import Count
return User.objects.all().annotate(friends_count=Count("friends"))
class GetAllUsersSerializer(serializers.ModelSerializer):
...
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
if request.user != obj and obj.friends_count > 0:
return True
else:
return False
Both of these solutions will avoid N+1 queries, but the one you pick depends on what you are trying to achieve.
Described N+1 problem is a number one issue during Django REST Framework performance optimization, so from various opinions, it requires more solid approach then direct prefetch_related() or select_related() in get_queryset() view method.
Based on collected information, here's a robust solution that eliminates N+1 (using OP's code as an example). It's based on decorators and slightly less coupled for larger applications.
Serializer:
class GetAllUsersSerializer(serializers.ModelSerializer):
friends = FriendSerializer(read_only=True, many=True)
# ...
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related("friends")
return queryset
Here we use static class method to build the specific queryset.
Decorator:
def setup_eager_loading(get_queryset):
def decorator(self):
queryset = get_queryset(self)
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
return decorator
This function modifies returned queryset in order to fetch related records for a model as defined in setup_eager_loading serializer method.
View:
class GetAllUsers(generics.ListAPIView):
serializer_class = GetAllUsersSerializer
#setup_eager_loading
def get_queryset(self):
return User.objects.all()
This pattern may look like an overkill, but it's certainly more DRY and has advantage over direct queryset modification inside views, as it allows more control over related entities and eliminates unnecessary nesting of related objects.
Using this metaclass DRF optimize ModelViewSet MetaClass
from django.utils import six
#six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
You can split the view into two query.
First, only get the Users list (without is_friend_already field). This only require one query.
Second, get the friends list of request.user.
Third, modify the results depending on if the user is in the request.user's friend list.
class GetAllUsersSerializer(serializers.ModelSerializer):
...
class UserListView(ListView):
def get(self, request):
friends = request.user.friends
data = []
for user in self.get_queryset():
user_data = GetAllUsersSerializer(user).data
if user in friends:
user_data['is_friend_already'] = True
else:
user_data['is_friend_already'] = False
data.append(user_data)
return Response(status=200, data=data)
Related
I am building a blog app with React and Django and I am serializing model's instances saved by particular user, First I am just trying to test with .all() then I am planning to filter by specific user But when I serialize queryset with Serializer like:
class BlogSerializerApiView(viewsets.ModelViewSet):
serializer_class = BlogSerializer
def get_queryset(self, *args, **kwargs):
queryset = Blog.objects.all()
output_serializer = BlogSerializer(queryset, many=True)
print(output_serializer.data)
return "Testing"
It is showing in console:
[OrderedDict(), OrderedDict()]
and when I access it like
print(output_serializer)
Then it is showing:
BlogSerializer(<QuerySet [<Blog: user_1 - Blog_title>, <Blog: user_2 - second_blog_title>]>, many=True):
serializer.py:
class BlogSerializer(serializers.Serializer):
class Meta:
model = Blog
fields = ['title']
models.py:
class Blog(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=30, default='')
def __str__(self):
return f"{self.user} - {self.title}"
What I am trying to do:
I am trying to serialize queryset to show on page in react frontend, I will relate with specific user later.
I have tried many times by changing CBV serialization method by generics.ListAPIView instead of viewsets.ModelViewSet but still same thing.
There is a concept error here. The get_queryset function is not supposed to return serialized data. It must return a QuerySet of model objects.
To achieve what you want you can just do:
class BlogSerializerApiView(viewsets.ModelViewSet):
serializer_class = BlogSerializer
def get_queryset(self, *args, **kwargs):
return Blog.objects.all()
The Django Rest Framework will take care of serializing data.
In fact, you can even do it way more simple. Defining the view's queryset field like this:
class BlogSerializerApiView(viewsets.ModelViewSet):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
Additional:
You said you will relate to current user later. You could achieve that in fact in the get_queryset method filtering aginst the user
class BlogSerializerApiView(viewsets.ModelViewSet):
serializer_class = BlogSerializer
def get_queryset(self, *args, **kwargs):
return Blog.objects.filter(user_id=USER_ID)
Hope this helps!
I was using
class BlogSerializer(serializers.Serializer):
.......
so it was showing empty results (no idea why, I think its deprecated)
After replaceing it with
class BlogSerializer(serializers.HyperlinkedModelSerializer):
It worked
First, I would like to present how my managers, models, serializers and views look like upfront.
class PublishedManager(models.Manager):
"""
Only published articles. `due_date` is past.
"""
def get_queryset(self):
now = timezone.now()
return super().get_queryset().filter(due_date__lt=now)
class UnpublishedManager(models.Manager):
"""
Only unpublished articles. `due_date` is future.
"""
def announced(self):
return self.get_queryset().filter(announced=True)
def get_queryset(self):
now = timezone.now()
return super().get_queryset().filter(due_date__gt=now)
class Article(models.Model):
content = models.TextField()
due_date = models.DateTimeField()
announced = models.BooleanField()
# managers
objects = models.Manager() # standard manager
published = PublishedManager()
unpublished = UnpublishedManager()
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("content", "due_date")
class ArticleRUDView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Article.objects.all()
In this code, ArticleRUDView naturally responds with all Article because of Article.objects.all(), yet this is not what I want to do. What I want to do is:
If the user is authenticated, then Article.objects.all().
If the user is anonymous,
If the entry is published (which means its due_date is less than now), then serialize all fields.
If the entry is not published (which means its due_date is greater than now), then still serialize, but content should be null in JSON.
Or, in short, how do I alter the serializer's data in a view?
Troubleshooting
This section might get updated in time. I will elaborate on what I find.
Overriding get_serializer Method from GenericAPIView
So I've found out I can get an instance of ArticleSerializer. So I did below:
def get_serializer(self, *args, **kwargs):
serializer = super().get_serializer()
if self.request.user.is_authenticated:
return serializer
obj = self.get_object() # get_object, hence the name, gets the object
due_date = obj.due_date
now = timezone.now()
if due_date > now:
serializer.data["content"] = None
return serializer
However, my tests didn't go well at all. This, oddly, returns an empty string on content field in JSON. I've tried different things but got that empty string. I do not have any single clue about what to do from here.
Environment
Python 3.7.4
Django 2.2.7
Django Rest Framework 3.10.3
I think you want to use get_serializer_class as opposed to get_serializer. You can allow the serializer class to choose what to stick in content instead of all the mucking around with managers, since you want to serialize all objects anyway. Something like this should work:
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("content", "due_date")
class AnonymousArticleSerializer(ArticleSerializer):
content = serializers.SerializerMethodField()
#staticmethod
def get_content(obj):
if obj.due_date > timezone.now():
return None
return obj.content
class ArticleRUDView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Article.objects.all()
def get_serializer_class(self):
if self.serializer_class:
return self.serializer_class
if self.request.user.is_authenticated:
self.serializer_class = ArticleSerializer
else:
self.serializer_class = AnonymousArticleSerializer
return self.serializer_class
One thing I don't like about this solution is that if you have a more complicated serializer field you're overwriting, you'd have to put the logic somewhere, but in this case (context being a text field) it's pretty simple.
I am using Django Rest Framework as a backend for an app.
I have a User that has one Wallet. Then I have Item. If a User wants an Item it creates an instance in his/her Wallet called WalletItem. All works well.
Now I want to limit the number of items for the User using an attribute limit_usage.
First, I added a check to post method adding new instance that checks the number of item instances in User's Wallet. So the user gets 403 when trying to add third WalletItem if limit_usage == 2 for this Item.
I would like to override a get_queryset() method or queryset in list()/retrieve() methods so that if anonymous user calls /items/ there are unfiltered items in response. However if the user is authenticated I would like to filter only those Items that s/he is allowed to put in the Wallet, i.e. those that have not exceeded limit_usage for current user.
class Wallet(models.Model):
user = models.OneToOneField('auth.User', related_name='wallet')
class Item(models.Model):
valid_from = models.DateTimeField()
valid_to = models.DateTimeField()
limit_usage = models.PositiveSmallIntegerField(default=0)
class WalletItem(models.Model):
wallet = models.ForeignKey('Wallet', related_name='%(class)ss')
offer = models.ForeignKey('Item', related_name='offer')
class ItemViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Item.objects.all().order_by('-created_at')
serializer_class = ItemSerializer
def list(self, request, *args, **kwargs):
time_now = now()
self.queryset = self.queryset.filter(
valid_from__lte=time_now,
valid_to__gte=time_now,
)
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)
I created a method of Item class that should have helped me but I realized I cannot use it in the queryset:
def is_visible_for_user(self, user=None):
if not self.limit_usage or not user:
return True
ct = WalletItem.objects.filter(item=self, wallet=user.wallet).count()
return self.limit_usage > ct
So I can iterate through the queryset to see if each item can be visible for the user, however I cannot construct the queryset out of this filtered list.
I found something similar here on SO: Django REST Framework : filtering with another table but the response did not help me.
You firstly need to check if user is authenticated, it not, then return every Item. Then to filter out Item objects if corresponding WalletItem objects exceeded its limit.
from django.db.models import Count, F, Sum
...
class ItemViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
queryset = super().get_queryset()
user = self.request.user
if user.is_anonymous:
return queryset
queryset = queryset.annotate(user_wallet_items=Sum(
Case(
When(walletitem__wallet_id=user.wallet_id, then=1),
default=0, output_field=IntegerField()
)) \
.filter(user_wallet_items__lte=F('limit_usage'))
return queryset
I suggest you to move your filtration based on current time to the same get_queryset() method, since it's belong there.
Note: i've not tested this approach.
I'm have a Django model that serves as a request description. It is created to issue a request by a REST client, serves to record the tasks current status, and record historical requests received by clients.
This model has a few fields that are used to fine-tune and control the requested task (say, a target object and the type of action). Obviously, I'd like the client to control those fields on object creation but not afterwards (you can't change the object once the task started running).
I was hoping for something similar to serializers.ReadOnlyField, so I could have something similar to this:
class TaskSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
task_id = serializers.ReadOnlyField()
target_object = serializers.CreateOnlyField()
but couldn't find it in the documentation or google.
Just to expand on Wim's answer, this is a way to select a different serialiser based on the incoming request method:
class RequestViewSet(viewsets.ModelViewSet):
serializer_class = RequestModelSerializer
model = Request
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'POST':
serializer_class = SerializerWithoutCertainFields
return serializer_class
The answer of #fabio.sussetto put me on the right track. I think my answer is slightly prettier; I don't specify the serializer on the class directly but only in get_serializer_class(). Also, I do not switch it based on the HTTP type (i.e. POST) but rather on the action, update, which I think is more declarative.
class RequestViewSet(viewsets.ModelViewSet):
model = Request
def get_serializer_class(self):
if self.action == 'update':
return serializer_class = SerializerWithoutCertainFields
return RequestModelSerializer
This can be achieved with one serializer by using to_internal_value method
class TaskSerializer(serializers.ModelSerializer):
# Field settings here
def to_internal_value(self, data):
data = super().to_internal_value(data)
# Remove target_object if serializer is updating object
if self.instance:
data.pop('target_object', None)
return data
class Meta:
model = Task
fields = ('owner', 'task_id', 'target_object')
could also be done with a combination of required=False and dropping the field value when updating like in this example:
class SectionSerializer(serializers.ModelSerializer):
# do not require field lesson when updating
lesson = serializers.PrimaryKeyRelatedField(queryset=Lesson.objects.all(), required=False)
# do not allow changing the lesson field
def update(self, instance, validated_data):
validated_data.pop("lesson", None)
return super().update(instance, validated_data)
When fields need to be filled programmatically in Django Rest Framework, the pre_save method may be overridden in the APIView, and the needed fields can be populated there, like:
def pre_save(self, obj):
obj.owner = self.request.user
This works great for flat objects, but in case of nested situations, the nested object cannot be accessed in the pre_save method. The only solution I found so far is to override the save_object method, and check if the object is an instance of the nested class, and if so, populate that field there. Although this works, I don't like the solution, and would like to know if anyone found a better way?
Demonstrating the situation:
class Notebook(models.Model):
owner = models.ForeignKey(User)
class Note(models.Model):
owner = models.ForeignKey(User)
notebook = models.ForeignKey(Notebook)
note = models.TextField()
class NoteSerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.username')
class Meta:
model = Note
fields = ('note', 'owner')
class NotebookSerializer(serializers.ModelSerializer):
notes = NoteSerializer(many=True)
owner = serializers.Field(source='owner.username')
class Meta:
model = Notebook
fields = ('notes', 'owner')
def save_object(self, obj, **kwargs):
if isinstance(obj, Note):
obj.owner = obj.notebook.owner
return super(NotebookSerializer, self).save_object(obj, **kwargs)
class NotebookCreateAPIView(CreateAPIView):
model = Notebook
permission_classes = (IsAuthenticated,)
serializer_class = NotebookSerializer
def pre_save(self, obj):
obj.owner = self.request.user
Before asking why don't I use different endpoints for creating notebooks and notes separately, let me say that I do that, but I also need a functionality to provide initial notes on creation of the notebook, so that's why I need this kind of endpoint as well.
Also, before I figured out this hackish solution, I actually expected that I will have to override the save_object method of the NoteSerializer class itself, but it turned out in case of nested objects, it won't even be called, only the root object's save_objects method, for all the nested objects, but I guess it was a design decision.
So once again, is this solvable in a more idiomatic way?
You can access the request in your serializer context.
So my approach to this would be:
class NoteSerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.username')
def restore_object(self, attrs, instance=None):
instance = super(NoteSerializer, self).restore_object(attrs, instance)
instance.owner = self.context['request'].user
return instance
class Meta:
model = Note
fields = ('note', 'owner')
And the same on the NotebookSerializer.
The Serializer context will be made available to all used serializers in the ViewSet.