Как вести таблицу всех прокси-моделей в Django?
У меня есть модель A и я хочу создать ее подклассы.
class A(models.Model):
type = models.ForeignKey(Type)
data = models.JSONField()
def compute():
pass
class B(A):
def compute():
df = self.go_get_data()
self.data = self.process(df)
class C(A):
def compute():
df = self.go_get_other_data()
self.data = self.process_another_way(df)
# ... other subclasses of A
B и C не должны иметь свои собственные таблицы, поэтому я решил использовать атрибут proxy в Meta. Однако я хочу, чтобы существовала таблица всех реализованных прокси.
В частности, я хочу вести запись имени и описания каждого подкласса.
Например, для B имя будет "B", а описание - docstring для B.
Поэтому я создал еще одну модель:
class Type(models.Model):
# The name of the class
name = models.String()
# The docstring of the class
desc = models.String()
# A unique identifier, different from the Django ID,
# that allows for smoothly changing the name of the class
identifier = models.Int()
Теперь я хочу, чтобы при создании A я мог выбирать только между различными подклассами A.
Следовательно, таблица Type всегда должна быть актуальной.
Например, если я хочу протестировать поведение B, мне нужно будет использовать соответствующий экземпляр Type для создания экземпляра B, поэтому этот экземпляр Type уже должен быть в базе данных .
Посмотрев на сайте Django, я увидел два способа достижения этой цели: фикстуры и миграции данных. Fixtures недостаточно динамичны для моего случая, поскольку атрибуты буквально приходят из кода. Остается миграция данных.
Я попробовал написать один, который выглядит примерно так:
def update_results(apps, schema_editor):
A = apps.get_model("app", "A")
Type = apps.get_model("app", "Type")
subclasses = get_all_subclasses(A)
for cls in subclasses:
id = cls.get_identifier()
Type.objects.update_or_create(
identifier=id,
defaults=dict(name=cls.__name__, desc=cls.__desc__)
)
class Migration(migrations.Migration):
operations = [
RunPython(update_results)
]
# ... other stuff
Проблема в том, что я не понимаю, как хранить идентификатор внутри класса, чтобы экземпляр Django Model мог его восстановить.
Пока вот что я пробовал:
Я попробовал использовать довольно новую конструкцию __init_subclass__ в Python. Теперь мой код выглядит так:
class A:
def __init_subclass__(cls, identifier=None, **kwargs):
super().__init_subclass__(**kwargs)
if identifier is None:
raise ValueError()
cls.identifier = identifier
Type.objects.update_or_create(
identifier=identifier,
defaults=dict(name=cls.__name__, desc=cls.__doc__)
)
# ... the rest of A
# The identifier should never change, so that even if the
# name of the class changes, we still know which subclass is referred to
class B(A, identifier=3):
# ... the rest of B
Но это update_or_create не работает, когда база данных новая (например, во время модульных тестов), потому что таблица Type не существует.
Когда у меня возникает эта проблема в процессе разработки (мы все еще находимся на ранних стадиях, поэтому удаление БД все еще разумно), мне приходится
закомментировать update_or_create в __init_subclass__. Затем я могу мигрировать и вставить его обратно.
Конечно, это решение также не очень хорошо, потому что __init_subclass__ выполняется гораздо чаще, чем нужно. В идеале этот механизм должен происходить только при миграции.
Вот и все! Надеюсь, постановка задачи имеет смысл.
Спасибо, что дочитали до конца, и я с нетерпением жду ответа; даже если у вас есть другие дела, я желаю вам хорошего отдыха :)
С небольшой помощью друзей Django-экспертов я решил эту проблему с помощью сигнала post_migrate.
Я удалил update_or_create в __init_subclass, а в project/app/apps.py добавил:
from django.apps import AppConfig
from django.db.models.signals import post_migrate
def get_all_subclasses(cls):
"""Get all subclasses of a class, recursively.
Used to get a list of all the implemented As.
"""
all_subclasses = []
for subclass in cls.__subclasses__():
all_subclasses.append(subclass)
all_subclasses.extend(get_all_subclasses(subclass))
return all_subclasses
def update_As(sender=None, **kwargs):
"""Get a list of all implemented As and write them in the database.
More precisely, each model is used to instantiate a Type, which will be used to identify As.
"""
from app.models import A, Type
subclasses = get_all_subclasses(A)
for cls in subclasses:
id = cls.identifier
Type.objects.update_or_create(identifier=id, defaults=dict(name=cls.__name__, desc=cls.__doc__))
class MyAppConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "app"
def ready(self):
post_migrate.connect(update_As, sender=self)
Надеюсь, это будет полезно для будущих Django-кодеров!