142

I have a question about dealing with m2m / through models and their presentation in django rest framework. Let's take a classic example:

models.py:

from django.db import models

class Member(models.Model):
    name = models.CharField(max_length = 20)
    groups = models.ManyToManyField('Group', through = 'Membership')

class Group(models.Model):
    name = models.CharField(max_length = 20)

class Membership(models.Model):
    member = models.ForeignKey('Member')
    group = models.ForeignKey('Group')
    join_date = models.DateTimeField()

serializers.py:

imports...

class MemberSerializer(ModelSerializer):
    class Meta:
        model = Member

class GroupSerializer(ModelSerializer):
    class Meta:
        model = Group

views.py:

imports...

class MemberViewSet(ModelViewSet):
    queryset = Member.objects.all()
    serializer_class = MemberSerializer

class GroupViewSet(ModelViewSet):
    queryset = Group.objects.all()
    serializer_class = GroupSerializer

When GETing an instance of Member, I successfully receive all of the member's fields and also its groups - however I only get the groups' details, without extra details that comes from the Membership model.

In other words I expect to receive:

{
   'id' : 2,
   'name' : 'some member',
   'groups' : [
      {
         'id' : 55,
         'name' : 'group 1'
         'join_date' : 34151564
      },
      {
         'id' : 56,
         'name' : 'group 2'
         'join_date' : 11200299
      }
   ]
}

Note the join_date.

I have tried oh so many solutions, including of course Django Rest-Framework official page about it and no one seems to give a proper plain answer about it - what do I need to do to include these extra fields? I found it more straight-forward with django-tastypie but had some other problems and prefer rest-framework.

Banjer
  • 8,118
  • 5
  • 46
  • 61
mllm
  • 17,068
  • 15
  • 53
  • 64
  • Would http://eugene-yeo.me/2012/12/4/django-tastypie-manytomany-through-part-2/ help ? – karthikr Jun 23 '13 at 01:16
  • 8
    This is for tasty pie, I'm working with Django Rest Framework. – mllm Jun 25 '13 at 22:26
  • you can find the latest answer here https://stackoverflow.com/questions/41976819/django-serialize-a-model-with-a-many-to-many-relationship-with-a-through-argume(https://stackoverflow.com/questions/41976819/django-serialize-a-model-with-a-many-to-many-relationship-with-a-through-argume) – Akhil S Jun 21 '21 at 17:31

4 Answers4

179

How about.....

On your MemberSerializer, define a field on it like:

groups = MembershipSerializer(source='membership_set', many=True)

and then on your membership serializer you can create this:

class MembershipSerializer(serializers.HyperlinkedModelSerializer):

    id = serializers.Field(source='group.id')
    name = serializers.Field(source='group.name')

    class Meta:
        model = Membership

        fields = ('id', 'name', 'join_date', )

That has the overall effect of creating a serialized value, groups, that has as its source the membership you want, and then it uses a custom serializer to pull out the bits you want to display.

EDIT: as commented by @bryanph, serializers.field was renamed to serializers.ReadOnlyField in DRF 3.0, so this should read:

class MembershipSerializer(serializers.HyperlinkedModelSerializer):

    id = serializers.ReadOnlyField(source='group.id')
    name = serializers.ReadOnlyField(source='group.name')

    class Meta:
        model = Membership

        fields = ('id', 'name', 'join_date', )

for any modern implementations

mlissner
  • 17,359
  • 18
  • 106
  • 169
thebaron
  • 1,806
  • 1
  • 12
  • 2
  • 4
    fyi, I've tried many variants of this and I can't get this working. This isn't in the official docs? Where is membership_set defined? – clay Jun 18 '15 at 22:55
  • 6
    `membership_set` is the default related name for Member -> Membership – dustinfarris Jul 11 '15 at 14:37
  • 1
    The trick part for me was discovering the "membership_set" name. I had a through model with no explicit "related" name, so I had to guess the name of it, by reading the docs at [Django Many to Many](https://docs.djangoproject.com/en/1.9/topics/db/examples/many_to_many/). – miceno Jan 25 '16 at 19:19
  • this works great, thanks for the hint. I think however DRF in this case is somewhat counterintuitive because class Member already defines a m2m field called groups and this solution seems to override the field in the serialiser by forcing it to point to the reverse relationship from the through model. I'm not very much into DRF implementation details, but probably with model introspection it could be handed automatically. just some food for thought :) – gru Apr 08 '16 at 09:10
  • Any case you could update us on whether this works with the latest version of DRF? Or at least tell which version you were using? I can not make DRF to return the through field model - it always ends up with the original relation (instead of Membership - it would always return Group). – Andrey Cizov Oct 17 '16 at 11:46
  • 1
    @AndreyCizov I'm using DRF 3.5.3 and the above solution worked. However, I now can't figure out how to make the serializers "updateable", e.g. create a Group with a bunch of a Members via the Group serializer. The data doesn't even come through since we're using ReadOnlyField(). – getup8 Dec 28 '16 at 07:01
  • My reference every single time I need to write code for many to many relations. Life saver. – SMir Feb 26 '19 at 06:47
  • Thank You sooooo much – Mahdi Bahari Aug 26 '20 at 15:50
  • 1
    this is great but the performance is very bad and if you inspect SQL queries there are so many similar and duplicate queries. Anyone knows how to improve this? I think it should be done using `prefetch_related` but I don't know how to use it in this context – Amin_mmz Oct 08 '20 at 11:14
  • @Amin_mmz did you end up finding a way to decrease the queries here? Huge amount of queries for me as well, would like to run some kind of prefetch related.. – Thorvald Nov 25 '21 at 10:39
  • 1
    One gotcha is that if you had a `MyMembership` model/serializer then the default source name is `mymembership_set` and NOT my_membership_set. Hopefully I save someone the hour(s) that I spent on that... – Bradleo Dec 14 '21 at 15:48
28

I was facing this problem and my solution (using DRF 3.6) was to use SerializerMethodField on the object and explicitly query the Membership table like so:

class MembershipSerializer(serializers.ModelSerializer):
    """Used as a nested serializer by MemberSerializer"""
    class Meta:
        model = Membership
        fields = ('id','group','join_date')

class MemberSerializer(serializers.ModelSerializer):
    groups = serializers.SerializerMethodField()

    class Meta:
        model = Member
        fields = ('id','name','groups')

    def get_groups(self, obj):
        "obj is a Member instance. Returns list of dicts"""
        qset = Membership.objects.filter(member=obj)
        return [MembershipSerializer(m).data for m in qset]

This will return a list of dicts for the groups key where each dict is serialized from the MembershipSerializer. To make it writable, you can define your own create/update method inside the MemberSerializer where you iterate over the input data and explicitly create or update Membership model instances.

FriC
  • 786
  • 10
  • 14
  • I don't understand how one would use update on this. SerializerMethodField is read only after all, so how would one access the validate data from it? It returns an empty dict if one tries. Can you please elaborate? @FariaC – Thorvald Jan 08 '21 at 17:56
  • I am learning, but I could send data that is not in the field of Meta class, and access it on the create functions. You can use request.data.get(somejsonkey) where 'somejsonkey' is the field sent to the server. – MarcoFerreira Aug 17 '22 at 14:21
0

I just had the same problem and I ended it up solving it with an annotation on the group queryset.

from django.db.models import F    

class MemberSerializer(ModelSerializer):
    groups = serializers.SerializerMethodField()
    class Meta:
        model = Member
    def get_groups(self, instance):
        groups = instance.groups.all().annotate(join_date=F(membership__join_date))
        return GroupSerializer(groups, many=True).data

    
class GroupSerializer(ModelSerializer):
    join_date = serializers.CharField(required=False) # so the serializer still works without annotation
    class Meta:
        model = Group
        fields = ..., 'join_date']
shmn
  • 26
  • 2
-6

NOTE: As a Software Engineer, I love to use Architectures and I have deeply worked on Layered Approach for Development so I am gonna be Answering it with Respect to Tiers.

As i understood the Issue, Here's the Solution models.py

class Member(models.Model):
    member_id = models.AutoField(primary_key=True)
    member_name = models.CharField(max_length = 

class Group(models.Model):
    group_id = models.AutoField(primary_key=True)
    group_name = models.CharField(max_length = 20)
    fk_member_id = models.ForeignKey('Member', models.DO_NOTHING, 
                             db_column='fk_member_id', blank=True, null=True)

class Membership(models.Model):
    membershipid = models.AutoField(primary_key=True)
    fk_group_id = models.ForeignKey('Group', models.DO_NOTHING, 
                             db_column='fk_member_id', blank=True, null=True)
    join_date = models.DateTimeField()

serializers.py

import serializer

class AllSerializer(serializer.Serializer):
    group_id = serializer.IntegerField()
    group_name = serializer.CharField(max_length = 20)
    join_date = serializer.DateTimeField()

CustomModels.py

imports...

    class AllDataModel():
        group_id = ""
        group_name = ""
        join_date = ""

BusinessLogic.py

imports ....
class getdata(memberid):
    alldataDict = {}
    dto = []
    Member = models.Members.objects.get(member_id=memberid) #or use filter for Name
    alldataDict["MemberId"] = Member.member_id
    alldataDict["MemberName"] = Member.member_name
    Groups = models.Group.objects.filter(fk_member_id=Member)
    for item in Groups:
        Custommodel = CustomModels.AllDataModel()
        Custommodel.group_id = item.group_id
        Custommodel.group_name = item.group_name
        Membership = models.Membership.objects.get(fk_group_id=item.group_id)
        Custommodel.join_date = Membership.join_date
        dto.append(Custommodel)
    serializer = AllSerializer(dto,many=True)
    alldataDict.update(serializer.data)
    return alldataDict

You would technically, have to pass the Request to DataAccessLayer which would return the Filtered Objects from Data Access Layer but as I have to Answer the Question in a Fast Manner so i adjusted the Code in Business Logic Layer!

Syed Faizan
  • 958
  • 9
  • 19
  • 1
    This is a Full Customized Approach which I use for most of my Rest API developments as I am not really a fan of work with Bounds even though Django Rest Framework is quite flexible! – Syed Faizan Aug 02 '18 at 18:00
  • 4
    This is waaaay too over engineered, also it doesn't even use DRF. – michauwilliam Jan 26 '19 at 17:05