Pull to refresh

Comments 22

Мы как-то так делаем:
— изменения схемы (таблицы/колонки/ключи/индексы) — только миграциями
— хранимки/вьюшки — из исходников, 1 объект на файл
— для «создать с нуля», мы берем бекап с прода, чистим от левака, обфусцируем, и раздаем желающим

Подход «сравним действительное с желаемым, и накатим разницу» — не работает в общем виде. Данные надо мигрировать на новую схему, эти миграции — часто сложные, и их надо уметь воспроизводить на нескольких БД до релиза (девелоперская, тестерская, UAT). Как мне это делать — руками что-ли? И потом просить всех девелоперов — типа прогоните скриптик, или там «восстановите бекап себе, как код возьмете»?

Подход с «все только через миграции» — встречает понятное сопротивление, если заставить людей вьюшки с хранимками так писать. Если они на проекте активно юзаются — там до 95% изменений. А изменения схемы — они дай бог, раз в день.

Все это, на самом деле, выстрадано и придумано давно. Вот статья 10-ти летней давности, например: blog.codinghorror.com/get-your-database-under-version-control

Но при этом, никто почему-то, не делает до сих пор тулы по-человечески — как описано выше и в статье. Либо пытаются сделать только дифф (что теоретически не может работать), либо пытаются заставить хранимки в миграции пихать (не знаю из каких соображений — может лень, может слабоумие).
> Данные надо мигрировать на новую схему, эти миграции — часто сложные… Как мне это делать — руками что-ли?

Ну, в моей практике — конечно же, не претендующей на репрезентативность — миграция данных — довольно редкая операция. 95% всех изменений — это изменение вьюшек, добавление полей в таблицы, изменение типов полей типа добавления количества символов в varchar и т. п. Это не требует никакой миграции данных. И наша Celesta, получая новую версию DDL, это всё делает молча и незаметно.

Там, где миграция всё-таки нужна, мой ответ: да, сделать руками. Просто помечаем себе: когда будем выкатывать такую-то фичу, надо сначала прогнать такой-то скрипт. Обкатываем этот скрипт на девелоперской машине, на staging-е, и, в конце концов, запускаем на проде.

Я думал о том, можно ли (и как) совместить «конвергентный» подход к изменению структуры БД с «changeset»--подходом к модификации данных. Истина, возможно, где-то посередине, но пока изящного решения не придумалось.
Ща проглядел пару десятков миграций у нас — в половине какой-то DML есть. Где-то — просто одно поле пред-заполнить исходя из других. Где-то — полноценная перезаливка данных в другие таблицы. Ну и вьюшки и прочие хранимки мы не кладем в миграции.

>Обкатываем этот скрипт на девелоперской машине, на staging-е, и, в конце концов, запускаем на проде.
И говорим остальным 10-ти девелоперам типа «когда вольете к себе последний код, вот этот скриптик накатите, а то у вас всё крешится будет. Так что-ли?

>Я думал о том, можно ли (и как) совместить «конвергентный» подход к изменению структуры БД с «changeset»--подходом к модификации данных

У нас вот Entity Framework, там миграции генерятся как DIFF между моделью после последней миграции и текущей моделью из кода. Добавил поля, выполнил add-migration, посмотрел что сгенерилось на всякий, если надо — всунул свой DML, закомиттал. Весьма удачное совмещение — и модель имеется, и с миграциями всё строго.
> Ну и вьюшки и прочие хранимки мы не кладем в миграции.

А с ними как поступаете — периодически прогоняете полные «CREATE OR REPLACE VIEW»-скрипты?

> И говорим остальным 10-ти девелоперам типа «когда вольете к себе последний код, вот этот скриптик накатите, а то у вас всё крешится будет. Так что-ли?

А у вас 10 девелоперов сидят на 10 копиях базы? Хмм, везде где я работал, обычно development-база одна и shared, и периодически копируется с прода. Ну вот так вот всё у всех по-разному.

А кстати какая у вас СУБД?
У нас SQL Server, так что опыт весь про него.

Вьюшки/хранимки — надо хранить по отдельным файликам. Правильнее всего, из сносить все и создавать заново после применения всех миграций схемы. Так будет видно, что не осталось зависимостей на удаленные хранимки/вьюхи, и что внутри у них ничего не поломалось после изменения схемы.

С порядком наката хранимок/вьюх есть засада — надо как-то автоматически определить зависимости хранимок и вьюх друг от друга. Если интересно — расскажу подробнее как мы делали.

У каждого девелопера своя БД локально. На старте приложения, если надо, БД обновляется миграциями автоматом. Запустить приложение на БД не той версии — вообще не получится.

Я работал в проекте с общей на всех девов DB, с кучей хранимок — это был ад сотоны. Ну типа надо тебе поработать с хранимкой — делаешь копию с постфиксом _Ivan, в коде на нее перецепляешься, и погнал. Ну или посередине дня у тебя функционал отлетает — кто-то что-то поменял в хранимке и коде, а у тебя код несовместимый. Короче работает у тебя фича локально или нет — это было что-то про теорию вероятностей.

Именно там я, страдая от этой боли, раскурил вопрос, и сделал тулзу, которая гоняет миграции по описанной схеме (миграции для данных + хранимки/вьюхи перенакатить). Сразу выяснилось, что 20% хранимок вообще нельзя даже накатить — схема протухла.

После этого, мы сделали проект чисто в таком стиле, и это были радость и счастье. Сейчас мы юзаем встроенный в EF механизм для миграций, с прикрученным к нему сбоку механизмом для хранимок/вьюшек — встроенного там такого нету.
Ясно, значит у вас проблема миграций решена в такой комбинации: MSSQL, Entity Framework (подсмотрел в википедии, что это ORM для .NET — следовательно, видимо, приложение пишете на .NET), плюс свои собственные наработки по поводу того, чтобы определить правильный порядок накатки.

Интересно узнать вот что: как у вас обстоит дело с модульным тестированием процедур, изменяющих данные в базе? Какой подход используете или не делаете юнит-тесты таких процедур вообще? Потому что история про «несовместимый код», возникающий в базе «в середине дня» — это история про отсутствие юнит-тестов, выполняемых в локальном в development environment-е программиста прежде, чем он выкатит изменения на персистентную базу.

В Java-мире есть великий и ужасный H2 in-memory, но на ней не потестируешь процедуры. Хотя вот у нас, например, по определению процедур и нет, поэтому мы активно для юнит-тестов юзаем H2. Есть TestContainers, но они появились относительно недавно и я сам только планирую их попробовать. Что-то слышал про запуск СУБД на RAM-дисках. Все эти подходы предполагают, что в момент запуска юнит-тестов у программиста поднимается «быстрая и пустая» копия базы на основе самого свежего DDL, поэтому мигрировать ничего не надо, она существует в течение выполнения юнит-теста и «забывается».
>Потому что история про «несовместимый код», возникающий в базе «в середине дня» — это история про отсутствие юнит-тестов, выполняемых в локальном в development environment-е программиста прежде, чем он выкатит изменения на персистентную базу.

Это история когда общая база на всех девелоперов. Вася в обед делает фичу, в которой в хранимке добавляет новый параметр, и в коде у себя — тоже добавляет параметр. У него все отлично работает. А вот Дима — не брал код с утра, у него в обед приложение начинает падать.

Юнит-тестирование мы почти не юзаем. У нас простой бекенд — считал с БД, отдал в UI, и наоборот. Юнит-тесты в таких системах — дорогая и бесполезная игрушка.

Когда-то надо было интеграционные тесты — гоняли на живой БД, со всеми справочниками и прочим. Тесты запускали в транзакции, и откатывали после запуска.
Чем больше в этой отрасли, тем больше понимаю, насколько у всех всё по-разному)) У нас например без юнит-тестов было бы совсем кисло, при правильном подходе они довольно легки для написания и запуска. Но, опять же: у всех всё по-разному, разные сценарии, разные потребности. Наше нынешнее состояние — это результат многолетних мучений и эволюции и в итоге мы наполовину работаем на «доморощенных» инструментах.

Мы живём на shared-базе. Если наш гипотетический Дима не взял код с утра (а shared-база уже сконвертировалась в более свежую ревизию), уже при запуске приложения у Димы свалится: «Дима, твоя база имеет более свежую ревизию, чем приложение, которое ты пытаешься запустить»). Дима делает адейт с контроля версий и ок.
Там Ваня не с утра сделал breaking change, а в обед. Дима с утра взял код, с обеда приходит — а у него крешится от входа…
У MS SQL при создании процедур проверяется на наличие используемых внутри процедур?
Конечно, у каждого разработчика своя БД в докере, так же как и свой брокер очередей. Стенды — это для разного тестирования и для прода, а зачем разработчику от стенда зависеть?
После всего этого ваш скрипт можно просто выкинуть

А если баз несколько десятков, а то и сотен? И за один раз нельзя обновить все. Причем версия некоторой базы может отличаться на несколько пунктов (т.е. давно не обновлялась)?
При change logaх не будет таких проблем. А тут видимо придется для каждого отдельного случая смотреть на логи данного инструмента.
Выше ответил! если баз несколько десятков — это однозначно кейс для change log-ов.

Convergent-системы — это кейс для случая, когда баз меньше десяти.
Очень актуально.
У нас вся логика лежит в SQL, на стороне клиента фактически интерфейс и всё.
Пока наше всё — backup, срезы, экспорт в набор скриптов. Управляется так себе.
Горячо рекомендую посмотреть в сторону Liquibase.
После всего этого ваш скрипт можно просто выкинуть, потому что больше он вам не понадобится никогда! Ведь ваши рабочие базы уже находятся в нужном вам состоянии по структуре, а если вы задумаете делать новую базу «с нуля», то тогда вам не надо заставлять базу проходить весь тот путь, который вы прошли, дорабатывая её структуру в процессе разработки.

Начиналось всё хорошо, но в этом абзаце всё пошло прахом… Если моё приложение имеет версию 20, а предыдущие версии моего приложения (от 1 до 20) стоят у тысячи пользователей, то скрипт делающий апгрейд должен уметь проапгрейдить БД каждой из 19 версий до версии 20. Поэтому вышеупомянутый скрипт ну никак нельзя выбрасывать.
Ну в данном случае — конечно нет! В следующем предложении я же написал слово «базы» во множественном числе)) Конечно, если у вас несколько баз, то сначала надо проапгрейдиться везде.

А если у вас аж 1000 независимых баз… то, думаю, это железный кейс для changeset-управления.
Я не скажу за все СУБД, мой опыт в этой сфере ограничивается MS SQL, но решение как бы есть :)
На двух довольно ёмких и длительных проектах был успешно применён подход, реализующий оба затрагиваемых принципа. Корень подхода — дисциплина внесения изменений.
Первое железобетонное правило, за нарушение которого «били по рукам» — все изменения начинаются с написания скрипта. Никаких дизайнеров или ковыряния в базе.
Второе — скрипты должны быть не ломающими. Да, кода получается сильно больше. Приходится DDL обкладывать проверками. Но если один раз привыкнуть писать эти скрипты по определённому шаблону — оно оказывается не так уж и сложно. Одна проблема — пока не удалось автоматизировать эту работу, хотя и есть понимание, от чего можно отталкиваться и как делать. Не было времени создать инструмент. Зато эти скрипты отлично ложатся в tfs/git/etc и по истории проекта легко отслеживается суть изменений. При этом накат скриптов на базу легко автоматизируется.
Третье, уже вывод из многих раздумий… Я понимаю, почему разработчики СУБД не предлагают ничего похожего на CONVERGE — уж слишком легко сломать и не вернуть (одно спасение — бэкапы). Суть проблемы в том, что при редактировании кода мы не можем потерять клиентские данные, за их целостность отвечает тестирование продукта, а код — это только текст. Схема данных — это данные, на основании которых определяется стратегия хранения клиентских данных, и внесение изменений влечёт серьёзные последствия. И никакой инструмент не позволит себе решать, какое изменения в данных корректно, а какое нет. И по сути останутся только изменения, не модифицирующие данные, т.е. костыль.
Ах, да, суть подхода, кратко — каждое изменение атомарно, как транзакция. Обрамляется проверкой предусловий, завершается 'go'. Изменения одной задачи объединяются в один скрипт, проверяющий некую мета-информацию о базе с условием допустимости выполнения (например версию БД). Как итог — накатывать можно всю порцию скриптов. Устаревшие игнорируются, слишком новые выкидывают фатальные ошибки.
Практикуем подобный подход.
С клиентской стороны это некий патч, который
— выполняет трансформацию базы (alter таблиц и [пере]заполнение их)
— модификация процедур и вьюшек
— заменяет интерфейсные модули

Своего рода минусом является жесткая последовательность установки патчей без вариантов кумулятивных сервиспаков. Но как показала практика даже апдейт «заморозившегося» 10 лет назад клиента — процесс относительно быстрый и практически безболезненный… ну не считая того, что за 10 лет система не только перетекла на иную платформу, но и функционально трансформировалась из простой учетной в ERP

Сейчас как раз наблюдаю за таким скоростным ростом одного из клиентов — процесс тормозится скорее на уровне не самого обновления, а изучения новшеств и их осознания + требует от рулящего системой неких действий уровня назначения разветвившихся новых прав, уровней доступа и т.п.

Что делать, если мы, допустим, захотим добавить в непустую таблицу NOT NULL-поле и не снабдим его DEFAULT-значением? Ведь если такое поле не заполнить предварительно данными, то база данных не даст выполнить ALTER TABLE ADD-скрипт. А если мы хотим добавить Foreign Key, но не все данные в таблице удовлетворяют ограничен им? А если, допустим, логика приложения изменилась и требуется перенести данные из одного столбца в другой?

Почему бы не снабдить CONVERGE опциями, в которых будут разруливаться возможные проблемы. Например, самое страшное, если в конверге ошибочно «потеряется» столбец, который есть в таблице и он дропнится со всеми данными. Пусть конверге идёт с опцией запрета на удаление по умолчанию, а при необходимости можно добавить в конце что-то типа enable drop columns (col1, 2 ). Другие 2 опции — before converge begin… end, after converge — т.е. по сути триггеры уровня DDL, где можно прописать и перенос данных из 1-й колонки в другую, и заполнение not null поля чем хочешь, а не только default значением, и сохранение какой-то истории, данных в буфер и т.д. Это так, к размышлению.
Относительно дропа: я об этом в статье не написал, но на мой взгляд, самым лучшим решением было бы во время выполнения CONVERGE дропать только такой столбец, который содержит NULL (или DEFAULT-константу) для всех строк. Иначе — ошибка CONVERGE, как и для всякой иной ситуации, когда наличествующие данные не позволяют произвести изменение структуры.

Именно такая логика, например, реализована в ERP-системе MS Dynamics NAV при «импорте таблиц» в базу данных. Таким образом ты в NAV новую версию структуры таблицы не заимпортируешь, пока в старой версии данные не подготовишь к миграции. В Celesta реализовано так: ничего не дропается. Но так как Celesta идёт в паре с фреймворком для доступа к данным, там просто столбец и таблица пропадают из API для доступа к данным. Т. е. в базе они остаются, но из кода ты ими пользоваться уже не сможешь. Уже постфактум ты их можешь отмигрировать и дропнуть.

Относительно begin-after-converge: если сделать как Вы предлагаете, то произойдёт в одном месте смешение идемпотентного (декларативного) и императивного кода. Декларативный код — он для всех сценариев один и тот же, а императивный — при одном сценарии нужен один код миграции, в другом случае — другой код… в итоге потеряется стройность DDL-скрипта, он забьётся всё разрастающимся кодом миграции. Их надо как минимум разделять. Мне кажется, задача совмещения changeset- и идемпотентного подхода ещё ждёт своего изящного решения.
Sign up to leave a comment.

Articles