Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In the Django Rest Framework, how do you add ManyToMany related objects?

Here's my code:

Models

class Recipe(models.Model):
  name = models.CharField(max_length=50, unique=True)
  ingredient = models.ManyToManyField(Ingredient)

class Ingredient(models.Model):
  name = models.CharField(max_length=50, unique=True)

View

class RecipeDetailAPIView(RetrieveUpdateDestroyAPIView):
  permission_classes = (IsAdminOrReadOnly,)
  serializer_class = RecipeSerializer
  queryset = Recipe.objects.all()

  def put(self, request, *args, **kwargs):
    return self.update(request, *args, **kwargs)

  def perform_update(self, serializer):
    serializer.save(updated_by_user=self.request.user)

Serializers

class IngredientSerializer(serializers.ModelSerializer):

    class Meta:
        model = Ingredient
        fields = [
      'id',
            'name',
        ]

class RecipeSerializer(serializers.ModelSerializer):
  ingredient = IngredientSerializer(many=True, read_only=False)

  class Meta:
    model = Recipe
    fields = [
      'id',
      'name',
      'ingredient',
    ]

I'm starting with the following Recipe object:

{
    "id": 91
    "name": "Potato Salad"
    "ingredient": [
        {
            "id": 5,
            "name": "Potato"
        }
    ]
}

Now, I am attempting to update that object by putting the following JSON object to the IngredientSerializer:

{
    "id": 91
    "name": "Potato Salad"
    "ingredient": [
        {
            "id": 5,
            "name": "Potato"
        },
        {
            "id": 6,
            "name": "Mayo"
        }
    ]
}

What I want is it to recognize that the relationship to Potato already exists and skip over that, but add a relationship to the Mayo object. Note that the Mayo object already exists in Ingredients, but is not yet tied to the Potato Salad object.

What actually happens is the Serializer tries to create a new Ingredient object and fails because "ingredient with this name already exists."

How do I accomplish this?

like image 939
Webucator Avatar asked Apr 03 '18 14:04

Webucator


1 Answers

DRF does not have any automatic "write" behavior for nested serializers, precisely because it does not know things like how to go about updates in the scenario you mentioned. Therefore, you need to write your own update method in your RecipeSerializer.

class IngredientSerializer(serializers.ModelSerializer):
  def validate_name(self, value):
    # manually validate
    pass

  class Meta:
    model = Ingredient
    fields = ['id', 'name']
    extra_kwargs = {
        'name': {'validators': []}, # remove uniqueness validation
    }


class RecipeSerializer(serializers.ModelSerializer):
  ingredient = IngredientSerializer(many=True, read_only=False)

  def update(self, instance, validated_data):
    ingredients = validated_data.pop('ingredient')
    # ... logic to save ingredients for this recipe instance
    return instance

  class Meta:
    model = Recipe
    fields = ['id', 'name', 'ingredient']

Relevant DRF documentation:

Saving Instances

Writable Nested Serializer

Updating nested serializers

Update:

If DRF validation fails for the uniqueness constraint in the name field, you should try removing validators for that field.

Alternative solution: Only use full serializer as read only field

You can change you RecipeSerializer to the following:

class RecipeSerializer(serializers.ModelSerializer):
  ingredient_details = IngredientSerializer(many=True, read_only=True, source='ingredient')

  class Meta:
    model = Recipe
    fields = ['id', 'name', 'ingredient', 'ingredient_details']

And that's it. No need to override update or anything. You'll get the detailed representation when you get a recipe, and you can just PUT with the ingredient ids when updating. So your json when updating will look something like this:

{
    "id": 91
    "name": "Potato Salad"
    "ingredient": [5, 6]
}
like image 170
slider Avatar answered Oct 02 '22 14:10

slider