How can I efficiently retrieve a users feed in a social media app with Django

136 views Asked by At

I am new to programming and new to Django and I am trying to design my first social media app. I wanted to see if theres an efficient way of retrieving a users feed that would result in quick retrieval of the users feed without being intensive on the database. I want to learn what the best practices are to follow to construct efficient API views.

Provided below are my current User model, Follow model, and API view to retrieve a users feed. I was wondering what improvements I could make to my current implementation so I could learn more about Django and how to construct an efficient app.

Currently, my API view retrieves all of the users the requesting user is following and then retrieves Posts made by those users. However if the user is following a lot of people, the query to get a list of all the users the requesting user is following might be inefficient, at least I think so. Would there be a better implementation to perform so I can get the users feed that would not be intensive in terms of database retrieval?

I have the following User model:

class User(AbstractUser):
# Additional fields for user profiles
profile_picture = models.ImageField(upload_to='profiles/', null=True, blank=True)  # Profile picture for the user
bio = models.TextField(max_length=300, blank=True)  # Short bio or description for the user
contact_information = models.CharField(max_length=100, blank=True)  # Contact information for the user
profile_privacy = models.CharField(max_length=10, choices=[('public', 'Public'), ('private', 'Private')], default='public')  # Privacy setting for user profile
num_followers = models.PositiveIntegerField(default=0)  # counter to keep track of users num of followers
num_following = models.PositiveIntegerField(default=0)  # counter to keep track of num of users a user is following
num_posts = models.PositiveIntegerField(default=0)  # counter to keep track of num of posts made by the user

and I have a Follow model:

class Follow(models.Model):
FOLLOW_STATUS_CHOICES = [
    ('pending', 'Pending'),
    ('accepted', 'Accepted'),
]

follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name='following')  # ForeignKey User that is following another User
following = models.ForeignKey(User, on_delete=models.CASCADE, related_name='follower')  # ForeignKey User that is being followed by another User
follow_status = models.CharField(max_length=10, choices=FOLLOW_STATUS_CHOICES, default='pending')

class Meta:
    unique_together = ('follower', 'following')  # Ensure unique follower-following pairs
    indexes = [
        models.Index(fields=['follower', 'following', 'follow_status']),  # Combined index
    ]

Below is my current implemtation for API view to retrieve a Users feed:

# API view to get posts from the users that the current user follows
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def user_feed(request):
    # Obtains all the users the requesting user is following
    following_users = User.objects.filter(follower__follower=request.user, follower__follow_status='accepted')

    # Set a default page size of 20 returned datasets per page
    default_page_size = 20
    # Utility function to get current page number and page size from the request's query parameters and calculate the pagination slicing indeces
    start_index, end_index, validation_response = get_pagination_indeces(request, default_page_size)
    if validation_response:
        return validation_response

    # fetch the posts from the users in following_users
    feed_posts = Post.objects.filter(user__in=following_users)[start_index:end_index]

    # The context is used to pass the request to the PostSerializer to perform custom logic
    serializer = PostSerializer(feed_posts, many=True, context={'request': request})

    return Response(serializer.data, status=status.HTTP_200_OK)

My current approach works but I was just wondering if it could be better

1

There are 1 answers

3
willeM_ Van Onsem On

There is no need to retrieve following_users first, you can filter directly with:

feed_posts = Post.objects.filter(
    user__follower__follower=request.user, 
    user__follower__follow_status='accepted'
)

It is probably also not a good idea to implement pagination yourself, or instantiate the serializer yourself. This can all be done with a ListAPIView [drf-doc] which has been tested more extensively, and thus is less likely to contain bugs:

class UserFeedListAPIView(ListAPIView):
    permission_classes = [IsAuthenticated]
    serializer_class = PostSerializer
    queryset = Post.objects.all()

    def get_queryset(self):
        return (
            super()
            .get_queryset()
            .filter(
                user__follower__follower=request.user,
                user__follower__follow_status='accepted',
            )
        )

You can then inject a custom paginator, but the settings can also be applied over all views, which is likely a better idea, since that introduces uniformity. You can read more about this in the pagination section of the Django Rest Framework documentation. Paginators will for example also order the queryset if it is not ordered. In that case for example by a primary key. This is necessary, otherwise querying the same page could produce different results, so the "next page" would not per se be a next page.


Note: Your Follow model acts as a junction table for a many-to-many relation between User and User. You can span a ManyToManyField [Django-doc] on the User model with:

class User(models.Model):
    # …
    followers = models.ManyToManyField(
        'User',
        through='Follow',
        through_fields=('following', 'follower')
    )

Note: You do not have to store the number of items of a ManyToManyField in another field. You can use .annotate(…) [Django-doc] when you need to determine this by the database. Storing this explicitly in a field is a form of data duplication, and it turns out that keeping these in sync is harder than what one might expect.


Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.