Opened 5 years ago

Closed 5 years ago

#30976 closed Bug (invalid)

ManyToManyField doesn't JOIN when target object has a ForeignKey with the same name as a default query name.

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 (last modified by Ben Davis)

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 related_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 relation names. It seems that there should also be a warning for this case.

Change History (2)

comment:1 by Ben Davis, 5 years ago

Description: modified (diff)
Summary: ManyToManyField does not create JOIN when target object contains a ForeignKey with the same name as the default reverse_query_nameManyToManyField does not create JOIN when target object contains a ForeignKey with the same name as the default related_query_name

comment:2 by Mariusz Felisiak, 5 years ago

Resolution: invalid
Status: newclosed
Summary: ManyToManyField does not create JOIN when target object contains a ForeignKey with the same name as the default related_query_nameManyToManyField doesn't JOIN when target object has a ForeignKey with the same name as a default query name.

Django 1.10+ raises errors in such case (see 85ef98dc6ec565b1add417bd76808664e7318026):

ERRORS:
test_one.Entity.groups: (fields.E303) Reverse query name for 'Entity.groups' clashes with field name 'UserGroup.entity'.
        HINT: Rename field 'UserGroup.entity', or add/change a related_name argument to the definition for field 'Entity.groups'.
Note: See TracTickets for help on using tickets.
Back to Top