使用Serializer写入Recipe时触发AttributeError问题排查
Django序列化器错误:'Ingredient'对象无'amount'属性
错误信息
AttributeError: 尝试在序列化器RecipeIngredientWriteSerializer上获取字段"amount"的值时出现AttributeError。序列化器字段名称可能不正确,与"Ingredient"实例的任何属性或键都不匹配。原始异常信息:'Ingredient'对象没有属性"amount"。
模型代码
class Ingredient(models.Model): name = models.CharField( max_length=constants.MAX_NAME_LENGTH, verbose_name='Название' ) measurement_unit = models.CharField( max_length=constants.MAX_NAME_LENGTH, verbose_name='Единица измерения' ) class Meta: verbose_name = 'Ингредиент' verbose_name_plural = 'Ингредиенты' def __str__(self): return self.name class Recipe(models.Model): name = models.CharField( max_length=constants.MAX_NAME_LENGTH, verbose_name='Название' ) text = models.TextField( verbose_name='Описание' ) author = models.ForeignKey( User, on_delete=models.CASCADE, verbose_name='Автор', related_name='recipes' ) cooking_time = models.PositiveSmallIntegerField( verbose_name='Время приготовления (в минутах)', validators=[ MinValueValidator( 1, 'Допустимое значение не меньше 1' ) ] ) tags = models.ManyToManyField( Tag, verbose_name='Тег', through='RecipeTag' ) ingredients = models.ManyToManyField( Ingredient, verbose_name='Ингредиенты', through='RecipeIngredient' ) image = models.ImageField( upload_to='recipe/images/', null=True, default=None, verbose_name='Картинка' ) pub_date = models.DateTimeField( verbose_name='Дата публикации', auto_now_add=True ) class Meta: ordering = ['-pub_date'] verbose_name = 'рецепт' verbose_name_plural = 'рецепты' def __str__(self): return self.name class RecipeIngredient(models.Model): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE) amount = models.IntegerField( verbose_name='количество ингредиента', validators=[ MinValueValidator( 1, 'Допустимое значение не меньше 1' ) ] ) class Meta: verbose_name = 'ингредиент' verbose_name_plural = 'ингредиенты'
写入序列化器代码
class RecipeIngredientWriteSerializer(serializers.Serializer): id = serializers.IntegerField(required=True) amount = serializers.IntegerField(required=True) def validate_amount(self, value): if value <= 0: raise serializers.ValidationError( 'Количесто ингредиента не может быть меньше 0' ) return value class Meta: model = RecipeIngredient fields = ( 'id', 'amount' ) class RecipeWriteSerializer(serializers.ModelSerializer): tags = serializers.PrimaryKeyRelatedField( many=True, queryset=Tag.objects.all() ) image = Base64ImageField(required=True) ingredients = RecipeIngredientWriteSerializer(many=True) class Meta: model = Recipe fields = ( 'ingredients', 'tags', 'image', 'name', 'text', 'cooking_time' ) def create(self, validated_data): ingredients = validated_data.pop('ingredients') tags = validated_data.pop('tags') recipe = Recipe.objects.create(**validated_data) for tag in tags: RecipeTag.objects.create( tag=tag, recipe=recipe) for ingredient in ingredients: current_ingredient = get_object_or_404( Ingredient, id=ingredient['id'] ) RecipeIngredient.objects.get_or_create( ingredient=current_ingredient, amount=ingredient['amount'], recipe=recipe ) return recipe
请求示例
{ "ingredients": [ { "id": 1123, "amount": 10 } ], "tags": [ 1, 2 ], "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAgMAAABieywaAAAACVBMVEUAAAD///9fX1/S0ecCAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNoAAAAggCByxOyYQAAAABJRU5ErkJggg==", "name": "string", "text": "string", "cooking_time": 1 }
用户疑问
此前读取数据时遇到过同类错误,通过为amount字段添加source参数解决,但该方法在写入场景无效,请问我的代码存在什么问题?
问题根源
当RecipeWriteSerializer完成创建并返回recipe实例后,会自动序列化该实例以返回给前端。此时ingredients字段关联的是Recipe模型的多对多字段,获取的是Ingredient对象集合,但你使用的RecipeIngredientWriteSerializer期望处理包含amount字段的RecipeIngredient对象(中间表实例),而Ingredient本身没有amount属性,因此触发错误。
修复方案
方案1:将ingredients设为仅写入字段(简单快捷)
如果创建成功后不需要返回食材数据,直接给ingredients字段添加write_only=True,让序列化器在返回数据时忽略该字段,避免尝试序列化Ingredient实例:
class RecipeWriteSerializer(serializers.ModelSerializer): tags = serializers.PrimaryKeyRelatedField( many=True, queryset=Tag.objects.all() ) image = Base64ImageField(required=True) # 添加 write_only=True 标记 ingredients = RecipeIngredientWriteSerializer(many=True, write_only=True) # 其余代码保持不变
方案2:调整序列化器以返回带amount的食材数据
如果需要在创建成功后返回包含数量的食材信息,需要拆分写入和读取逻辑:
- 优化
RecipeIngredientWriteSerializer:去掉不必要的Meta类(因为它继承自Serializer而非ModelSerializer)
class RecipeIngredientWriteSerializer(serializers.Serializer): id = serializers.IntegerField(required=True) amount = serializers.IntegerField(required=True) def validate_amount(self, value): if value <= 0: raise serializers.ValidationError( 'Количество ингредиента не может быть меньше 0' ) return value
- 修改
RecipeWriteSerializer,拆分写入用的字段和返回用的字段:
class RecipeWriteSerializer(serializers.ModelSerializer): tags = serializers.PrimaryKeyRelatedField( many=True, queryset=Tag.objects.all() ) image = Base64ImageField(required=True) # 用于返回带数量的食材数据 ingredients = serializers.SerializerMethodField() # 用于接收写入的食材数据,标记为仅写入 ingredients_data = RecipeIngredientWriteSerializer(many=True, write_only=True, source='ingredients') class Meta: model = Recipe fields = ( 'ingredients_data', 'ingredients', 'tags', 'image', 'name', 'text', 'cooking_time' ) def get_ingredients(self, obj): # 从中间表获取关联数据并组装返回格式 recipe_ingredients = obj.recipeingredient_set.all() return [ { 'id': ri.ingredient.id, 'amount': ri.amount, 'name': ri.ingredient.name, 'measurement_unit': ri.ingredient.measurement_unit } for ri in recipe_ingredients ] def create(self, validated_data): # 从validated_data中取出食材数据(source指定为'ingredients',所以键是'ingredients') ingredients = validated_data.pop('ingredients') tags = validated_data.pop('tags') recipe = Recipe.objects.create(**validated_data) for tag in tags: RecipeTag.objects.create(tag=tag, recipe=recipe) for ingredient in ingredients: current_ingredient = get_object_or_404(Ingredient, id=ingredient['id']) RecipeIngredient.objects.get_or_create( ingredient=current_ingredient, amount=ingredient['amount'], recipe=recipe ) return recipe
内容的提问来源于stack exchange,提问作者Maivery
相关产品推荐
相关产品推荐

