Внешние ключи модели 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.
После нескольких часов работы я придумал возможный (но непростой) обходной путь:
- Remove my reusable app from
INSTALLED_APPS - Create
MyInviteandMyPersonin thetesterapp (they both inherit fromdjango.models.Model - Create those models by running
makemigrations tester - Add my reusable app to
INSTALLED_APPS - Define the swap settings
- Change the inheritance of my models to their respective abstract counterparts
- Run
makemigrationsagain.
Это работает, потому что миграция 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 не планируют поддерживать это, так как я не нашел никаких открытых проблем или запросов на это.