Posting a one-to-many relationship

2020-06-03 05:32发布

问题:

I'm trying to expose an API to my Django model through Django REST framework.

I have an object Observation. An observation can contain multiple things that have been observed. So I represented it like this:

class Observation(models.Model):

    photo_file = models.ImageField( upload_to=img_dir,   blank=True, null=True )
    titestamp = models.DateTimeField(blank=True, null=True)
    latitude = models.FloatField()
    longitude = models.FloatField()


class ObservedThing(models.Model):
    thing = models.ForeignKey(Thing) # the thing being observed
    observation = models.ForeignKey(Observation, related_name='observed_thing')
    value = models.FloatField()

As I understand this is a one-to-many relationship.

I now have an API View:

class ObsvList(generics.ListCreateAPIView):
    """
    API endpoint that represents a list of observations.
    """
    model = Observation
    serializer_class = ObsvSerializer

and the corresponding serialiser:

class ObsvSerializer(serializers.ModelSerializer):

    observed_thing = serializers.PrimaryKeyRelatedField(many=True)

    class Meta:
        model = Observation

What do I have to do to be able to POST an observation with several things detected? I cannot figure it out. Many thanks.

回答1:

(answer more or less copied from another similar but less clear question)

To create multiple related objects in a single POST requires writable nested serializers which are not yet available.

Full support is a work in progress, but in the mean time one (hacky) solution is to override the create method in the view in each case:

class FooListCreateView(ListCreateAPIView):
    model = Foo
    serializer_class = FooSerializer

    def create(self, request, *args, **kwargs):
        data=request.DATA

        f = Foo.objects.create()

        # ... create nested objects from request data ...  

        # ...
        return Response(serializer.data, 
                        status=status.HTTP_201_CREATED,
                        headers=headers)

Probably not ideal, but it works for me until the proper way comes along.

The other option is to create the related Observation objects individually with separate POSTs, and the use PrimaryKeyRelatedField or HyperlinkedRelatedField to make the associations in the final ObservedThing POST.



回答2:

I know this thread has already an answer but I started working to solve this problem, and since this post was one of my inspirations, I would like to share my final solution. It can be useful to someone. I have the models, so the parent class:

#parent model class
class Parent(models.Model):

    id = models.AutoField(primary_key=True)
    field = models.CharField(max_length=45)

    class Meta:
        managed = False
        db_table = 'parent'

then, the child class:

#child model class
class Child(models.Model):

    id = models.AutoField(primary_key=True)
    field = models.CharField(max_length=45)
    parent = models.ForeignKey(Parent, related_name='children')

    class Meta:
        managed = False
        db_table = 'child'

I had to define the serializers, since I didn't want to create a router accessible url to directly manage Children objects, but I wanted to create them through the ModelViewSet of the parent ModelViewSet, this is what I needed:

class ChildSerializer(serializers.ModelSerializer):
    class Meta:
        model = Child
        read_only_fields = ('id',)

class ParentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Banner
        read_only_fields = ('id',)

class ParentSerializerNested(ParentSerializer):
    children = ChildSerializer(many=True)

I was then ready to create the ModelViewSet, overriding/extending the create/update mixins, and make it generic in order to reuse it for other cases:

class ParentChildViewSet(viewsets.ModelViewSet):

    def create(self, request, *args, **kwargs):
        serializer = self.serializer_parent(data=request.DATA,
                                            files=request.FILES)

        try:
            if serializer.is_valid():
                with transaction.commit_on_success():
                    self.pre_save(serializer.object)
                    parent = serializer.save(force_insert=True)
                    self.post_save(parent, created=True)

                    # need to insert children records
                    for child in request.DATA[self.child_field]:
                        child[self.parent_field] = parent.id
                        child_record = self.serializer_child(data=child)
                        if child_record.is_valid():
                            child_record.save(force_insert=True)
                        else:
                            raise ValidationError('Child validation failed')

                    headers = self.get_success_headers(serializer.data)

                    serializer.data[self.child_field] = self.serializer_child(
                        self.model_child.objects.filter(
                            **{self.parent_field: parent.id}).all(),
                            many=True).data
                    return Response(serializer.data,
                                    status=status.HTTP_201_CREATED,
                                    headers=headers)
        except ValidationError:
            pass
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

So I can reuse it for every nested relationship case I have in my app like this:

class ParentViewSet(ParentChildViewSet):
    child_field = 'children'
    parent_field = 'parent'
    model = Parent
    model_child = Child
    serializer_class = ParentSerializerNested
    serializer_parent = ParentSerializer
    serializer_child = ChildSerializer

And in the end, the routing:

router = routers.DefaultRouter()
router.register(r'parents', ParentViewSet)

It works like a charm!



回答3:

thing = models.ManyToManyField('Thing')

you need to use many to many relationship for creating a temporary table that will store the keys and associate data automatically.