Конфликты миграций Django в команде: как разрешать
Две ветки создали миграции 0042 на одну модель. При слиянии Django падает с InconsistentMigrationHistory. Разбираем, откуда берётся конфликт и как его чинить без дропа базы.

Вы сливаете ветку коллеги в main, запускаете python manage.py migrate и получаете django.db.migrations.exceptions.InconsistentMigrationHistory или предупреждение Conflicting migrations detected; multiple leaf nodes in the migration graph. Причина почти всегда одна: две ветки независимо добавили миграции с номером 0042 в одно приложение, каждая ссылается на 0041 как на родителя, и граф миграций перестал быть линейным. Ниже — как это устроено, как чинить типовой конфликт через makemigrations --merge, когда автоматическое слияние не спасает и что настроить, чтобы конфликты не доходили до main. Актуально для Django 4.2 LTS и 5.x — механика графа миграций между этими версиями не менялась.
Почему граф миграций ломается именно в команде
Django хранит миграции не как список, а как ориентированный граф. У каждой миграции есть поле dependencies — список родителей. Обычно родитель один: предыдущая миграция того же приложения. Команда migrate строит из этого графа линейный порядок и применяет миграции по очереди, записывая каждую в таблицу django_migrations.
Проблема появляется, когда двое стартуют от одного состояния. У обоих последняя миграция — 0041_add_status. Разработчик A в своей ветке делает makemigrations и получает 0042_add_phone с зависимостью от 0041. Разработчик B в своей ветке делает то же и получает 0042_add_avatar — тоже с зависимостью от 0041. Пока ветки живут отдельно, всё работает. При слиянии в графе появляются два «листа» (leaf node) — две миграции без потомков, обе претендующие на роль последней.
Django не умеет угадать, какая из двух 0042 должна идти первой. Он честно останавливается и просит человека решить порядок — потому что от порядка может зависеть результат.
Само по себе совпадение номеров — не ошибка, а симптом. Номер 0042 Django берёт инкрементом от последней найденной миграции. Две ветки видели одну и ту же «последнюю» — отсюда одинаковый номер. Переименование файла проблему не решает: конфликт в зависимостях, а не в именах.
Термин: leaf node
Leaf node (лист графа) — миграция, на которую не ссылается ни одна другая как на родителя. В здоровом приложении лист ровно один: самая свежая миграция. Два и более листа в одном приложении — это и есть конфликт, который Django требует смёржить.
Разбор на реальном примере
Приложение accounts, модель UserProfile. Исходное состояние — миграция 0041_add_status уже в main. Дальше две ветки.
Ветка feature/phone добавила поле телефона:
# accounts/migrations/0042_userprofile_phone.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0041_add_status"),
]
operations = [
migrations.AddField(
model_name="userprofile",
name="phone",
field=models.CharField(max_length=20, blank=True),
),
]
Ветка feature/avatar независимо добавила аватар — и файл тоже назвался 0042:
# accounts/migrations/0042_userprofile_avatar.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0041_add_status"),
]
operations = [
migrations.AddField(
model_name="userprofile",
name="avatar",
field=models.ImageField(upload_to="avatars/", blank=True),
),
]
После слияния обеих веток в вашей рабочей ветке лежат оба файла. Запускаете migrate и видите:
CommandError: Conflicting migrations detected; multiple leaf nodes
in the migration graph: (0042_userprofile_phone, 0042_userprofile_avatar in accounts).
To fix them run 'python manage.py makemigrations --merge'
Шаг 1. Убедитесь, что база ещё чистая
Проверьте, что ни одна из конфликтующих миграций локально не применена — иначе слияние будет чинить не то. Команда покажет статус:
python manage.py showmigrations accounts
Оба поля 0042 должны быть с пустыми скобками [ ] — не применены. Если у вас одна из них уже стоит [X] (успели помигрировать в своей ветке до слияния) — сначала откатитесь до 0041: python manage.py migrate accounts 0041_add_status.
Шаг 2. Создайте merge-миграцию
python manage.py makemigrations --merge
Django спросит подтверждение и создаст файл вида 0043_merge_<дата>.py. Внутри — миграция без операций, но с двумя родителями:
# accounts/migrations/0043_merge_20250115.py
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0042_userprofile_phone"),
("accounts", "0042_userprofile_avatar"),
]
operations = []
Merge-миграция ничего не делает со схемой. Её задача — свести два листа в один узел, чтобы граф снова стал линейным ниже точки слияния. После неё лист один — 0043_merge.
Шаг 3. Примените и проверьте
python manage.py migrate
python manage.py showmigrations accounts
Все три (обе 0042 и merge) должны стать [X]. В базе появятся оба поля — phone и avatar. На финал прогоните проверку, что модели и миграции согласованы:
python manage.py makemigrations --check --dry-run
Если команда молчит и не предлагает новых миграций — состояние консистентно.
Когда --merge не спасает
Автослияние работает, пока миграции независимы: добавили разные поля, разные таблицы. Оно ломается, когда обе ветки трогают одно и то же.
- Один и тот же столбец. Ветка A переименовала
statusвstate, ветка B изменила уstatusтип. Merge-миграция сведёт графы, но порядок применения станет важен: если сначала переименование, вторая операция упадёт — столбцаstatusуже нет. Здесь нужно вручную решить, какая операция первая, и при необходимости переписать вторую под новое имя. - Данные, а не только схема. Если одна из миграций содержала
RunPythonс переносом данных, порядок относительно чужих схемных изменений критичен. Merge порядок не гарантирует между разными листами — проверьте зависимости руками. - Конфликт между приложениями. Внешний ключ из
ordersна новую модель изaccounts. Тогда зависимость должна ссылаться на конкретную миграцию другого приложения.--mergeмежприложенческие зависимости не расставляет — их добавляете вы.
Если конфликт нетривиальный, надёжнее не мёржить, а пересоздать: удалить свою неприменённую 0042, влить чужую, затем makemigrations заново — новая миграция встанет поверх чужой линейно, без развилки. Это работает только при условии, что ваша миграция ещё нигде не применена и не ушла к другим.
Золотое правило: если миграция уже в общей ветке или на стенде — её нельзя удалять и переписывать, только добавлять поверх. Переписанная «общая» миграция ломает историю у всех, кто её уже применил.
Два способа разрешить: сравнение
| Критерий | makemigrations --merge | Пересоздать миграцию |
|---|---|---|
| Когда применимо | Изменения независимы, обе миграции в чужих руках уже применены | Ваша миграция ещё нигде не применена и не расшарена |
| История миграций | Остаётся с развилкой + merge-узлом | Линейная, чище |
| Риск при пересечении полей | Высокий: порядок не гарантирован | Ниже: новая миграция видит финальное состояние |
| Безопасность для команды | Ничего чужого не трогает | Опасно, если чужая миграция уже применена |
| Трудозатраты | Одна команда | Удаление + повторная генерация |
Практический критерий выбора простой: миграция уже покинула вашу машину — только merge. Не покинула и конфликт по одному полю — пересоздавайте.
Что ломается на проде и как не доводить
На локальной SQLite конфликт вы заметите сразу. На проде опаснее сценарий, когда merge-миграция прошла ревью, но порядок операций в ней некорректен для реальных данных — на пустой тестовой базе AddField с null=False без default проходит, а на таблице с миллионом строк падает или блокирует запись. Это не специфика конфликта, но конфликтные merge-миграции чаще проскакивают ревью, потому что выглядят «пустыми».
Чтобы конфликты не доходили до main:
- Добавьте в CI шаг
python manage.py makemigrations --check --dry-run— сборка падает, если в ветке есть несогенерированные или конфликтующие миграции. - Мигрируйте маленькими ветками и мёржите часто: чем короче живёт ветка с миграцией, тем меньше шанс, что кто-то создаст параллельную 0042.
- Договоритесь в команде: изменения схемы одного приложения в один момент делает один человек. Дешевле, чем разбирать графы.
- Merge-миграции ревьюйте отдельно, проверяя порядок в
dependenciesи наличиеRunPythonв родителях.
Что делать дальше
Настройте CI-проверку makemigrations --check --dry-run — это ловит конфликт до слияния, а не после. Изучите вывод python manage.py sqlmigrate accounts 0043_merge_20250115: он показывает SQL, который Django выполнит, — так вы увидите реальный порядок операций до применения на проде. Для схемных изменений на больших таблицах отдельно смотрите на блокировки: тема прогона тяжёлых миграций под нагрузкой (неблокирующие индексы, разбиение AddField и заливки default) заслуживает своего разбора и здесь не раскрывается.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Django documentation: Migrations, Django management commands: makemigrations --merge
Частые вопросы
У меня Django 4.2, а не 5.x — команды те же?
makemigrations --merge не убрал конфликт, что делать?
Можно ли просто удалить одну из миграций 0042 и сгенерировать заново?
Грозит ли merge-миграция потерей данных?
Как предотвратить такие конфликты в команде?
Что если конфликтуют миграции в двух разных приложениях?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.