Opened 5 years ago

Last modified 5 years ago

#30976 closed Bug

ManyToManyField does not create JOIN when target object contains a ForeignKey with the same name as the default reverse_query_name — at Initial Version

Reported by: Ben Davis Owned by: nobody
Component: Database layer (models, ORM) Version: 2.2
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Consider the following models. There is an Entity model which can be one of two types, a USER or a GROUP. There are User and UserGroup models that represent the sub-types, each having a OneToOneField pointing back to Entity (basically model inheritance w/o subclassing).

An entity (which could be a user or a group) can exist another group (so, a user can belong to a group, and a group can also belong to a group)

class Entity(models.Model):
    entity_id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=128)
    type = models.CharField(max_length=32, choices=(
        ('USER', 'User'),
        ('USER_GROUP', 'User group')
    ))
    groups = models.ManyToManyField(
        'UserGroup',
        through='UserGroupMember',
        through_fields=('member_entity', 'user_group')
    )

    class Meta:
        managed = False
        db_table = 'entity'
        unique_together = (('type', 'name'),)


class User(models.Model):
    user_id = models.AutoField(primary_key=True)
    entity = models.OneToOneField(Entity, models.DO_NOTHING, unique=True)
    email = models.EmailField()

    class Meta:
        managed = False
        db_table = 'user'


class UserGroup(models.Model):
    user_group_id = models.AutoField(primary_key=True)
    entity = models.OneToOneField(Entity, models.DO_NOTHING, unique=True, related_name='group')
    disabled = models.BooleanField()

    class Meta:
        managed = False
        db_table = 'user_group'

    def __str__(self):
        return self.entity.name


class UserGroupMember(models.Model):
    user_group = models.ForeignKey(UserGroup, models.DO_NOTHING)
    member_entity = models.ForeignKey(Entity, models.DO_NOTHING)

    class Meta:
        managed = False
        db_table = 'user_group_member'
        unique_together = (('user_group', 'member_entity'),)

    def __str__(self):
        print(f'{self.user_group}->{self.member_entity}')

When I make the following query, it does not produce the expected join:

>>> entity = Entity.objects.get(pk=2)
>>> print(entity.groups.all().query)
SELECT "user_group"."user_group_id", "user_group"."entity_id", "user_group"."disabled" FROM "user_group" WHERE "user_group"."entity_id" = 2

This happens because the field accessor creates this implicit filter from the target model: UserGroup.objects.filter(entity__entity_id=2). It assumes the default reverse name, which happens to conflict with the entity field on the target model.

A workaround is to simply supply the reverse_query_name argument on the ManyToManyField with something other than "entity". However, it took me quite a while to figure out what was happening here.

Django already warns about conflicting reverse names. It seems that there should also be a warning for this case.

Change History (0)

Note: See TracTickets for help on using tickets.
Back to Top