DRF Intermediary Table - POSTing data to the Intermediary Table

Viewed 42

I have a many-to-many relationship in my DB design and I am having trouble with the POST aspect. I currently have a table called Loads, Containers, and Container_Loads (this is the intermediary table).

My question is this: I want to be able to send a POST request into the ContainerLoad intermediary table and just update that table with the values it requires which are: Load ID (PK of the Load table), Container ID (PK of the Container Table) and # of pallets (unique field to the intermediary table). I am able to GET/retrieve the records just fine, but when I try to send a POST request with a payload such as

{
        "id":3,
        "pallets":"4",
        "containerNumberId":5,
        "loadNumberId":53
}

(where containerNumberID and loadNumberID are the existing keys in their respective tables), it seems that my code wants to create a whole new Load entry as well (as it asks me for the remaining fields of the Load model), where as I just want to create an entry in the intermediary table without creating a new entry in the Load table.

So for the purpose of my project, a load can be on many containers [imagine that it's split because all of it couldn't fit on one] and a container can belong to many loads.

My models.py looks like this:

class ContainerLoad(models.Model):
    id = models.AutoField(primary_key=True)
    load_number = models.ForeignKey(Load,on_delete=models.CASCADE)
    container_number = models.ForeignKey(Container,on_delete=models.CASCADE)
    pallets = models.CharField(blank=True,null=True,default=0,max_length=20)

    class Meta:
        db_table = 'ContainerLoad'


#load model shortened for brevity
class Load(models.Model):

    id = models.AutoField(primary_key=True)
    bnsf_container_number = models.ManyToManyField(Container, through='ContainerLoad',through_fields=('load_number','container_number'))

    class Meta:
        db_table = "Load"

class Container(models.Model):
    id = models.AutoField(primary_key=True)
    container_number = models.CharField(max_length=15)
    in_use = models.BooleanField()
    

    class Meta:
        db_table = "Container"

my serializers.py currently looks like this, the commented out section is from me attempting to get the POST to work)

class ContainerLoadSerializer(WritableNestedModelSerializer):

    # load_number_id = LoadSerializer(read_only=False)
    # container_number_id = ContainerSerializer(read_only=False)


    class Meta:
        model = ContainerLoad
        fields = "__all__"
        depth = 2

class LoadSerializer(WritableNestedModelSerializer):

    primary_driver = DriverSerializer(read_only=False)
    second_driver = DriverSerializer(allow_null=True,read_only=False)
    third_driver = DriverSerializer(allow_null=True,read_only=False)
    bnsf_container_number = ContainerSerializer(read_only=False)
    pickup_location = LocationSerializer(read_only=False)
    delivery_location = LocationSerializer(read_only=False)
    broker = BrokerSerializer(read_only=False)
    booked_by = EmployeeSerializer(read_only=False) 

    class Meta:
        model = Load
        fields = '__all__'
        depth = 1

class ContainerSerializer(serializers.ModelSerializer):
    container_number = serializers.CharField()
    in_use = serializers.BooleanField()

    class Meta:
        model = Container
        fields = '__all__'
        depth = 1

And finally the views.py

class ContainerLoadViews(APIView):
    def get(self, request, id=None):
        if id:
            container = ContainerLoad.objects.get(id=id)
            serializer = ContainerLoadSerializer(container)
            return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
        else:
            containers = ContainerLoad.objects.all()
            serializer = ContainerLoadSerializer(containers, many=True)
            return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
    
    def post(self, request):
        serializer = ContainerLoadSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
        else:
            return Response({"status": "Error", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
3 Answers

If you look at the code of the save method of the BaseSerializer class you will see this:

if self.instance is not None:
    self.instance = self.update(self.instance, validated_data)
    assert self.instance is not None, (
        '`update()` did not return an object instance.'
    )
else:
    self.instance = self.create(validated_data)
    assert self.instance is not None, (
        '`create()` did not return an object instance.'
    )

How you are not passing the instance in the post function:

serializer = ContainerLoadSerializer(data=request.data)

The save is always calling to create. You should do something like.

try:
    instance = ContainerLoad.object.get(id=request.data['id'])
except: 
    instance = None

serializer = ContainerLoadSerializer(instance=instance, data = request.data)

You have depth is set to 2 in your ContainerLoadSerializer Meta class, which is telling the serializer to generate a nested representation of your models.

https://www.django-rest-framework.org/api-guide/serializers/#specifying-nested-serialization

The default ModelSerializer uses primary keys for relationships, but you can also easily generate nested representations using the depth option:

The depth option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.

If you remove the depth attribute, the serializer should default back to expecting a primary key value, which is your desired behaviour.

Your serializer should look something like this:

class ContainerLoadSerializer(serializers.ModelSerializer):

    class Meta:
        model = ContainerLoad
        fields = "__all__"

The solution to this was that I needed a nested response when reading the data from the ContainerLoad table but a simple write (not nested) function when POSTing the data.

The solution was to use the to_representation and to_internal_value methods (https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior) available within DRF to override the behavior of the serializers. Here is the code that now works for both GET and POST requests and it is no longer asking me for fields related to the Load or Container models when inserting data.

class ContainerSerializer(serializers.ModelSerializer):
    container_number = serializers.CharField()
    in_use = serializers.BooleanField()

    class ContainerFieldSerializer(serializers.Field):
        def to_internal_value(self,value):
            return Container.objects.get(id=value)
        
        def to_representation(self,instance):
            return ContainerSerializer(instance=instance).data

    class Meta:
        model = Container
        fields = '__all__'
        depth = 1

I did the same for the Load Serializer.

and then for my ContainerLoad Serializer I just assign the FK fields to the new classes I created:

class ContainerLoadSerializer(serializers.ModelSerializer):

    cl_container = ContainerSerializer.ContainerFieldSerializer()
    cl_load = LoadSerializer.LoadFieldSerializer()

    class Meta:
        model = ContainerLoad
        fields = "__all__"
        depth = 2
Related