Code

Opened 6 years ago

Closed 6 years ago

Last modified 3 years ago

#8279 closed (fixed)

Models with multiple ManyToManyField to 'self' use the last table for every m2m

Reported by: Alberto García Hierro <fiam@…> Owned by: mtredinnick
Component: Database layer (models, ORM) Version: master
Severity: Keywords:
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: UI/UX:

Description (last modified by mtredinnick)

class Entry(models.Model):
    more_fields
    related = models.ManyToManyField('self', verbose_name=_('related entries'))
    translations = models.ManyToManyField('self', verbose_name=_('translations'))

From the database (postgres):

blango=# select * from blango_entry_related;
 id | from_entry_id | to_entry_id 
----+---------------+-------------
  1 |            27 |          29
  2 |            29 |          27
  3 |            27 |          30
  4 |            30 |          27
  5 |            27 |          38
  6 |            38 |          27
(6 rows)

blango=# select * from blango_entry_translations;
 id | from_entry_id | to_entry_id 
----+---------------+-------------
  3 |             4 |           3
  4 |             3 |           4
  7 |             8 |           7
  8 |             7 |           8
 11 |            13 |          12
 12 |            12 |          13
(6 rows)

blango=# 

And from ./manage.py shell:

In [2]: from blango.models import *;Entry.objects.get(pk=27).related.all()
Out[2]: []
In [3]: from blango.models import *;Entry.objects.get(pk=27).translations.all()
Out[3]: []

The database query log indicates that Django is using the wrong table:

2008-08-12 22:38:10 CEST LOG:  statement: SELECT "blango_entry"."id", "blango_entry"."title", "blango_entry"."slug", "blango_entry"."author_id", "blango_entry"."language_id", "blango_entry"."body", "blango_entry"."body_html", "blango_entry"."pub_date", "blango_entry"."draft", "blango_entry"."allow_comments" FROM "blango_entry" INNER JOIN "blango_entry_translations" ON ("blango_entry"."id" = "blango_entry_translations"."to_entry_id") WHERE "blango_entry_translations"."from_entry_id" = 27 

If I change the order of the m2m in the model to:

class Entry(models.Model):
   more_fields
   translations = models.ManyToManyField('self', verbose_name=_('translations'))
   related = models.ManyToManyField('self', verbose_name=_('related entries'))

Then I get all the related entries as related and as translations.

If it serves as help, I've added a print statement in django/db/models/fields/related.py at line 776:

...
775         else:
776             print '%s_%s' % (opts.db_table, self.name)
777             return '%s_%s' % (opts.db_table, self.name)
...

Which prints:

In [1]: from blango.models import *;Entry.objects.get(pk=27).related.all()
blango_entry_related
blango_entry_translations
Out[1]: []

In [2]: from blango.models import *;Entry.objects.get(pk=27).related.all()
blango_entry_related
Out[2]: []

Attachments (0)

Change History (10)

comment:1 Changed 6 years ago by mtredinnick

  • milestone set to 1.0
  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset
  • Triage Stage changed from Unreviewed to Accepted

comment:2 Changed 6 years ago by Alberto García Hierro <fiam@…>

I've added some debug print statements, printing the join_table for the RelatedManager and the sql generated by the query. Here are the results:

In [6]: t1.related2.all()

"testapp_testmodel_related2"

('SELECT "testapp_testmodel"."id", "testapp_testmodel"."value" FROM "testapp_testmodel" INNER JOIN "testapp_testmodel_related3" ON ("testapp_testmodel"."id" = "testapp_testmodel_related3"."to_testmodel_id") WHERE "testapp_testmodel_related3"."from_testmodel_id" = %s ', (1,))

So join_table is correctly passed to the manager, but somewhere it overrides (or ignores it) an ends up using testapp_testmodel_related3 as the table for join (while it should use testapp_testmodel_related2).

comment:3 Changed 6 years ago by Alberto García Hierro <fiam@…>

I forgot to add that t1.related2.add() and t1.related2.remove() work as expected. They write to the testapp_testmodel_related2 table.

comment:4 Changed 6 years ago by metajack

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

comment:5 in reply to: ↑ description Changed 6 years ago by metajack

After some digging, it looks like a workaround for this is to set related_name.

Options.init_name_map() iterates through the m2m related fields, but f.field.related_query_name() is always 'entry'. This overwrites the other 'entry' key in the cache object, which is why it always joins on the final table.

comment:6 Changed 6 years ago by metajack

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

I was unsuccessful trying to backtrack through the code to figure out how to pass in a more descriptive search key. I'll let someone else more familiar with the code take it from here.

comment:7 Changed 6 years ago by mtredinnick

  • Description modified (diff)

(Fixed description formatting).

comment:8 Changed 6 years ago by mtredinnick

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

comment:9 Changed 6 years ago by mtredinnick

  • Resolution set to fixed
  • Status changed from assigned to closed

(In [8721]) Fixed #8279 -- Multiple many-to-many relations to "self" are now possible.

comment:10 Changed 3 years ago by jacob

  • milestone 1.0 deleted

Milestone 1.0 deleted

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as closed
as The resolution will be set. Next status will be 'closed'
The resolution will be deleted. Next status will be 'new'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.