Opened 4 years ago
Closed 4 years ago
#32484 closed Bug (needsinfo)
Can't access inherited fields from multi-table inherited model when using apps from MigrationExecutor
Reported by: | Max N | Owned by: | nobody |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 3.1 |
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
I came across an issue when using the apps
value from the MigrationExecutor
. If I use this apps.get_model
I can't use fields from a multi-table inherited model.
I have two applications (api
and labels
), with two models (DocumentLabel
and Label
respectively). DocumentLabel
uses multi-table inheritance from Label
. If I use the MigrationExecutor
to load a migration, and then use apps.get_model
from that state (executor.loader.project_state(migrate_from).apps
) then the returned model from get_model
of DocumentLabel
behaves incorrectly, it doesn't allow me to reference the fields from the inherited Label
model. It also throws errors if I try to access the label_ptr
field.
If I use apps
from django.apps
it works as expected.
For example, api.models
contains:
class DocumentLabel(LabelsLabel): """Label for organising documents.""" assigned_to = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL ) workspace = models.ForeignKey("Workspace", on_delete=models.SET_NULL, null=True) class Meta: constraints: List[BaseConstraint] = []
The DocumentLabel
inherits from the Label
class from another app labels.models
:
class Label(CreatedAtUpdatedAt): # type: ignore """The label model should be used as default""" name = models.TextField() type = models.ForeignKey( f"{LABEL_TYPE_MODEL_APP}.{LABEL_TYPE_MODEL_NAME}", on_delete=models.CASCADE, ) class Meta: constraints = [ UniqueConstraint( fields=["name", "type"], name="%(app_label)s_label_unique_name_and_type", ) ]
I am testing a migration, so I am using the MigrationExecutor to go to a certain migration, it does not change the models in question. When I try to access the type
field of the Label
model through the DocumentLabel
model, something that is possible usually, I get an exception saying that type
is not set on the DocumentLabel
model. The code:
executor = MigrationExecutor(connection) old_apps = executor.loader.project_state(migrate_from).apps executor.migrate(migrate_from) LabelType = old_apps.get_model("api", "LabelType") DocumentLabel = old_apps.get_model("api", "DocumentLabel") project = LabelType.objects.create(name="Project") label = DocumentLabel.objects.create(type=project, name="test",workspace=workspace)
It throws the following:
Traceback (most recent call last): File "/opt/project/api/tests/test_migrations.py", line 131, in test_projects_data_migration label_1 = DocumentLabel.objects.create(type=project, name="test",workspace=workspace) File "/usr/local/lib/python3.8/site-packages/django/db/models/manager.py", line 85, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "/usr/local/lib/python3.8/site-packages/django/db/models/query.py", line 445, in create obj = self.model(**kwargs) File "/usr/local/lib/python3.8/site-packages/django/db/models/base.py", line 501, in __init__ raise TypeError("%s() got an unexpected keyword argument '%s'" % (cls.__name__, kwarg)) TypeError: DocumentLabel() got an unexpected keyword argument 'type'
Interestingly, LabelType
is also a model that is using multi-table inheritance from the same application, but that works fine. The only difference I can think of is that Label
has a ForeignKey
field in it, but the inherited LabelType
field has no foreign key.
I also tried to directly set the label_ptr
:
LabelType = old_apps.get_model("api", "LabelType") Label = old_apps.get_model("label", "Label") DocumentLabel = old_apps.get_model("api", "DocumentLabel") project = LabelType.objects.create(project=True, name="Project", workspace=workspace, allow_multiple=True) label_label_1 = Label.objects.create(name="test", type=project) label_1 = DocumentLabel.objects.create(label_ptr=label_label_1, workspace=workspace) for label in DocumentLabel.objects.all(): print(label.label_ptr)
It throws the exception:
Traceback (most recent call last): File "/usr/local/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 173, in __get__ rel_obj = self.field.get_cached_value(instance) File "/usr/local/lib/python3.8/site-packages/django/db/models/fields/mixins.py", line 15, in get_cached_value return instance._state.fields_cache[cache_name] KeyError: 'label_ptr' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/opt/project/api/tests/test_migrations.py", line 136, in test_projects_data_migration print(label.label_ptr) File "/usr/local/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 187, in __get__ rel_obj = self.get_object(instance) File "/usr/local/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 302, in get_object kwargs = {field: getattr(instance, field) for field in fields} File "/usr/local/lib/python3.8/site-packages/django/db/models/fields/related_descriptors.py", line 302, in <dictcomp> kwargs = {field: getattr(instance, field) for field in fields} AttributeError: 'DocumentLabel' object has no attribute 'id'
I looked into the model at that point, and the label_ptr
is not found. I also looked into the code of ForwardManyToOneDescriptor
and found that get_cached_value
throws a KeyError
exception, which triggers some logic. I tried to 'pre-cache' the related model by doing the following:
DocumentLabel.objects.all().prefetch_related("label_ptr")
This actually causes the label_ptr
to be set, but it doesn't allow the 'transparent' ability to query Label
fields as if they are part of the DocumentLabel
model.
Hello. I need to ask you to provide a sample project with exact steps here to have a chance to reproduce this. There's simply not enough detail otherwise. Sorry.
The only thing that catches my eye is this:
I'd suspect you'd want the project state after the migration… — but as I say, without being able to look in depth, it's not really possible to say.
Thanks.