Внешние ключи модели Django с возможностью замены

Предположим, у меня есть приложение многократного использования, которое определяет модель Person и модель Invite.

А Person имеет поле OneToOne к AUTH_USER_MODEL и определяет некоторые основные поля (такие как день рождения). Это сменная модель, так что проект, использующий это приложение, может легко добавить другие поля (такие как пол и т.д.)

В моем многоразовом приложении я определяю параметр, который предоставляет модель замены (в противном случае будет использоваться модель по умолчанию, точно так же, как это делает django.contrib.auth.

Модель Invite имеет OneToOneField к заменяемой модели Person и поле email. (Я думаю, вполне понятно, для чего нужна эта модель). Сама модель также является заменяемой, но я не думаю, что это имеет какое-либо значение для проблемы, с которой я столкнулся.

повторно используемые модели приложений:

class AbstractPerson(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='person')
    
    birthdate = models.DateField()

    class Meta:
        abstract = True

class Person(AbstractPerson):
    class Meta(AbstractPerson.Meta):  
        swappable = 'REUSABLEAPP_PERSON_MODEL'

class AbstractInvite(models.Model):
    email = models.EmailField()
    person = models.OneToOneField(settings.REUSABLEAPP_PERSON_MODEL, on_delete=models.CASCADE, null=False, related_name='+')

    class Meta:
        abstract = True

class Invite(AbstractInvite):
    class Meta(AbstractInvite.Meta):
        swappable = 'REUSABLEAPP_INVITE_MODEL'

Если я создаю начальную миграцию для своего многоразового приложения (используя фиктивный проект и не меняя местами свои модели), я получаю следующую миграцию для своего многоразового приложения:

class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='Person',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('birthdate', models.DateField()),
                ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person', to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'abstract': False,
                'swappable': 'REUSABLEAPP_PERSON_MODEL',
            },
        ),
        migrations.CreateModel(
            name='Invite',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('email', models.EmailField(max_length=254)),
                ('person', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.REUSABLEAPP_PERSON_MODEL)),
            ],
            options={
                'abstract': False,
                'swappable': 'REUSABLEAPP_INVITE_MODEL',
            },
        ),
    ]

Если я затем включаю мое многократно используемое приложение в другой проект и меняю местами модели Person и Invite, я получаю ошибку при выполнении makemigrations:

ValueError: Поле myreusable_app.Invite.person было объявлено с ленивой ссылкой на 'tester.myperson', но приложение 'tester' не установлено.

.

(tester - это приложение, которое определяет поменянные местами модели, очевидно)

Если я удалю миграцию из моего многоразового приложения и снова запущу makemigrations, она работает. созданная миграция почти идентична приведенной выше, за исключением новой зависимости:

migrations.swappable_dependency(settings.REUSABLEAPP_PERSON_MODEL),

Миграция, созданная в приложении tester, выглядит следующим образом:

class Migration(migrations.Migration):

    initial = True

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name='MyPerson',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('birthdate', models.DateField()),
                ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person', to=settings.AUTH_USER_MODEL)),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='MyInvite',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('email', models.EmailField(max_length=254)),
                ('person', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.REUSABLEAPP_PERSON_MODEL)),
            ],
            options={
                'abstract': False,
            },
        ),
    ]

Я посмотрел, что на самом деле делает swappable_dependency: Оно смотрит только какое имя приложения определено (допустим, установлено tester.mymodel), и создает зависимость от этого приложения initial migration.

Теперь, если я удалю созданную миграцию из моего tester приложения, я не могу запустить makemigrations снова, я получаю ту же ошибку, что и выше.

Прояснение до этого момента: Все работает как надо, если я удалю начальную миграцию (и, следовательно, все будущие миграции!) из моего многократно используемого приложения.

Проблема, как я ее понимаю, заключается в следующем: Многоразовое приложение имеет зависимость от начальной миграции клиентского приложения, которая определяет поменянные местами модели. Но такой миграции еще не существует (черт, я пытаюсь ее создать!), поэтому makemigration не работает. (Запуск makemigrations tester не помогает). Но почти то же самое безупречно работает при замене стандартной модели User на пользовательскую. Кроме того, я не совсем понимаю, почему в сообщении об ошибке говорится, что приложение tester не установлено . Оно определенно находится внутри моего INSTALLED_APPS и его подхватывает django-ecosystem.

После нескольких часов работы я придумал возможный (но непростой) обходной путь:

  1. Remove my reusable app from INSTALLED_APPS
  2. Create MyInvite and MyPerson in the tester app (they both inherit from django.models.Model
  3. Create those models by running makemigrations tester
  4. Add my reusable app to INSTALLED_APPS
  5. Define the swap settings
  6. Change the inheritance of my models to their respective abstract counterparts
  7. Run makemigrations again.

Это работает, потому что миграция initial моего многоразового приложения теперь может выполнить зависимость от поменянных моделей, глядя на начальную миграцию tester, которая определяет модели с тем же именем, которое определено в переменных подкачки. Но я уверен, что должен быть лучший способ сделать это

В связи с этим у меня возникают следующие вопросы:

Как я могу обрабатывать отношения внешнего ключа к поменявшимся местами моделям?

Почему я не могу создавать миграции для одного приложения, не просматривая миграции других приложений?

К сожалению, я должен ответить на свой собственный вопрос.

Этот пакет предоставляет публичный интерфейс к API свопинга. Я понял, что смотреть на закрытые вопросы иногда полезнее, чем читать открытые.

Особенно #12, и открытый #10 ответ на мой вопрос. Подводя итог, то, что работает для настройки AUTH_USER_MODEL, не работает для любой другой заменяемой модели, потому что ошибка lazy_reference игнорируется , когда целевая модель установлена как AUTH_USER_MODEL внутри django, здесь:

# There shouldn't be any operations pending at this point.
from django.core.checks.model_checks import _check_lazy_references
ignore = {make_model_tuple(settings.AUTH_USER_MODEL)} if ignore_swappable else set()
errors = _check_lazy_references(self, ignore=ignore)
if errors:
   raise ValueError("\n".join(error.msg for error in errors))

Предлагаемое решение - это реестр, где все поменянные местами модели регистрируются и затем ищутся, чтобы игнорировать ошибку ленивой ссылки. К сожалению, похоже, что разработчики django не планируют поддерживать это, так как я не нашел никаких открытых проблем или запросов на это.

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