0

I feel like I'm chasing my tail here and so I've come to your fine folks to help me understand where I've screwed up and why my thinking on this must be flawed in some way.

I'm writing an API in DRF and while it doesn't have a lot of tables in the DB, there are many to many relationships in the DB which makes it feel complicated, or at least it makes it difficult to just view the db tables and intuitively understand what's related.

First my model. I'm getting this when attempting to POST a new object to the jobs model. I'm needing to validate that the requesting worker has the permission to create a job with the related target and workergroup

Models:

class Workers(models.Model):
    class Meta:
        ordering = ['id']
    workerid = models.CharField(max_length=16, verbose_name="Worker ID", unique=True, default="Empty")
    customer = models.ForeignKey(Customers, on_delete=models.DO_NOTHING, default=1)
    workername = models.CharField(max_length=64, verbose_name="Worker Friendly Name", default="")
    datecreated = models.DateTimeField(auto_now_add=True)
    awsarn = models.CharField(max_length=60, verbose_name="ARN Name of Worker", blank=True, null=True)
    customerrights = models.ManyToManyField(Customers, related_name="access_rights", default="")


class Targets(models.Model):
    class Meta:
        ordering = ['id']
    customer = models.ForeignKey(Customers, on_delete=models.SET_NULL, default=1, null=True)
    friendly_name = models.CharField(max_length=70, verbose_name="Target Friendly Name", unique=False)
    hostname = models.CharField(max_length=120, verbose_name="Target Hostname", default="")
    ipaddr = models.GenericIPAddressField(protocol='both', unpack_ipv4=True, default="", null=True)


class WorkerGroups(models.Model):
    class Meta:
        ordering = ['id']
    name = models.CharField(max_length=60, default="Default Group")
    workers = models.ManyToManyField(Workers)


class Jobs(models.Model):
    target = models.ForeignKey(Targets, on_delete=models.DO_NOTHING)
    datecreated = models.DateTimeField(auto_now_add=True)
    startdate = models.DateTimeField()
    enddate = models.DateTimeField(null=True)
    frequency = models.TimeField(default='00:05')
    workergroup = models.ForeignKey(WorkerGroups, on_delete=models.DO_NOTHING)
    jobdefinition = models.ForeignKey(JobDefinitions, on_delete=models.DO_NOTHING)

In my serializers I have a JobSerializer which is referencing PrimaryKeyRelatedField classes which I think should have the effect of limiting the queryset which validates those related models. BTW, the customerrightsids are being built in the view and that seems to work fine for all other models.

Serializers:

    def get_queryset(self):
        print("In Custom TargetPK get_queryset")
        queryset = Targets.objects.filter(customer_id__in=self.context['auth'].customerrightsids)
        if isinstance(queryset, (QuerySet, Manager)):
            queryset = queryset.all()
        return queryset


class WorkerGroupPKSerializer(serializers.PrimaryKeyRelatedField):

    def get_queryset(self):
        print("In Custom WorkerGroupPK get_queryset")
        queryset = WorkerGroups.objects.filter(workers__customer_id__in=self.context['auth'].customerrightsids)
        if isinstance(queryset, (QuerySet, Manager)):
            queryset = queryset.all()
        return queryset

class JobSerializer(serializers.ModelSerializer):
    workergroup = WorkerGroupPKSerializer(many=False) # Commenting this out removes error
    target = TargetPKSerializer(many=False) # This seems to work fine even though it's similar to the line above

    class Meta:
        model = Jobs
        fields = '__all__'

    def create(self, validated_data):
        print(self.context['auth'])
        return super().create(validated_data)

There's nothing special in the create method of the viewset object. It's taking the request and passing a couple into the ViewSet and updating its context. I can share that if needed, but that doesn't seem to be where the issue is.

So finally, for the error. When I perform a POST to /jobs/ I'm getting the following:

Error:

MultipleObjectsReturned at /jobs/
get() returned more than one WorkerGroups -- it returned 2!

The error clearly states that I'm getting multiple WorkerGroups returned in a get() but I don't know where or how to resolve that in this case.

It is clearly a problem with the WorkerGroupPKSerializer. If I comment out the reference to it from the JobSerializer the error goes away. That stops the validation of that field though, so that's not a workable solution!

Community
  • 1
  • 1

2 Answers2

1

I'm not 100% sure I'm on the right track here, but it seems you might have a problem of duplicate/multiple results. You should try and use .distinct() on the Queryset in WorkerGroupPKSerializer (also, see Django docs). You used a ForeignKey on the customer property of the Worker model, so that makes it possible to have multiple Worker's belonging to the same WorkerGroup matching the filter query, thus returning the same WorkerGroup twice. So when the id of that WorkerGroup is POSTed, the get() will match two results, throwing that error.

Commenting out seems to clear the error, and this would be because of the fact that you're also commenting out many=False, thus, .get() is not called anymore. But as mentioned in the question, this would disable the custom queryset used to filter on.

  • 1
    Thank you my kind Sasja! I think I'm following you but I'll have to sort it in the morning when I've had a better night's rest. This looks like the best lead I've had all day! – Jarred Masterson May 21 '20 at 05:29
  • Ok! That totally worked. I'm reading over your comment again. I understand conceptually how uniqueness eliminates duplicates. But I'm not sure if I understand the specific duplication that is happening here. Workers are "owned" buy a specific customer but many workers can be members of many workergroups. I understand the duplication due to the workergroup, but not necessarily due to the customer. I'm going to post my updated serializer in an answer below, just for the sake of posterity. – Jarred Masterson May 21 '20 at 16:45
  • @JarredMasterson the underlying cause comes from how RDBMS like MySQL work (found [this](https://stackoverflow.com/questions/23786401/why-do-multiple-table-joins-produce-duplicate-rows) question on SO) Basically, you filtered on customer's id's in `Worker`s (django orm doing multiple sql `JOIN`s underlying to 'get to' that table), so every time a customer's id is encountered it says 'filter = true' so return `WorkerGroup`. So if it encounters a customer id twice, it will return a WorkerGroup each time (possibly the same). – Sasja Vandendriessche May 21 '20 at 16:59
0

Thanks to Sasja's answer. Here is the updated WorkerGroupPKSerializer that now works.

class WorkerGroupPKSerializer(serializers.PrimaryKeyRelatedField):

    def get_queryset(self):
        print("In Custom WorkerGroupPK get_queryset")
        queryset = WorkerGroups.objects.filter(workers__customer_id__in=self.context['auth'].customerrightsids).distinct()
        if isinstance(queryset, (QuerySet, Manager)):
            queryset = queryset.all()
        return queryset