P4nd4b0b3r1n0
P4nd4b0b3r1n0

Reputation: 2411

In Django, how to keep many-to-many relations in sync?

What's the best way, in Django, to set and keep up-to-date a many-to-many field that is (for a lack of a better term) a composite of many-to-many fields from other models?

To give a concrete example, I have a Model representing a Resume.

class Resume(models.Model):
    owner = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="resume",
    )
    description = models.TextField(default="Resume description")
    skills = models.ManyToManyField(Skill, related_name="resume")

The Resume object is referenced by another model called WorkExperience:

class WorkExperience(models.Model):
    ...
    skills = models.ManyToManyField(Skill, related_name="work")
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        null=False,
        default=1,
        related_name="work_experience",
    )
    resume = models.ForeignKey(
        Resume,
        on_delete=models.CASCADE,
        null=False,
        default=1,
        related_name="work_experience",
    )

Notice that there's a fair amount of redundancy here, with both Resume and WorkExperience pointing to the same owner etc. That said, both of these Models (Resume & WorkExperience) have a field called Skills. That reference another Model called Skills.

What I'd like is to have the Resume skills to point to the same skills as the ones in WorkExperience. Any suggestions on how to do this? I also have a Model called EducationExperience which also references Skills and has a foreign key relation to Resume. Is there any way to keep the skills in Resume be in sync with both the skills in WorkExperience and EducationExperience?

A straightforward option would be to implement a method called set_resume_skills which would, when called, add the skills they have to the list of skills that Resume has

Class WorkExperience(models.Model):
    # Same model as above
    def set_resume_skills(self):
        self.resume.skills.add(self.skills)

The issue I have with this approach is that it only adds skills, it doesn't really keep them in sync. So if I remove a skill from WorkExperience it won't be removed from Resume. Another boundary condition would be that if multiple WorkExperience objects are referencing a skill and then I remove the skill from one Object how would I make sure that the reference in Resume is still intact? I.e., two work experience objects refer to the Skill "Javascript". I remove the Skill from one WorkExperience object. The Skill "Javascript" should still be referenced by the Resume because one WorkExperience object still has a reference to it.

Edit: The reason I want to do it like this is to reduce the amount of querying done on the front-end. If the only way to filter the skills are through the the "sub-models" (WorkExperience, EducationExperience), I'd need to do two queries in my front-end instead of one. Although now that I think about it, doing two queries isn't that bad.

Upvotes: 1

Views: 251

Answers (2)

Muhammad Arsalan
Muhammad Arsalan

Reputation: 375

By your design, it seems that Skills actually belong to owner i.e the AUTH_USER. By having its M2M relation in Resume & other models will definitely cause redundancy. Why don't you just create a M2M of Skills in User model?

The point about reducing queries, there are other ways to do this as well. By using select_related or prefetch_related

class User(models.Model):
skills = models.ManyToManyField(Skill, related_name="resume")
# ... Other fields here

class Resume(models.Model):
owner = models.OneToOneField(
    settings.AUTH_USER_MODEL,
    on_delete=models.CASCADE,
    related_name="resume",
)
description = models.TextField(default="Resume description")
# ... Other fields here

Upvotes: 2

P4nd4b0b3r1n0
P4nd4b0b3r1n0

Reputation: 2411

I was approaching the problem from the wrong end. I don't have to worry about keeping the Skills in check when I just do a reverse query on the Skill objects. Currently I'm filtering the Skill QuerySet in my view like this:

class SkillViewSet(viewsets.ModelViewSet):
    serializer_class = SkillSerializer

    def get_queryset(self):
        queryset = Skill.objects.all()
        email = self.request.query_params.get("email", None)
        if email:
            User = get_user_model()
            user = User.objects.get(email=email)
            query_work_experience = Q(work__owner=user)
            query_education_experience = Q(education__owner=user)
            queryset = queryset.filter(
                query_work_experience | query_education_experience
            )

        return queryset

This removes the need to keep the Skills in sync with the Resume Model.

Upvotes: 0

Related Questions