Mutating Data¶
Django RESTQL got your back on creating and updating nested data too, it has two components for mutating nested data, NestedModelSerializer
and NestedField
. A serializer NestedModelSerializer
has update
and create
logics for nested fields on the other hand NestedField
is used to validate data before calling update
or create
method.
Using NestedField and NestedModelSerializer¶
Just like in querying data, mutating nested data with Django RESTQL is very simple, you just have to inherit NestedModelSerializer
on a serializer with nested fields and use NestedField
to define those nested fields which you want to be able to mutate. Below is an example which shows how to use NestedModelSerializer
and NestedField
.
from rest_framework import serializers
from django_restql.serializers import NestedModelSerializer
from django_restql.fields import NestedField
from app.models import Location, Amenity, Property
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ["id", "city", "country"]
class AmenitySerializer(serializers.ModelSerializer):
class Meta:
model = Amenity
fields = ["id", "name"]
# Inherit NestedModelSerializer to support create and update
# on nested fields
class PropertySerializer(NestedModelSerializer):
# Define location as nested field
location = NestedField(LocationSerializer)
# Define amenities as nested field
amenities = NestedField(AmenitySerializer, many=True)
class Meta:
model = Property
fields = [
'id', 'price', 'location', 'amenities'
]
With serializers defined as shown above, you will be able to send data mutation request like
POST /api/property/
With a request body like
{
"price": 60000,
"location": {
"city": "Newyork",
"country": "USA"
},
"amenities": {
"add": [3],
"create": [
{"name": "Watererr"},
{"name": "Electricity"}
]
}
}
And get a response as
{
"id": 2,
"price": 60000,
"location": {
"id": 3,
"city": "Newyork",
"country": "USA"
},
"amenities": [
{"id": 1, "name": "Watererr"},
{"id": 2, "name": "Electricity"},
{"id": 3, "name": "Swimming Pool"}
]
}
Just to clarify what happed here:
- location has been created and associated with the property created
create
operation has created amenities with values specified in a list and associate them with the propertyadd
operation has added amenity with id=3 to a list of amenities of the property.
Note
POST for many related fields supports two operations which are create
and add
.
Below we have an example where we are trying to update the property we have created in the previous example.
PUT/PATCH /api/property/2/
Request Body
{
"price": 50000,
"location": {
"city": "Newyork",
"country": "USA"
},
"amenities": {
"add": [4],
"create": [{"name": "Fance"}],
"remove": [3],
"update": {1: {"name": "Water"}}
}
}
After sending the requst above we'll get a response which looks like
{
"id": 2,
"price": 50000,
"location": {
"id": 3,
"city": "Newyork",
"country": "USA"
},
"amenities": [
{"id": 1, "name": "Water"},
{"id": 2, "name": "Electricity"},
{"id": 4, "name": "Bathtub"},
{"id": 5, "name": "Fance"}
]
}
From the request body add
, create
, remove
and update
are operations
What you see in the response above are details of our property, what really happened after sending the update request is
add
operation added amenitiy with id=4 to a list of amenities of the propertycreate
operation created amenities with values specified in a listremove
operation removed amenities with id=3 from a propertyupdate
operation updated amenity with id=1 according to values specified.
Note
PUT/PATCH for many related fields supports four operations which are create
, add
, remove
and update
.
Self referencing nested field¶
Currently DRF doesn't allow declaring self referencing nested fields but you might have a self referencing nested field in your project since Django allows creating them. Django RESTQL comes with a nice way to deal with this scenario.
Let's assume we have a student model as shows below
# models.py
class Student(models.Model):
name = models.CharField(max_length=50)
age = models.IntegerField()
study_partners = models.ManyToManyField('self', related_name='study_partners')
As you can see from the model above study_partners
is a self referencing field. Below is the corresponding serializer for our model
# serializers.py
class StudentSerializer(NestedModelSerializer):
# Define study_partners as self referencing nested field
study_partners = NestedField(
'self',
many=True,
required=False,
exclude=['study_partners']
)
class Meta:
model = Student
fields = ['id', 'name', 'age', 'study_partners']
You can see that we have passed self
to NestedField
just like in Student
model, this means that study_partners
field is a self referencing field.
The other important thing here is exclude=['study_partners']
, this excludes the field study_partners
on a nested field to avoid recursion error if the self reference is cyclic.
NestedField kwargs¶
NestedField
accepts extra kwargs in addition to those accepted by a serializer, these extra kwargs can be used to do more customizations on a nested field as explained below.
accept_pk kwarg¶
accept_pk=True
is used if you want to be able to update nested field by using pk/id of existing data(basically associate existing nested resource with the parent resource). This applies to foreign key relations only. The default value for accept_pk
is False
.
Below is an example showing how to use accept_pk
kwarg.
from rest_framework import serializers
from django_restql.fields import NestedField
from django_restql.serializers import NestedModelSerializer
from app.models import Location, Property
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ["id", "city", "country"]
class PropertySerializer(NestedModelSerializer):
# pk based nested field
location = NestedField(LocationSerializer, accept_pk=True)
class Meta:
model = Property
fields = [
'id', 'price', 'location'
]
Now sending mutation request as
POST /api/property/
Request Body
{
"price": 40000,
"location": 2
}
Note
Here location resource with id=2 exists already, so what's done here is create a new property resource and associate it with this location whose id is 2.
Response
{
"id": 1,
"price": 40000,
"location": {
"id": 2,
"city": "Tokyo",
"country": "China"
}
}
Using accept_pk
doesn't limit you from sending data(instead of pk to nested resource), setting accept_pk=True
means you can send both data and pks. For instance from the above example you could still do
POST /api/property/
Request Body
{
"price": 63000,
"location": {
"city": "Dodoma",
"country": "Tanzania"
}
}
Response
{
"id": 2,
"price": 63000,
"location": {
"id": 3,
"city": "Dodoma",
"country": "Tanzania"
}
}
accept_pk_only kwarg¶
accept_pk_only=True
is used if you want to be able to update nested field by using pk/id only. This applies to foreign key relations only as well. The default value for accept_pk_only
kwarg is False
, if accept_pk_only=True
is set you won't be able to send data to create a nested resource.
Below is an example showing how to use accept_pk_only
kwarg.
from rest_framework import serializers
from django_restql.fields import NestedField
from django_restql.serializers import NestedModelSerializer
from app.models import Location, Property
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ["id", "city", "country"]
class PropertySerializer(NestedModelSerializer):
# pk based nested field
location = NestedField(LocationSerializer, accept_pk_only=True)
class Meta:
model = Property
fields = [
'id', 'price', 'location'
]
Sending mutation request
POST /api/property/
Request Body
{
"price": 40000,
"location": 2 // You can't send data in here, you can only send pk/id
}
Response
{
"id": 1,
"price": 40000,
"location": {
"id": 2,
"city": "Tokyo",
"country": "China"
}
}
Note
By default accept_pk=False
and accept_pk_only=False
, so nested field(foreign key related) accepts data only by default, if accept_pk=True
is set, it accepts data and pk/id, and if accept_pk_only=True
is set it accepts pk/id only. You can't set both accept_pk=True
and accept_pk_only=True
.
create_ops and update_ops kwargs.¶
These two kwargs are used to restrict some operations when creating or updating nested data. Below is an example showing how to restrict some operations by using these two kwargs.
from rest_framework import serializers
from django_restql.fields import NestedField
from django_restql.serializers import NestedModelSerializer
from app.models import Location, Amenity, Property
class AmenitySerializer(serializers.ModelSerializer):
class Meta:
model = Amenity
fields = ["id", "name"]
class PropertySerializer(NestedModelSerializer):
amenities = NestedField(
AmenitySerializer,
many=True,
create_ops=["add"], # Allow only add operation
update_ops=["add", "remove"] # Allow only add and remove operations
)
class Meta:
model = Property
fields = [
'id', 'price', 'amenities'
]
Sending create mutation request
POST /api/property/
Request Body
{
"price": 60000,
"amenities": {
"add": [1, 2]
}
}
Note
Since create_ops=["add"]
, you can't use create
operation in here!.
Response
{
"id": 2,
"price": 60000,
"amenities": [
{"id": 1, "name": "Watererr"},
{"id": 2, "name": "Electricity"}
]
}
Sending update mutation request
PUT/PATCH /api/property/2/
Request Body
{
"price": 50000,
"amenities": {
"add": [3],
"remove": [2]
}
}
Note
Since update_ops=["add", "remove"]
, you can't use create
or update
operation in here!.
Response
{
"id": 2,
"price": 50000,
"amenities": [
{"id": 1, "name": "Water"},
{"id": 3, "name": "Bathtub"}
]
}
allow_remove_all kwarg¶
This kwarg is used to enable and disable removing all related objects on many related nested field at once by using __all__
directive. The default value of allow_remove_all
is False
, which means removing all related objects on many related nested fields is disabled by default so if you want to enable it you must set its value to True
. For example
class CourseSerializer(NestedModelSerializer):
books = NestedField(BookSerializer, many=True, allow_remove_all=True)
class Meta:
model = Course
fields = ["name", "code", "books"]
With allow_remove_all=True
as set above you will be able to send a request like
PUT/PATCH /courses/3/
Request Body
{
"books": {
"remove": "__all__"
}
}
This will remove all books associated with a course being updated.
Using DynamicFieldsMixin and NestedField together¶
You can use DynamicFieldsMixin
and NestedModelSerializer
together if you want your serializer to be writable(on nested fields) and support querying data, this is very common. Below is an example which shows how you can use DynamicFieldsMixin
and NestedField
together.
from rest_framework import serializers
from django_restql.fields import NestedField
from django_restql.mixins import DynamicFieldsMixin
from django_restql.serializers import NestedModelSerializer
from app.models import Location, Property
class LocationSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
class Meta:
model = Location
fields = ["id", "city", "country"]
# Inherit both DynamicFieldsMixin and NestedModelSerializer
class PropertySerializer(DynamicFieldsMixin, NestedModelSerializer):
location = NestedField(LocationSerializer)
class Meta:
model = Property
fields = [
'id', 'price', 'location'
]
NestedField
is nothing but a serializer wrapper, it returns an instance of a modified version of a serializer passed, so you can pass all the args and kwargs accepted by a serializer on it, it will simply pass them along to a serializer passed when instantiating an instance. So you can pass anything accepted by a serializer to a NestedField
wrapper, and if a serializer passed inherits DynamicFieldsMixin
just like LocationSerializer
on the example above then you can pass any arg or kwarg accepted by DynamicFieldsMixin
when defining location as a nested field, i.e
location = NestedField(LocationSerializer, fields=[...])
location = NestedField(LocationSerializer, exclude=[...])
location = NestedField(LocationSerializer, return_pk=True)
Note
If you want to use required=False
kwarg on NestedField
you might want to include allow_null=True
too if you want your nested field to be set to null
if you haven't supplied it. For example
from rest_framework import serializers
from django_restql.fields import NestedField
from django_restql.mixins import DynamicFieldsMixin
from django_restql.serializers import NestedModelSerializer
from app.models import Location, Property
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = ["id", "city", "country"]
class PropertySerializer(NestedModelSerializer):
# Passing both `required=False` and `allow_null=True`
location = NestedField(LocationSerializer, required=False, allow_null=True)
class Meta:
model = Property
fields = [
'id', 'price', 'location'
]
The required=False
kwarg allows you to create Property without including location
field and the allow_null=True
kwarg allows location
field to be set to null if you haven't supplied it. For example
Sending mutation request
POST /api/property/
Request Body
{
"price": 40000
// You can see that the location is not included here
}
Response
{
"id": 2,
"price": 50000,
"location": null // This is the result of not including location
}
If you use required=False
only without allow_null=True
, The serializer will allow you to create Property without including location
field but it will throw error because by default allow_null=False
which means null
/None
(which is what's passed when you don't supply location
value) is not considered a valid value.
Working with data mutation without request¶
Django RESTQL allows you to do data mutation without having request object, this is used if you don't want to get your mutation data input(serializer data) from a request, in fact NestedModelSerializer
and NestedFied
can work independently without using request. Below is an example showing how you can work with data mutation without request object.
from rest_framework import serializers
from django_restql.fields import NestedField
from django_restql.mixins import DynamicFieldsMixin
from django_restql.serializers import NestedModelSerializer
from app.models import Book, Course
class BookSerializer(DynamicFieldsMixin, NestedModelSerializer):
class Meta:
model = Book
fields = ['id', 'title', 'author']
class CourseSerializer(DynamicFieldsMixin, NestedModelSerializer):
books = NestedField(BookSerializer, many=True, required=False)
class Meta:
model = Course
fields = ['id', 'name', 'code', 'books']
From serializers above you can create a course like
data = {
"name": "Computer Programming",
"code": "CS50",
"books": {
"add": [1, 2],
"create": [
{'title': 'Basic Data Structures', 'author': 'J. Davis'},
{'title': 'Advanced Data Structures', 'author': 'S. Mobit'}
]
}
}
serializer = CourseSerializer(data=data)
serializer.is_valid()
serializer.save()
print(serializer.data)
# This will print
{
"id": 2,
"name": "Computer Programming",
"code": "CS50",
"books": [
{'id': 1, 'title': 'Programming Intro', 'author': 'K. Moses'},
{'id': 2, 'title': 'Understanding Computers', 'author': 'B. Gibson'},
{'id': 3, 'title': 'Basic Data Structures', 'author': 'J. Davis'},
{'id': 4, 'title': 'Advanced Data Structures', 'author': 'S. Mobit'}
]
}
To update a created course you can do it like
data = {
"code": "CS100",
"books": {
"remove": [2, 3]
}
}
course_obj = Course.objects.get(pk=2)
serializer = CourseSerializer(course_obj, data=data)
serializer.is_valid()
serializer.save()
print(serializer.data)
# This will print
{
"id": 2,
"name": "Computer Programming",
"code": "CS100",
"books": [
{'id': 1, 'title': 'Programming Intro', 'author': 'K. Moses'},
{'id': 2, 'title': 'Understanding Computers', 'author': 'B. Gibson'}
]
}