Opened 5 years ago
Closed 5 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.