在DRF中,如何创建一个POST序列化器,在其中可以添加多个外键字段的值?

huangapple go评论59阅读模式
英文:

IN DRF, how to create a POST serializer where I can add multiple values of a Foreign Key field

问题

这是我有的两个模型:

class Skill(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name + " - ID: " + str(self.id)

class Experience(models.Model):
    consultant = models.ForeignKey("Consultant", related_name="experience", on_delete=models.CASCADE)
    project_name = models.CharField(max_length=100)
    company = models.CharField(max_length=100)
    company_description = models.TextField(null=True, blank=True)
    from_date = models.DateField()
    to_date = models.DateField()
    project_description = models.CharField(max_length=100)
    contribution = models.TextField()
    summary = models.TextField()
    is_pinned = models.BooleanField(default=False)
    role = models.CharField(max_length=100, null=True)
    skill = models.ForeignKey("Skill", related_name="experience", on_delete=models.CASCADE)

我想做的事情在DRF中似乎不能直接实现,我想要一个具有POST方法的/experience/端点,我可以在其中发送一系列技能ID(skill字段,外键)。例如:

{
  "project_name": "Project AVC",
  "company": "XYZ Company",
  "company_description": "Description of XYZ Company",
  "from_date": "2022-01-01",
  "to_date": "2022-12-31",
  "project_description": "Description of Project ABC",
  "contribution": "Contributions to Project ABC",
  "summary": "Summary of Experience",
  "is_pinned": false,
  "role": "Consultant",
  "skills_ids": [1,2,3],
  "consultant": 1
}

如果数据库中存在ID为1、2、3的Skill记录,则会创建3条Experience记录(每个技能一条)。如果没有这些ID的技能,那么在验证过程中应向用户返回错误信息。

字段的名称可以是skillskillsskill_ids...都可以。

这是我创建的ExperienceSerializer:

class ExperienceSerializer(serializers.ModelSerializer):
    skills = serializers.PrimaryKeyRelatedField(
        many=True,
        queryset=Skill.objects.all(),
        write_only=True
    )

    class Meta:
        model = Experience
        exclude = ['skill']

    def create(self, validated_data):
        skills_data = validated_data.pop('skills', [])
        experience = Experience.objects.create(**validated_data)

        for skill in skills_data:
            experience.skill.add(skill)

        return experience

但这会导致错误:

django.db.utils.IntegrityError: null value in column "skill_id" of relation "coody_portfolio_experience" violates not-null constraint
DETAIL: Failing row contains (21, BOOM, XYZ Company, 2022-01-01, 2022-12-31, Description of Project ABC, Contributions to Project ABC, Summary of Experience, 1, null, f, Consultant, Description of XYZ Company).

我还尝试使用serializers.ListField,但似乎不太适合这个序列化器。

我还尝试了这个答案中的方法,然后我的序列化器如下:

class ExperienceSerializer(serializers.ModelSerializer):
    skill_ids = serializers.ListField(
        child=SkillSerializer(),
        write_only=True
    )

    class Meta:
        model = Experience
        fields = (
            'consultant',
            'project_name',
            'company',
            'company_description',
            'from_date',
            'to_date',
            'project_description',
            'contribution',
            'summary',
            'is_pinned',
            'role',
            'skill',
            'skill_ids'
        )

    def create(self, validated_data):
        skill_ids = validated_data.pop('skill_ids')
        experience = Experience.objects.create(**validated_data)
        experience.skill.set(skill_ids)

        return experience

我稍微修改了答案中的部分,将child=serializers.IntegerField更改为child=SkillSerializer(),因为它给出了未实例化的错误。还注意到现在使用了ListField

在这个版本中,我的载荷如下:

{
  "project_name": "BOOM",
  "company": "XYZ Company",
  "company_description": "Description of XYZ Company",
  "from_date": "2022-01-01",
  "to_date": "2022-12-31",
  "project_description": "Description of Project ABC",
  "contribution": "Contributions to Project ABC",
  "summary": "Summary of Experience",
  "is_pinned": false,
  "role": "Consultant",
  "skill_ids": [3, 4,2,1],
  "consultant": 1
}

这将导致400错误:

{
	"skill": [
		"This field is required."
	],
	"skill_ids": {
		"0": {
			"non_field_errors": [
				"Invalid data. Expected a dictionary, but got int."
			]
		},
		"1": {
			"non_field_errors": [
				"Invalid data. Expected a dictionary, but got int."
			]
		},
		"2": {
			"non_field_errors": [
				"Invalid data. Expected a dictionary, but got int."
			]
		},
		"3": {
			"non_field_errors": [
				"Invalid data. Expected a dictionary, but got int."
			]
		}
	}
}

我还尝试了这个例子,但无济于事。

我花了一些时间阅读这篇完整的帖子,解释了嵌套序列化的问题,但我认为这与我的问题不太相关。我只想在POST中发送一个列表。

我实在是陷入了尝试不同组件的兔子洞,但我不知道DRF想要我如何处理这些问题,而且他们的文档非常糟糕,缺乏简单的示例。

如果有人能提供示例并进行解释,而不仅仅是解决方案,那将不胜感激。

英文:

These are 2 models I have:


class Skill(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name + " - ID: " + str(self.id)

class Experience(models.Model):
    consultant = models.ForeignKey("Consultant", related_name="experience", on_delete=models.CASCADE)
    project_name = models.CharField(max_length=100)
    company = models.CharField(max_length=100)
    company_description = models.TextField(null=True, blank=True)
    from_date = models.DateField()
    to_date = models.DateField()
    project_description = models.CharField(max_length=100)
    contribution = models.TextField()
    summary = models.TextField()
    is_pinned = models.BooleanField(default=False)
    role = models.CharField(max_length=100, null=True)
    skill = models.ForeignKey("Skill", related_name="experience", on_delete=models.CASCADE)

I want to do something that is quite common but apparently not possible out of the box with DRF: I want to have an endpoint /experience/ with a POST method where I can send a LIST of skill ids (skill field, ForeignKey). For example:

{
  "project_name": "Project AVC",
  "company": "XYZ Company",
  "company_description": "Description of XYZ Company",
  "from_date": "2022-01-01",
  "to_date": "2022-12-31",
  "project_description": "Description of Project ABC",
  "contribution": "Contributions to Project ABC",
  "summary": "Summary of Experience",
  "is_pinned": false,
  "role": "Consultant",
  "skills_ids": [1,2,3],
  "consultant": 1
}

If there are Skill records in the DB with ids 1,2,3 then it will create 3 records in the experience table (one for each skill ofc) . If there's no skill with such id, then during validation it should return an error to the user informing so.

The name of the field can be either skill , skills, skill_ids... it does not matter.

This is the ExperienceSerializer I created:

class ExperienceSerializer(serializers.ModelSerializer):
    skills = serializers.PrimaryKeyRelatedField(
        many=True,
        queryset=Skill.objects.all(),
        write_only=True
    )

    class Meta:
        model = Experience
        exclude = ['skill']

    def create(self, validated_data):
        skills_data = validated_data.pop('skills', [])
        experience = Experience.objects.create(**validated_data)

        for skill in skills_data:
            experience.skill.add(skill)

        return experience

but that gives me the error:

django.db.utils.IntegrityError: null value in column "skill_id" of relation "coody_portfolio_experience" violates not-null constraint
DETAIL: Failing row contains (21, BOOM, XYZ Company, 2022-01-01, 2022-12-31, Description of Project ABC, Contributions to Project ABC, Summary of Experience, 1, null, f, Consultant, Description of XYZ Company).

I also tried using serializers.ListField but it doesn't seem to be quite the serializer for this.

Tried the approach from this answer as well, so then I had my serializer like this:

class ExperienceSerializer(serializers.ModelSerializer):
    skill_ids = serializers.ListField(
        child=SkillSerializer(),
        write_only=True
    )

    class Meta:
        model = Experience
        fields = (
            'consultant',
            'project_name',
            'company',
            'company_description',
            'from_date',
            'to_date',
            'project_description',
            'contribution',
            'summary',
            'is_pinned',
            'role',
            'skill',
            'skill_ids'
        )

    def create(self, validated_data):
        skill_ids = validated_data.pop('skill_ids')
        experience = Experience.objects.create(**validated_data)
        experience.set(skill_ids)

        return experience

I modified the answer a bit from child = serializers.IntegerField, to child=SkillSerializer(), as it was giving me an error of child not being instantiated. Noticed also the use of ListField now as well.

And here is my payload in this version:

{
 "project_name": "BOOM",
 "company": "XYZ Company",
 "company_description": "Description of XYZ Company",
 "from_date": "2022-01-01",
 "to_date": "2022-12-31",
 "project_description": "Description of Project ABC",
 "contribution": "Contributions to Project ABC",
 "summary": "Summary of Experience",
 "is_pinned": false,
 "role": "Consultant",
 "skill_ids": [3, 4,2,1],
   "consultant": 1
}

which gives error 400:

{
	"skill": [
		"This field is required."
	],
	"skill_ids": {
		"0": {
			"non_field_errors": [
				"Invalid data. Expected a dictionary, but got int."
			]
		},
		"1": {
			"non_field_errors": [
				"Invalid data. Expected a dictionary, but got int."
			]
		},
		"2": {
			"non_field_errors": [
				"Invalid data. Expected a dictionary, but got int."
			]
		},
		"3": {
			"non_field_errors": [
				"Invalid data. Expected a dictionary, but got int."
			]
		}
	}
}

Tried also this example here to no avail.
Spend some time reading this entire post explaining the issue of nested serialization, but I don't think it's quite related to my issue. All I want is a list to be sent in POST

I'm honestly going into a rabbit hole now of just trying different pieces together, but I have no idea how DRF wants me to do these stuff and their documentation is awful and lacking simple examples.

If someone could post example but also with explanations and not just the solution that would be much appreciated

答案1

得分: 1

根据当前关系,如果您的载荷包含 "skills_ids": [1,2,3], 那么您将创建 三个 不同的 Experience 实例,每个实例包含一个技能,这是****您想要的,这是不好的做法。

相反,多对多 关系更为合适,将多个技能关联到一个 Experience,反之亦然,从而避免数据库中的重复值。

这也是您在 experience.skill.add(skill) 中使用的语法,这是您使用此关系将 Skill 附加到 Experience 的方式。但实际上,您无需做任何其他操作,只需让框架为您工作即可!

models.py

class Skill(models.Model):
    ...


class Experience(models.Model):
    ...
    skills = models.ManyToManyField(Skill)

serializers.py

class ExperienceSerializer(serializers.ModelSerializer):
    class Meta:
        model = Experience
        fields = '__all__'

载荷

{
  "project_name": "Project AVC",
  "company": "XYZ Company",
  "company_description": "Description of XYZ Company",
  "from_date": "2022-01-01",
  "to_date": "2022-12-31",
  "project_description": "Description of Project ABC",
  "contribution": "Contributions to Project ABC",
  "summary": "Summary of Experience",
  "is_pinned": false,
  "role": "Consultant",
  "skills": [1,2,3],
  "consultant": 1
}
英文:

With the current relation, if your payload contains "skills_ids": [1,2,3], then you would create three different instances of Experience each one containing a skill, which is NOT what you want, that is bad practice.

Instead, a many-to-many relationship is more adequate, associating multiple skills to an Experience and the other way around, thus avoiding duplicate values in your database.

Which is also the syntax that you are using at experience.skill.add(skill) that is how you would attach a Skill to an Experience using such relation. But, in reality you do not need to do anything other than letting the framework work for you!

models.py

class Skill(models.Model):
    ...


class Experience(models.Model):
    ...
    skills = models.ManyToManyField(Skill)

serializers.py

class ExperienceSerializer(serializers.ModelSerializer):
    class Meta:
        model = Experience
        fields = '__all__'

payload

{
  "project_name": "Project AVC",
  "company": "XYZ Company",
  "company_description": "Description of XYZ Company",
  "from_date": "2022-01-01",
  "to_date": "2022-12-31",
  "project_description": "Description of Project ABC",
  "contribution": "Contributions to Project ABC",
  "summary": "Summary of Experience",
  "is_pinned": false,
  "role": "Consultant",
  "skills": [1,2,3],
  "consultant": 1
}

huangapple
  • 本文由 发表于 2023年6月1日 23:50:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/76383683.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定