Opened 14 months ago
Last modified 8 days ago
#35453 new Bug
ManyToMany field incorrectly identified as a concrete field on the defining side.
Reported by: | Harro | Owned by: | nobody |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 5.0 |
Severity: | Normal | Keywords: | |
Cc: | Mariusz Felisiak, Simon Charette, Ryan P Kilby | Triage Stage: | Unreviewed |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
Was looking at some relationship bugs in: https://github.com/bmispelon/django-model-subquery
And when trying to fix it ran into the following problem:
ManyToMany fields are concrete and have a column on the defining side.
Take the User in django, if I do:
[f.column for f in User._meta.get_fields() if f.concrete]
You see groups
in there, but it's a ManyToMany so there is no actual column called groups in the user table.
Then I dove into django and actually see django itself use local_concrete_fields
internally a lot, which has all the actual fields that have a column.
Shouldn't the ManyToMany basically not be a concrete field so it actually matches what you expect and what the docs say about the field property:
https://docs.djangoproject.com/en/5.0/ref/models/fields/#django.db.models.Field.concrete
Change History (7)
comment:1 by , 14 months ago
Cc: | added |
---|---|
Resolution: | → invalid |
Status: | new → closed |
follow-up: 4 comment:2 by , 14 months ago
Which column does a ManyToMany have then?
As far as I see it it has a table with has two (or more) columns pointing to two different tables, so why doesn't the other side of the ManyToMany not also have a column?
local_concrete_fields
is local_fields
filtered on the concrete flag, but I can't find where the local_fields is filled in the source code, but it does not contain the ManyToMany fields defined on the model (so far that's correct)
Also, the documentation doesn't mention the local_fields
nor the local_concrete_fields
so techically they shouldn't be used, but the get_field(s) api calls are documented, so I expect it to give me the information I need, as to which models are actually on a database table without extra weird conditions to exclude specifically one side of the ManyToMany relation.
comment:3 by , 14 months ago
I tried adding
def get_attname_column(self): attname, column = super().get_attname_column() return attname, None
to the ManyToMany field and it broke 7 tests that all seem to be related to migrations/schema, nothing else.
So somewhere internally django migrations depend on ManyToMany having a column, which isn't actually there at all.
comment:4 by , 14 months ago
Replying to Harro:
Also, the documentation doesn't mention the
local_fields
nor thelocal_concrete_fields
so techically they shouldn't be used, but the get_field(s) api calls are documented, so I expect it to give me the information I need, as to which models are actually on a database table without extra weird conditions to exclude specifically one side of the ManyToMany relation.
Yes, these are not documented. Previous work to document Model _meta API was done as part of #12663.
I don't think using local_fields
is a big risk and you could use this, maybe there is a case for it being documented.
comment:5 by , 14 months ago
Yeah, maybe that would solve it, get_fields seems to have some caching but still does a lot of processing, while local_fields is stored on the options and the others are cached properties that only loop through the fields once.
So performance wise it might be better to use them anyway.
comment:6 by , 8 days ago
Cc: | added |
---|---|
Resolution: | invalid |
Status: | closed → new |
Summary: | ManyToMany field is a concrete field on the defining side. → ManyToMany field incorrectly identified as a concrete field on the defining side. |
I believe this ticket should be reopened. Related to https://code.djangoproject.com/ticket/36481 and the discussion in https://github.com/django/django/pull/19595, @charettes pointed out that ManyToManyField
is likely incorrectly identified as concrete
and that it should be investigated.
comment:7 by , 8 days ago
FWIW the only tests failing when applying
-
django/db/models/fields/related.py
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index bad71a5fd6..fdb4e47700 100644
a b def db_type(self, connection): 2062 2062 2063 2063 def db_parameters(self, connection): 2064 2064 return {"type": None, "check": None} 2065 2066 def get_attname_column(self): 2067 attname, column = super().get_attname_column() 2068 return attname, None
Are migrations
related one mainly because we skip non-concrete fields alterations (see _field_should_be_altered
) which can be further addressed by
-
django/db/backends/base/schema.py
diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index cf9243ccf0..5057013c0e 100644
a b def execute(self, sql, params=()): 204 204 cursor.execute(sql, params) 205 205 206 206 def quote_name(self, name): 207 if name is None: 208 return name 207 209 return self.connection.ops.quote_name(name) 208 210 209 211 def table_sql(self, model): … … def _field_indexes_sql(self, model, field): 1655 1657 return output 1656 1658 1657 1659 def _field_should_be_altered(self, old_field, new_field, ignore=None): 1658 if not old_field.concrete and not new_field.concrete: 1660 if not ( 1661 old_field.concrete or old_field.is_relation and old_field.many_to_many 1662 ) and not ( 1663 new_field.concrete or new_field.is_relation and new_field.many_to_many 1664 ): 1659 1665 return False 1660 1666 ignore = ignore or set() 1661 1667 _, old_path, old_args, old_kwargs = old_field.deconstruct()
I think it's worth discussing what we want to do with non-concrete field alterations in general as it has some implications with composite foreign key support as we'll most likely have to make such foreign keys non-concrete as discussed ticket:35956#comment:13.
I think the docs could potentially be clearer but I don't think
ManyToManyField
should have concrete as False.I believe
local_concrete_fields
is when the column exists on the model's table (rather than on a different table) and so is "local".I think the docs are correct that
ManyToManyField
does have a database column just not locally. Examples of non-concrete fields includeGenericForeignKey
,GenericRelation
andForeignObject
.