I am new to Django. Reading a lot of ways to do the same thing--but not finding the proverbial needle in a haystack. One such needle is a simple "Find or Create" pattern for Django Rest.
I am trying to find a simple example of how to go about implementing a find or create pattern for one of my model data using Django Rest ModelSerializer and CreateAPIView methods. Let say that I have a model Location with a unique field 'address'. I want to return an existing instance when the address already exists on my database. If the address does not exist, I want to create an entry in the database and populate other computed values for the object.
class Location(models.Model):
address = models.CharField(max_length=100, unique=True,)
thing1 = models.CharField(max_length=100, blank=True, null=True, )
thing2 = models.CharField(max_length=100, blank=True, null=True, )
def compute_things(self, address):
somevalue1 = ...
somevalue2 = ....
return somevalue1, somevalue2
Now, I am not exactly sure how to write the serializer and view so that:
A new location is created and returned with all the fields
initialized when a new address is seen for the first time
An existing location that matches 'address' in the database is
returned in lieu of step 1
What else should I define for the model? How do I write APIView and CreateSerializer to get the right thing? Where should I call the compute_thing() in order to populate the missing fields.
For the serializer:
class LocationCreateSerializer(ModelSerializer):
class Meta:
model = Location
And for the APIView:
class LocationCreateAPIView(CreateAPIView):
serializer_class = LocationCreateSerializer
queryset = Location.objects.all()
The APIView and Serializer above are not enough for what I need. What else do I need to add to model, View and Serializer to get that behavior that I am seeking?
I don't want the View nor the Serializer to return validation errors for duplicate 'addresses'--just the existing instance and not an error. It looks like restore_object() is deprecated. Is there a way to accomplish what I am seeking?
You missed one thing, that is,
fields =("Here will be your models fields. That you want to serialize.")
That is after the model = Location in serializer.
And you can follow official doc of Django-REST-Framework
Ok, I figured out the answer to my own question. I am not sure this is the best solution; however, for anyone that needs a solution, here is what I ended up doing:
class LocationCreateAPIView(CreateAPIView):
serializer_class = LocationCreateSerializer
queryset = Location.objects.all()
def post(self, request, format=None):
address = None
if 'address' in self.request.data:
address = self.request.data['address']
else:
return Response(status=HTTP_400_BAD_REQUEST)
try:
location = Location.objects.get(address=address)
serializer = self.get_serializer(location)
return Response(serializer.data, status=HTTP_200_OK)
except Location.DoesNotExist:
pass
serializer = LocationCreateSerializer(data=self.request.data)
if serializer.is_valid():
somevalue1, somevalue2 = Location.compute_things(self, address=address)
if (not somevalue1) | (not somevalue2):
return Response(status=HTTP_400_BAD_REQUEST)
serializer.save(address=address, thing1=somevalue1, thing2=somevalue2)
return Response(serializer.data, status=HTTP_201_CREATED)
return Response(status=HTTP_400_BAD_REQUEST)
If you have a better solution, please post it. I'd like to continue learning.
Related
my models.py is :
class clients(models.Model):
client_id = models.IntegerField(unique=True, primary_key=True )
'
'
money = models.IntegerField(default = 0)
class transfermoney(models.Model):
first_client_id = models.IntegerField()
second_client_id = models.IntegerField()
amountofmoney = models.PositiveIntegerField()
time = models.TimeField(auto_now=True)
date = models.DateField(auto_now=True)
my serializers.py is :
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = clients
fields = ('__all__')
class moneytransfer(serializers.ModelSerializer):
class Meta:
model = transfermoney
fields = ('__all__')
my views.py is :
class transferingmoney(APIView):
def post(self,request):
serializer = moneytransfer(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)
i'm using django rest framework , and what i want is ,everytime when i make a post request into "transfermoney" model , it take the "first_client_id" and search for it into the "client_id" in the "clients" model and add the "amountofmoney" from "transfermoney" model to the "money" field in "clients" model , and the same for the "second_client_id"
please how can i do that ?
Ok, the question of how to do it completely correct is too big to discuss it here.
I will only answer your question.
But the thing I need to mention - store money amounts in Decimal fields, not in Integer - that's just bad practice. Something like:
models.DecimalField(max_digits=6, decimal_places=2)
will do. Now, to the subject of the question.
First of all, I suggest you add custom 'validate()' method in your moneytransfer serializer and add checks on the client_ids there. You are using integer fields (for some reason I guess) in your transfermoney model instead of Foreign Keys. Because of that, the validate method of the serializeк won't check for you if these ids actually exist, so it's better to add some validation. Then it will be called in your View and return False if your custom checks will fail. If I understood it correctly you want something like:
class BillingRecordSerializer(serializers.ModelSerializer):
def validate(self, data):
try:
clients.objects.get(client_id = data['first_client_id'])
clients.objects.get(client_id = data['second_client_id'])
except clients.DoesNotExist:
raise serializers.ValidationError("One of the clients does not exist")
return data
class Meta:
fields = ('__all__')
model = transfermoney
With this, you will be sure that both the clients are present in your DB.
Then, you can do something like:
class transferingmoney(APIView):
def post(self,request):
data=request.data
serializer = moneytransfer(data=data)
if serializer.is_valid():
serializer.save()
client_1 = clients.objects.get(client_id=data['first_client_id'])
client_2 = clients.objects.get(client_id=data['second_client_id'])
client_1.money -= data['amountofmoney']
client_2.money += data['amountofmoney']
client_1.save()
client_2.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
You can also create a separate method for the logic I have added in the view to make code clear and reusable. Personally i'd prefer to override save() method of transfermoney model to do it there. And then it will be automatically called on the first save() of this model. But it all depends on what place you will be more comfortable with and what is the logic behind your code.
And I want to mention this is obviously not the right way to deal with money transactions, but describing the right way is way over the topic of the question and also there are a lot of examples out there.
P.S. I wrote the code right here, so there could иу some typos in it.
I have 2 models:
class Tag(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Question(models.Model):
name = models.CharField(max_length=255)
Tag_name = models.ManyToManyField(Tag)
def __str__(self):
return self.name
views.py
class QuestionList(APIView):
def get(self, request, tag_id):
res = Question.objects.filter(Tag_name=tag_id).prefetch_related('Tag_name').order_by('name')[:10]
print(res)
serializer = QuestionSerializers(res, many=True)
data = {}
return Response(serializer.data)
# return Response(data)
urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('tag=<int:tag_id>/', views.QuestionList.as_view()) //this needs to be edited
]
what will be the path in url.py file to send id and name parameter and fetch data like
http://127.0.0.1:8000?tag=4&order_by=name
so i get all questions with tag 4 and order by name ?
The query string [wiki] is not part of the path. These parameters can be obtained in the request.GET object, which is a dictionary-like object.
Your path thus should look like:
path('/', views.QuestionListView.as_view()),
In your QuestionListView, you can then filter on these parameters:
class QuestionListView(ListAPIView):
model = Question
serializers = QuestionSerializers
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
if 'tag' in self.request.GET:
queryset = queryset.filter(
Tag_name=self.request.GET['tag']
)
if 'order_by' in self.request.GET:
queryset = queryset.order_by(self.request.GET['order_by'])
return queryset
# …
That being said, the above will need extra scaffolding. Here you allow users to "inject" items in the .order_by(..). A hacker could exploit that, for example by ordering elements on related data, and thus binary searching on certain fields.
It might be worth taking a look at django-filter [GitHub], where you can define based on what elements you can filter, etc. It will furthermore encapsulate the filtering, and thus make it convenient to work with this in different views.
Note: normally the name of the fields in a Django model are written in snake_case, not PerlCase, so it should be: tags instead of Tag_name. This because a ManyToManyField refers to zero, one or more tags, and furthermore it refers to tag objects, not the name of the tags.
Note: Instead of implementing a view from scratch, it might be worth taking a look at
the ListAPIView class [drf-doc]
that can already implement a lot of boilerplate code.
If you want to send multiple Url parameters with Django using this Url:
http://127.0.0.1:8000?tag=4&order_by=name
using the path in urls.py try this:
path('tag=<int:tag_id>/order_by=<str:name>', views.QuestionList.as_view())
you have a good example here, Django docs or find my blog with articles about Django.
I'm novice in python and django rest. But I'm confused. What is the best way to update many to many relation in django rest framework.
I read the docs
http://www.django-rest-framework.org/api-guide/relations/#manytomanyfields-with-a-through-model
By default, relational fields that target a ManyToManyField with a through model specified are set to read-only.
If you explicitly specify a relational field pointing to a ManyToManyField with a through model, be sure to set read_only to True.
So if I have a code
class Master(models.Model):
# other fields
skills = models.ManyToManyField(Skill)
class MasterSerializer(serializers.ModelSerializer):
skills = SkillSerializer(many=True, read_only=False)
This will return skills as list of objects. And I don't have a way to update them. As far as I understood Django prefers work with objects vs object id when it comes to M2M. If I work with yii or rails I will work with "through" models. I would like to get skill_ids field. That I could read and write. And I can do this for write operation
class MasterSerializer(serializers.ModelSerializer):
skill_ids = serializers.ListField(write_only=True)
def update(self, instance, validated_data):
# ...
validated_data['skill_ids'] = filter(None, validated_data['skill_ids'])
for skill_id in validated_data['skill_ids']:
skill = Skill.objects.get(pk=skill_id)
instance.skills.add(skill)
return instance
But I cannot make it return skill_ids in field. And work for read and write operations.
A few things to note.
First, you don't have an explicit through table in your example. Therefore you can skip that part.
Second, you are trying to use nested serializers which are far more complex than what you're trying to achieve.
You can simply read/write related id by using a PrimaryKeyRelatedField:
class MasterSerializer(serializers.ModelSerializer):
skills_ids = serializers.PrimaryKeyRelatedField(many=True, read_only=False, queryset=Skill.objects.all(), source='skills')
Which should be able to read/write:
{id: 123, first_name: "John", "skill_ids": [1, 2, 3]}
Note that the mapping from JSON's "skill_ids" to model's "skills" is done by using the optional argument source
I will try to bring some light in terms of design: in Django if you specify the model for a ManyToManyRelation, then the relation field on the model becomes read-only. If you need to alter the associations you do it directly on the through model, by deleting or registering new records.
This means that you may need to use a completely different serializer for the through model, or to write custom update/create methods.
There are some sets back with custom through model, are you sure you're not good enough with the default implementation of ManyToManyFields ?
tl;dr:
For a much simpler, one-liner solution for M2M, I sussed out a solution of the form:
serializer = ServiceSerializer(instance=inst, data={'name':'updated', 'countries': [1,3]}, partial=True)
if serializer.is_valid():
serializer.save()
For a more complete example, I have included the following:
models.py
from django.db import models
class Country(models.Model):
name = models.CharField(max_length=50, null=False, blank=False)
class Service(models.Model):
name = models.CharField(max_length=20, null=True)
countries = models.ManyToManyField('Country')
serializers.py
from rest_framework import serializers
from .models import *
class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = ('name',)
class ServiceSerializer(serializers.ModelSerializer):
class Meta:
model = Service
fields = ('name', 'countries',)
Make sure some dummy service and country instances are created for testing. Then you can update an instance in a function like so:
Update example
# get an object instance by key:
inst = ServiceOffering.objects.get(pk=1)
# Pass the object instance to the serializer and a dictionary
# Stating the fields and values to update. The key here is
# Passing an instance object and the 'partial' argument:
serializer = ServiceSerializer(instance=inst, data={'name':'updated', 'countries': [1,3]}, partial=True)
# validate the serializer and save
if serializer.is_valid():
serializer.save()
return 'Saved successfully!'
else:
print("serializer not valid")
print(serializer.errors)
print(serializer.data)
return "Save failed"
If you inspect the relevant tables, the updates are carried through including to the M2M bridging table.
To extend this example, we could create an object instance in a very similar way:
### Create a new instance example:
# get the potential drop down options:
countries = ['Germany', 'France']
# get the primary keys of the objects:
countries = list(Country.objects.filter(name__in=countries).values_list('pk', flat=True))
# put in to a dictionary and serialize:
data = {'countries': countries, 'name': 'hello-world'}
serializer = ServiceOfferingSerializer(data=data)
I have dealt with this issue for quite some time and I have found that the best way to solve the general problem of updating any many to many field is by working around it.
In my case there is a model called Listing and a user can make a Subscription(the other model) to an instance of the Listing model. The Subscription works with a Generic Foreign Key and the Listing imports the Subscriptions of the users via Many2Many.
Instead of making a PUT request to the Listing Model via API, I simply add the Subscription instance to the right model in the POST Method of the API View of Subscription. Here is my adjusted code:
#Model
class Listing(models.Model):
#Basics
user = models.ForeignKey(settings.AUTH_USER_MODEL)
slug = models.SlugField(unique=True, blank=True)
timestamp = models.DateTimeField(auto_now_add=True, auto_now=False)
#Listing
title = models.CharField(max_length=200)
price = models.CharField(max_length=50, null=True, blank=True)
subscriptions = models.ManyToManyField(Subscription, blank=True)
class Subscription(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
timestamp = models.DateTimeField(auto_now_add=True)
#Views
class APISubscriptionCreateView(APIView): #Retrieve Detail
def post(self, request, format=None):
serializer = SubscriptionCreateSerializer(data=request.data)
if serializer.is_valid():
sub = serializer.save(user=self.request.user)
object_id = request.data['object_id']
lis = Listing.objects.get(pk=object_id)
lis.subscriptions.add(sub)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I hope this will help, it took me a while to figure this out
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)
I am generating aggregates for each item in a QuerySet:
def get_queryset(self):
from django.db.models import Count
queryset = Book.objects.annotate(Count('authors'))
return queryset
But I am not getting the count in the JSON response.
thank you in advance.
The accepted solution will hit the database as many times as results are returned. For each result, a count query to the database will be made.
The question is about adding annotations to the serializer, which is way more effective than doing a count query for each item in the response.
A solution for that:
models.py
class Author(models.Model):
name = models.CharField(...)
other_stuff = models...
...
class Book(models.Model):
author = models.ForeignKey(Author)
title = models.CharField(...)
publication_year = models...
...
serializers.py
class BookSerializer(serializers.ModelSerializer):
authors = serializers.IntegerField()
class Meta:
model = Book
fields = ('id', 'title', 'authors')
views.py
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.annotate(authors=Count('author'))
serializer_class = BookSerializer
...
That will make the counting at database level, avoiding to hit database to retrieve authors count for each one of the returned Book items.
The queryset returned from get_queryset provides the list of things that will go through the serializer, which controls how the objects will be represented. Try adding an additional field in your Book serializer, like:
author_count = serializers.IntegerField(
source='author_set.count',
read_only=True
)
Edit: As others have stated, this is not the most efficient way to add counts for cases where many results are returned, as it will hit the database for each instance. See the answer by #José for a more efficient solution.
Fiver's solution will hit the db for every instance in the queryset so if you have a large queryset, his solution will create a lot of queries.
I would override the to_representation of your Book serializer, it reuses the result from the annotation. It will look something like:
class BookSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
return {'id': instance.pk, 'num_authors': instance.authors__count}
class Meta:
model = Book
So, if you make an annotation like
Model.objects.annotate(
some_new_col=Case(
When(some_field=some_value, then=Value(something)),
# etc...
default=Value(something_default),
output_field=SomeTypeOfField(),
)
).filter()#etccc
and the interpreter throws in an error that something is not a model field for the related serializer, there is a workaround. It's not nice but if you add a method some_new_col, it recognizes the value from the query above.
The following will do just fine.
def some_new_col(self):
pass;