Django Rest update many to many by id

2020-02-05 02:59发布

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.

4条回答
女痞
2楼-- · 2020-02-05 03:15

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

查看更多
ゆ 、 Hurt°
3楼-- · 2020-02-05 03:22

A few things to note.

First, you don't an explicit through table in your example. Therefore you can skip that part.

Second is 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

查看更多
▲ chillily
4楼-- · 2020-02-05 03:22

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 ?

查看更多
beautiful°
5楼-- · 2020-02-05 03:31

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)
查看更多
登录 后发表回答