Как вести таблицу всех прокси-моделей в 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-кодеров!

Вернуться на верх