Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to update values in instance when have a custom .update() to update many-to-many relations in DRF writable nested serializer

I have three models Player, Team, and Membership, Where Player and Team have many-to-many relationship using Membership as intermediary model.

class Player(models.Model):
    name = models.CharField(max_length=254)
    rating = models.FloatField(null=True)
    install_ts = models.DateTimeField(auto_now_add=True, blank=True)
    update_ts = models.DateTimeField(auto_now_add=True, blank=True)


class Team(models.Model):
    name = models.CharField(max_length=254)
    rating = models.FloatField(null=True)
    players = models.ManyToManyField(
            Player,
            through='Membership',
            through_fields=('team', 'player'))
    is_active = models.BooleanField(default=True)
    install_ts = models.DateTimeField(auto_now_add=True, blank=True)
    update_ts = models.DateTimeField(auto_now_add=True, blank=True)


class Membership(models.Model):
    team = models.ForeignKey('Team')
    player = models.ForeignKey('Player')
    #date_of_joining = models.DateTimeField()
    install_ts = models.DateTimeField(auto_now_add=True, blank=True)
    update_ts = models.DateTimeField(auto_now_add=True, blank=True)

Now I was required to update this membership using django rest framework. I tried update those using Writable nested serializers by writing a custom .update() of team serializer.

@transaction.atomic
def update(self, instance, validated_data):
    '''
    Cutomize the update function for the serializer to update the
    related_field values.
    '''

    if 'memberships' in validated_data:
        instance = self._update_membership(instance, validated_data)

        # remove memberships key from validated_data to use update method of
        # base serializer class to update model fields
        validated_data.pop('memberships', None)

    return super(TeamSerializer, self).update(instance, validated_data)


def _update_membership(self, instance, validated_data):
    '''
    Update membership data for a team.
    '''
    memberships = self.initial_data.get('memberships')
    if isinstance(membership, list) and len(memberships) >= 1:
        # make a set of incoming membership
        incoming_player_ids = set()

        try:
            for member in memberships:
                incoming_player_ids.add(member['id'])
        except:
            raise serializers.ValidationError(
                'id is required field in memberships objects.'
            )

        Membership.objects.filter(
            team_id=instance.id
        ).delete()

        # add merchant member mappings
        Membership.objects.bulk_create(
            [
                Membership(
                    team_id=instance.id,
                    player_id=player
                )
                for player in incoming_player_ids
            ]
        )
        return instance
    else:
        raise serializers.ValidationError(
                'memberships is not a list of objects'
            )

Now this works well for updating values in database for membership table. Only problem I am facing is that I am not able to update prefetched instance in memory, which on PATCH request to this API updates values in database but API response is showing outdated data.

Next GET request for the same resource gives updated data. Anyone who have worked with many-to-many relation in django and wrote custom update/create methods for writable nested serializers can help me to understand the possible way of solving this problem.

like image 722
Manjit Kumar Avatar asked Apr 04 '16 08:04

Manjit Kumar


People also ask

What is To_representation in Django?

The docs on using to_representation is somewhat short. This method is used by Django Rest Framework 3.0+ to change the representation of your data in an API.

What is SlugRelatedField?

SlugRelatedField. SlugRelatedField may be used to represent the target of the relationship using a field on the target. For example, the following serializer: class AlbumSerializer(serializers. ModelSerializer): tracks = serializers.


2 Answers

I think you have to "reload" the players field with instance.players.all(), because it the queryset is already evaluated in the is_valid method of the serializer.

@transaction.atomic
def update(self, instance, validated_data):
    '''
    Cutomize the update function for the serializer to update the
    related_field values.
    '''

    if 'memberships' in validated_data:
        instance = self._update_membership(instance, validated_data)

        # remove memberships key from validated_data to use update method of
        # base serializer class to update model fields
        validated_data.pop('memberships', None)
        instance.players.all()

    return super(TeamSerializer, self).update(instance, validated_data)
like image 84
ilse2005 Avatar answered Nov 15 '22 23:11

ilse2005


I was able to track down the issue in this case. I was using .prefetch_related() on queryset which is causing instance to use prefetched data for many to many relations. I have got two solution which may cause a few more database hits to fetch updated data.

1. Not using .prefetch_related()

One obvious way is not to use .prefetch_related() Which is not recommended as it will result into high number of DB hits.

OR

2. Fetching updated model instance after updating many-to-many relations in update method of serializer

instance = self.context['view'].get_queryset().get(
    id=instance.id
)

Modified .update() of TeamSerializer

@transaction.atomic
def update(self, instance, validated_data):
    '''
    Cutomize the update function for the serializer to update the
    related_field values.
    '''

    if 'memberships' in validated_data:
        instance = self._update_membership(instance, validated_data)

        # remove memberships key from validated_data to use update method of
        # base serializer class to update model fields
        validated_data.pop('memberships', None)

        # fetch updated model instance after updating many-to-many relations
        instance = self.context['view'].get_queryset().get(
            id=instance.id
        )
    return super(TeamSerializer, self).update(instance, validated_data)

If someone have better approach I would appreciate if they can add an answer to this question.

like image 43
Manjit Kumar Avatar answered Nov 16 '22 00:11

Manjit Kumar