Django rest framework get or create for PrimaryKeyRelatedFie



我开始使用Django和Django rest框架为我的Web应用程序创建REST API,我需要一个逻辑问题。

有实体指令和标签。用户访问我的服务并创建自我说明并为其添加存在标签或新标签。

我创建了我的模型序列化器类,使用PrimaryKeyRelatedField作为关系Directive<->Tag。但是,如果我为带有新标签的新指令执行 POST,则会出现错误:">无效的 pk \"标签名称" - 对象不存在。我通过在字段类中重写to_internal_value方法解决了这个问题。

解决此问题的最佳实践是什么?在我看来,这个问题是Web和REST API的典型问题。

我的模型:

class Tag(Model):
name = CharField(max_length=32, verbose_name=_("Name"),
unique=True, validators=[alphanumeric], primary_key=True)
def __str__(self):
return self.name

class Instruction(Model):
user = ForeignKey(settings.AUTH_USER_MODEL,
related_name='instructions',
on_delete=CASCADE,
blank=False, null=False,
verbose_name=_("User"))
title = CharField(max_length=256,
verbose_name=_("Title"),
blank=False, null=False)
created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
modified_datetime = DateTimeField(
verbose_name=_("Last modification time"), blank=False, null=False)
tags = ManyToManyField(Tag,
related_name="instructions",
verbose_name=_("Tags"))
class Meta:
ordering = ['-created_datetime']
# singular_name = _("")
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
n = now()
if self.id is None:
self.created_datetime = n
self.modified_datetime = n
super(Instruction, self).save(force_insert, force_update, using, update_fields)
def __str__(self):
return self.title

我的序列化程序:

class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ('name',)
class InstructionSerializer(serializers.ModelSerializer):
tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
author = serializers.SerializerMethodField()
def get_author(self, obj):
return obj.user.username
class Meta:
model = Instruction
fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author')
read_only_fields = ('modified_datetime',)

我创建了一个新的字段类类 PrimaryKeyCreateRelatedField并覆盖了用于创建新 Tag 对象to_internal_value方法,而不是使用消息"does_not_exist"引发:

PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
if self.pk_field is not None:
data = self.pk_field.to_internal_value(data)
try:
return self.get_queryset().get(pk=data)
except ObjectDoesNotExist:
# self.fail('does_not_exist', pk_value=data)
return self.get_queryset().create(pk=data)
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)

我的观点:

class InstructionViewSet(viewsets.ModelViewSet):
queryset = Instruction.objects.all()
serializer_class = InstructionSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def create(self, request, *args, **kwargs):
data = dict.copy(request.data)
data['user'] = self.request.user.pk
serializer = InstructionSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

更新

models.py

alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$',
_('Only alphanumeric characters are allowed.'))

class Tag(Model):
name = CharField(max_length=32, verbose_name=_("Name"),
unique=True, validators=[alphanumeric], primary_key=True)
def __str__(self):
return self.name

class Step(PolymorphicModel):
instruction = ForeignKey(Instruction,
verbose_name=_("Instruction"),
related_name='steps',
blank=False, null=False,
on_delete=CASCADE)
position = PositiveSmallIntegerField(verbose_name=_("Position"), default=0)
description = TextField(verbose_name=_("Description"),
max_length=2048,
blank=False, null=False)
class Meta:
verbose_name = _("Step")
verbose_name_plural = _("Steps")
ordering = ('position',)
unique_together = ("instruction", "position")
def __str__(self):
return self.description[:100]

class Instruction(Model):
user = ForeignKey(settings.AUTH_USER_MODEL,
related_name='instructions',
on_delete=CASCADE,
blank=False, null=False,
verbose_name=_("User"))
title = CharField(max_length=256,
verbose_name=_("Title"),
blank=False, null=False)
created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
modified_datetime = DateTimeField(
verbose_name=_("Last modification time"), blank=False, null=False)
tags = ManyToManyField(Tag,
related_name="instructions",
verbose_name=_("Tags"))
# thumbnail = #TODO: image field
class Meta:
ordering = ['-created_datetime']
# singular_name = _("")
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
n = now()
if self.id is None:
self.created_datetime = n
self.modified_datetime = n
super(Instruction, self).save(force_insert, force_update, using, update_fields)
def __str__(self):
return self.title

views.py

class InstructionViewSet(viewsets.ModelViewSet):
queryset = Instruction.objects.all()
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get_serializer_class(self):
"""Return different serializer class for different action."""
if self.action == 'list':
return InstructionSerializer
elif self.action == 'create':
return InstructionCreateSerializer

serialiers.py

class PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
if self.pk_field is not None:
data = self.pk_field.to_internal_value(data)
try:
return self.get_queryset().get(pk=data)
except ObjectDoesNotExist:
# self.fail('does_not_exist', pk_value=data)
return self.get_queryset().create(pk=data)
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)

class InstructionCreateSerializer(serializers.ModelSerializer):
tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
steps = InstructionStepSerializer(many=True)
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Instruction
fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'steps')
read_only_fields = ('modified_datetime',)
def create(self, validated_data):
tags_data = validated_data.pop('tags')
steps_data = validated_data.pop('steps')
# NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
# "needs to have a value for field "id" before this many-to-many relationship can be used."
instruction = Instruction.objects.create(**validated_data)
for tag in tags_data:
instruction.tags.add(tag)
for step in steps_data:
Step.objects.create(instruction=instruction,
description=step['description'],
position=step['position'])
return instruction

class InstructionSerializer(serializers.ModelSerializer):
tags = serializers.StringRelatedField(many=True)
author = serializers.SerializerMethodField()
steps = InstructionStepSerializer(many=True)
def get_author(self, obj):
return obj.user.username
class Meta:
model = Instruction
fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author', 'steps')
read_only_fields = ('modified_datetime',)

就我而言,要解决问题,我需要覆盖方法run_validation。这允许在验证之前检查tags并创建它们(如果不存在)。

class InstructionCreateSerializer(serializers.ModelSerializer):
steps = InstructionStepSerializer(many=True)
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Instruction
fields = ('title', 'created_datetime', 'modified_datetime', 'tags', 'steps', 'id', 'user')
read_only_fields = ('modified_datetime',)
def run_validation(self, data=serializers.empty):
if 'tags' in data:
for tag in data['tags']:
Tag.objects.get_or_create(name=tag)
return super(InstructionCreateSerializer, self).run_validation(data)
def create(self, validated_data):
tags_data = validated_data.pop('tags')
steps_data = validated_data.pop('steps')
# NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
# "needs to have a value for field "id" before this many-to-many relationship can be used."
instruction = Instruction.objects.create(**validated_data)
for tag in tags_data:
instruction.tags.add(tag)
for step in steps_data:
Step.objects.create(instruction=instruction,
description=step['description'],
position=step['position'])
return instruction

除了 @YPCrumble 和 @SijanBhandari 给出的答案外,我只需要对您的代码中的某些内容进行评论。

在 models.py 中,您已经覆盖了用于添加created_at和modified_on的保存方法。为此,您可以添加

created_at = models.DateTimeField(auto_now_add=True)
modified_on = DateTimeField (auto_now=True)

auto_now_add选项设置首次创建对象的时间。 它不可编辑。每当保存对象时,即每当调用 object.save() 方法时,都会设置auto_now设置。

这些通常用于为对象添加时间戳以供将来引用。

为什么要写这么多行,而你只需要 2 行代码就可以做到这一点。 不过只是抬头!!

有关更多详细信息,请转到此处的文档

在"常规"Django 中,你通常希望在表单的save方法中创建模型实例,而不是视图。DRF 与此类似,因为您希望在序列化程序的createupdate方法中创建模型实例。这样做的原因是,如果需要向 API 添加新终结点,则可以重用序列化程序,而不必编写创建或更新模型实例的重复代码。

以下是我重构代码的方法:

  • ModelViewSet中删除整个create方法 - 无需覆盖它。
  • 删除自定义PrimaryKeyCreateRelatedField- 您只需要一个PrimaryKeyRelatedField
  • 向序列化程序添加两个方法 -createupdate
    • create方法中,先创建tag对象,然后再保存instruction对象,如 DRF 文档中所示。 您可以像在视图中一样获取当前用户,就像您在此create方法中的self.context['request'].user一样。因此,您可以像Instruction.objects.create(user=self.context['request'].user, **validated_data)一样创建Instruction,然后循环遍历tags(就像他们在文档中对tracks所做的那样)以将它们添加到Instruction中。
    • 文档没有示例update方法,但本质上您的update方法也为现有instruction采用instance参数。有关更多详细信息,请参阅DRF创建者的此答案

最好的方法是在视图的 CREATE 方法中整理所有内容。

我相信您的标签将以以下格式从前端发送到后端

[   1,
{'name': "TEST"},
{'name': 'TEST2'}
]

这里'1'是现有的标签 ID,'TEST''TEST2'是插入的两个新标签 用户。现在,您可以更改 CREATE 方法,如下所示:

class InstructionViewSet(viewsets.ModelViewSet):
queryset = Instruction.objects.all()
serializer_class = InstructionSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def create(self, request, *args, **kwargs):
data = dict.copy(request.data)
data['user'] = self.request.user.pk
# MODIFICATION.....
tags = self.request.get('tags', None)
tag_list = []
if tags:
for tag in tags:
if isinstance(tag, dict):
new_tag = Tag.objects.create(name=tag['name'])
tag_list.append(new_tag.id)
else:
tag_list.append(int(tag))

data = {
'title': ....
'tags': tag_list,
'user': ...
'author': ...
......

}
serializer = InstructionSerializer(data=data)

希望对您有所帮助。

最新更新