Opened 5 years ago

Last modified 5 days ago

#17002 assigned Bug

ManyToManyField through a model which extends some other model

Reported by: mitar Owned by: InvalidInterrupt
Component: Database layer (models, ORM) Version: master
Severity: Normal Keywords:
Cc: mmitar@…, gm.lawr@… Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

It seems that currently it is impossible to create a ManyToManyField through a model which extends some other model. Django assumes that all fields are accessible in the given model and does not traverse the model hierarchy. This means that it is impossible to use inheritance to prevent data (and code) duplication and have a normalized database.

Change History (10)

comment:1 Changed 5 years ago by sebastian

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset
  • Triage Stage changed from Unreviewed to Design decision needed
  • Type changed from New feature to Bug
  • Version changed from 1.3 to SVN

Django expects the foreign key attributes to be present in the intermediary model given as the through= argument (and not some non-abstract parent) when constructing the related queries. On the other hand, model validation and table creation works even when the foreign key attributes are in the base class, or split over base and derived class as in e.g.

class A(models.Model):
    pass
class B(models.Model):
    a_s = models.ManyToManyField(A, through='Derived')

class Base(models.Model):
    a = models.ForeignKey(A)
class Derived(Base):
    b = models.ForeignKey(B)

The example validates just fine and even generates the appropriate database structure, but related queries fail during runtime, e.g. B.objects.get(pk=1).a_s.all() throws "DatabaseError: no such column: app_derived.a_id".

Can you give an example where foreign keys in the base class of the intermediary model (as opposed to arbitrary attributes, or methods) are necessary or would be useful?

In any case, I think the behavior here should be consistent. If the query cannot be created (because attributes are expected to be present in the derived table), then the validation of the model should not succeed. Or otherwise, the query should take attributes imported from base classes into account and insert the necessary joins.

comment:2 Changed 5 years ago by mitar

The concrete example where we discovered this is when we wanted to make a schema other users could extend. For exmaple, we have defined something like;

class Node(models.Model):
    children = models.ManyToManyField(Child, through='Link')

class Child(models.Model):
    pass

class Link(models.Model):
    source = models.ForeignKey(Node)
    destination = models.ForeignKey(Child)

And then we wanted to provide users a way to extend our schema with their own applications/modules. So that they can instead derive their own schema, like:

class BetterNode(Node):
    better_children = models.ManyToManyField(Child, through='BetterLink')

class BetterLink(Link):
    weight = models.IntegerField()

And then old applications could still use the old fields, while users' extended applications could know how to handle this additional fields.

comment:3 Changed 3 years ago by Alex

  • Triage Stage changed from Design decision needed to Accepted

Yeah this is a bug (at the very minimum a docs one, although I think it's definitely a "true" bug), I'm not sure how much of a pain it'll be to fix off hand.

comment:4 Changed 12 days ago by InvalidInterrupt

  • Cc gm.lawr@… added
  • Version changed from master to 1.9

I recently encountered this bug on a project I am working on.

I think that we may be able to fix the issue by changing django.db.models.fields.related.ManyToManyField._get_path_info to use some logic from django.db.models.sql.query.Query.names_to_path (lines 1334-1349 in commit 698be78). To keep things maintainable, I'd like to split that logic out into a separate function (e.g. get_path_to_parent_model(model, parent)). Could someone with more familiarity with the codebase please suggest an appropriate place for this function to live?

comment:5 Changed 12 days ago by timgraham

Possibly django/db/models/query_utils.py, but it'll be easier to tell upon seeing the pull request so don't worry too much about it now.

comment:6 Changed 12 days ago by InvalidInterrupt

  • Owner changed from nobody to InvalidInterrupt
  • Status changed from new to assigned

comment:7 Changed 9 days ago by InvalidInterrupt

  • Has patch set
  • Patch needs improvement set
  • Version changed from 1.9 to master

Patch work is occurring on my topic branch at https://github.com/InvalidInterrupt/django/tree/ticket_17002. I need to add tests to confirm proxy models are handled correctly.

comment:8 Changed 9 days ago by InvalidInterrupt

  • Patch needs improvement unset

I've run the test suite for the py3, py2, and py3-posgtres environments successfully.

comment:9 Changed 6 days ago by InvalidInterrupt

  • Patch needs improvement set

Found that prefetch_related is also broken.

comment:10 Changed 5 days ago by InvalidInterrupt

  • Patch needs improvement unset
Note: See TracTickets for help on using tickets.
Back to Top