Внешние ключи модели 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
MyInvite
andMyPerson
in thetester
app (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
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 не планируют поддерживать это, так как я не нашел никаких открытых проблем или запросов на это.