Petar
Petar

Reputation: 33

Need architectural advice on how to handle and/or avoid circular imports when using Django/DRF

I would like advice regarding an architectural problem I've encountered many times now. I have a Model Event in events.py:

# models.py
import events.serializers

class Event(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=255, db_index=True)
    ...

    def save(self, *args, **kwargs):
        is_new = not self.pk
        super().save(*args, **kwargs)
        if is_new:
            self.notify_organiser_followers()

    def notify_organiser_followers(self):
        if self.organiser:
            event_data = events.serializers.BaseEventSerializer(self).data
            payload = {'title': f'New event by {self.organiser.name}',
                       'body': f'{self.organiser.name} has just created a new event: {self.name}',
                       'data': {'event': event_data}}
            send_fcm_messages(self.organiser.followers.all(), payload)

The model has a serializer called BaseEventSerializer. In the save method, I use notify_organiser_followers and in the process serialize the current event being saved. To do this, I have to import BaseEventSerializer.

Here's how the code of events.serializers looks:

# serializers.py
import events.models

class EventTrackSerializer(serializers.ModelSerializer):
    class Meta:
        model = events.models.EventTrack
        fields = ('id', 'name', 'color')


class BaseEventSerializer(serializers.ModelSerializer):
    event_type = serializers.CharField(source='get_event_type_display')
    locations = serializers.SerializerMethodField()

As you can see, serializers.py has to import models to use them in ModelSerializers. In this step, I end up with an obvious circular import.

The way I solved this was by importing BaseEventSerializer locally in the notify_organiser_followers function:

def notify_organiser_followers(self):
    if self.organiser:
        from events.serializers import BaseEventSerializer

This eliminates the issue, but I would really like avoiding this, especially because I would have to do the same fix on multiple spots in my repo. Another approach I thought of would be to separate 'regular' serializers and model serializers into separate files. However, this still feels like it's only healing the symptom and not the cause.

What I would like is advice on how to avoid this situation altogether. I had the same problem when importing two different apps that use each other's serializer. E.g. User serializes Events he's attending, Event serializes its attendees. How would you go about decoupling these two models? It seems that model relationships often force me into these situations and avoiding circular imports becomes really hard.

I would also appreciate if you had any larger Django/DRF Github projects showcasing how this was avoided, since this issue keeps popping up for me as soon as my application gets large enough.

Upvotes: 1

Views: 160

Answers (1)

rob
rob

Reputation: 2144

There are a few different ways to do this, and likely different combinations of strategies.

The two I would recommend here is using django signals, and creating a service class.

First, use django signals to trigger specific actions after a Models event. Django has built in listeners for when things like save happens on a model, and you can perform different things based on this. This allows you to decouple your event layer from your model.

For the singleton piece: I think a Models method should not be performing actions that are not related to itself. I think it would be best to create some kind of generic service as a singleton, something like

EventService(): 
   def do_event_related_logic(self, event):
       ...

event_service = EventService()

and you can simply import the event_service, and use that class for all event related logic. In this case, you could call one of your singleton's methods on a post_save event.

Upvotes: 1

Related Questions