Миграция схемы данных без головной боли: идемпотентность и конвергентность для DDL-скриптов

    Язык SQL и реляционные базы с нами уже более сорока лет. За это время стандарт SQL прошёл через множество ревизий, и, судя по всему, процесс развития на этом не останавливается. Реляционные базы в качестве хранилищ данных десятилетиями царствовали безраздельно, царствуют и поныне, и лишь только в последнее время их немного теснят альтернативные подходы.

    SQL практически всемогущ, если вопрос касается извлечения данных. (Не все знают, но одним SQL-запросом можно графически построить множество Мандельброта). Но одна проблема продолжает быть в нём концептуально не решена: проблема миграции схем данных.

    image

    Я думаю, на разных этапах своей карьеры в IT мы все сталкивались с тем, как это бывает тяжело: контролировать структуру рабочей базы данных и выполнять её обновления по мере разворачивания новых версий софта. Баги, возвращающиеся после того, как их вроде уже исправили, ошибки «поле отсутствует в таблице», жалобы «я исправил хранимку, а вы затёрли!» — знакомо ли вам это всё?

    Острота этой проблемы особенно ясна по контрасту с тем, насколько кардинально решена задача контроля версий исходного кода: системы типа Git или Subversion позволяют вам держать под контролем совместную работу многих разработчиков, не терять ни одного изменения, «записывать все ходы» и собирать результаты труда команды воедино. Но эта благостная картина заканчивается там, где заканчивается область применимости систем контроля версий. Если мы говорим о структуре схемы данных, то до сих пор применение систем контроля версий для их разработки — весьма ограничено.

    Так легко и соблазнительно бывает внести изменения структуры прямо на базе! Если такие изменения оказываются не задокументированы, то в будущем наступает необходимость развивать систему и приближается катастрофа. Но и осознав необходимость документировать каждый шаг по изменению структуры БД, мы приходим к тому, что делать это — очень неудобно, а язык DDL определения схемы данных нам в этом никак не помогает. Например, добавление поля в таблицу требует команды ALTER TABLE ADD. Выполнить это выражение имеет смысл ровно один раз и ровно тот самый момент, когда в таблице поле ещё отсутствует, а необходимость в нём – уже есть. Выполнение этого скрипта в другие моменты приведёт либо к ошибке выполнения самого скрипта, либо, что хуже, к неправильному состоянию структуры базы данных.

    Как же агрегировать все изменения, производимые разработчиками, в единый результат и как проконтролировать его выполнение?

    Подход №1: накопление change-скриптов


    Практическое решение этой задачи вручную, без использования каких-либо дополнительных инструментов, заключается в создании в кодовой базе проекта папки с пронумерованными скриптами модификации структуры базы данных, в накапливании их в этой папке и во введении строжайшей дисциплины среди участников проекта разработки. Должен быть формализован процесс записи изменений в очередном скрипт-файле и процесс «накатки» изменений на базы данных.

    Более продвинутым и удобным способом решения является использование систем типа Liquibase. Liquibase берёт на себя контроль номера последнего выполненного изменения структуры данных и обеспечивает прогон скриптов модификации структуры в линейном порядке, один и только один раз. Это уже очень много, и использование Liquibase однозначно ликвидирует хаос, царящий при разработке и обновлении схемы данных.

    Но тем не менее, использование Liquibase и других инструментов, основанных на накоплении change-скриптов, не делает задачу модификации схемы данных столь же легкой и техничной, как легка и технична модификация программного исходного кода вашего приложения.

    Во-первых, change-скрипты накапливаются. Достаточно долго идущий проект тянет за собой большой «хвост» из этих скриптов, большинство из которых утрачивают свою актуальность, т. к. содержат в себе изменения, выполненные очень давно. Сохранение всего «хвоста» бывает бессмысленно, т. к. изменение в одном месте лога может отменять результат изменения в другом месте: допустим, в ходе развития системы мы попробовали какой-то вариант и отказались от него, но два изменения уже навсегда остаются change-log-е. Глядя на разросшийся лог, становится невозможно понять наше текущее представление о структуре базы. А если мы хотим воспроизвести структуру базы данных «с нуля» на, допустим, совершенно новом проекте, нам придётся в процессе выполнения скрипта повторять весь её путь эволюционного развития!

    Во-вторых, при таком подходе сохраняется колоссальная разница между сложностью модификации структуры БД через changeset-ы и простотой модификации исходного кода программы.

    Допустим, у вас есть код, описывающий класс в вашем любимом языке программирования, и вам в процессе улучшения системы понадобилось один метод в класс добавить, а другой – удалить. Что вы делаете? Берёте и меняете исходный код класса, а затем коммитите его на систему контроля версий, не так ли? Не вынуждены же вы создавать change set с командами вида:

    alter class drop method foo;
    alter class add method bar(…) {

    }

    Почему мы должны это делать для структуры схемы данных?

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

    Подход №2: идемпотентный DDL-скрипт


    Проблемы, схожие с теми, что возникают при миграции схемы базы данных, давно возникали при конфигурировании серверов.

    Как автоматизировать установку нужного софта на сервер и обеспечить обновление конфигураций? Мы можем написать shell-скрипт, который, будучи выполнен на пустой виртуальной машине, всё на ней установит и сконфигурирует. Это аналог скрипта создания структуры БД (CREATE TABLE…-скрипта), но его проблема в том, что он может быть выполнен только один раз и только на пустой машине. Если машина уже развёрнута и работает, а по новой спецификации для работы системы нам нужна, например, другая версия Java, как тут постуить — дописывать change скрипт, сносящий старую версию и устанавливающий новую версию Java? Ну а если нам нужно будет воспроизвести конфигурацию на пустой машине — нам что же, проходить через все шаги, которые мы прошли исторически?

    Главный, ключевой вопрос, который при этом возникает: можно ли править инфраструктуру/схему данных так же легко, как мы правим исходный код – прямым изменением её описания и записи в контроль версий?

    Ответом на эти вопросы для задачи конфигурирования серверов явилось появление принципа Infastructure as Code (IaC) и целого класса систем, известных как configuration management-системы: Ansible, Chef, Puppet и т. д. В основе всех систем этого вида стоят два главных принципа: идемпотентность (idempotence) и конвергентность (сходимость, convergence). Оба этих термина позаимствованы из математики. Если отбросить ненужный формализм, применительно к нашей проблематике термины эти обозначают следующее:

    1. Идемпотентный и конвергентный скрипт описывает не действия, которые надо произвести над объектом, а состояние, в которое нужно привести объект.
    2. Идемпотентность означает, что если такой скрипт выполнен успешно и объект уже находится в нужном состоянии, то повторное выполнение скрипта ничего не изменит и ни к чему не приведёт. Например, если мы говорим о скрипте configuration management системы, декларирующем установку необходимых пакетов, то, если эти пакеты уже установлены, при повторном запуске скрипт просто не выполнит никаких операций.
    3. Конвергентность означает, что если скрипт не был выполнен или завершился неуспешно, при его повторном выполнении система будет стремиться к желаемому состоянию. Например, если установка одного из пакетов завершилась с ошибкой, т. к. в момент скачивания пакета пропала сеть, то повторный запуск скрипта приведёт к тому, что пропущенный пакет доустановится (а те, что установились в прошлый раз, останутся на своём месте).

    Данные принципы являются общими и применимы не только к программной конфигурации машин. Например, система Terraform позволяет вам в виде кода специфицировать необходимый набор виртуальных серверов и их аппаратную конфигурацию и контролировать ваш парк виртуальных машин в облаке.

    Я думаю, сказанного уже достаточно для того, чтобы понять, каким образом эти принципы могут помочь для контроля схемы данных. Предположим, что наша база данных поддерживает ключевое слово “CONVERGE” и у нас имеется такой скрипт:

    CONVERGE TABLE OrderHeader(
    id VARCHAR(30) NOT NULL,
    date DATETIME DEFAULT GETDATE(),
    customer_id VARCHAR(30),
    customer_name VARCHAR(100),
    CONSTRAINT Pk_OrderHeader PRIMARY KEY (id)
    );

    Ключевое слово CONVERGE должно интерпретироваться как «приведи таблицу к желаемой структуре». То есть: если таблицы нет — создай, если таблица есть, посмотри, какие в ней поля, с какими типами, какие индексы, какие внешние ключи, какие default-значения и т. п. и не надо ли что-то изменить в этой таблице, чтобы привести её к нужному виду.

    Если бы базы данных могли поддерживать такое ключевое слово, т. е. если бы была возможность писать для баз данных идемпотентные и конвергентные DDL-скрипты — необходимости в написании этой статьи не возникло бы. Все мы просто держали бы в системе контроля версий “CONVERGE TABLE”-скрипты, описывающие желаемую на текущий момент схему данных, и работали бы с ними точно так же, как работаем с исходным кодом: нужно новое поле в таблице — добавили, нужен другой состав полей в индексе — отредактировали. (Я слышу ваш вопрос: а как же быть с миграцией данных — но терпение, я к этому перейду уже скоро.)

    К сожалению, в мире реляционных БД движения к поддержке настоящей идемпотентности DDL пока не происходит. Всё, что до сих пор было сделано в базах данных по направлению к идемпотентности DDL-кода – это поддержка конструкции CREATE IF NOT EXISTS, но это, скажем прямо – довольно слабая попытка. Скрипт CREATE TABLE IF NOT EXISTS, конечно, внешне поведёт себя как идемпотентный (не выдаст ошибку, если таблица уже создана), но не как конвергентный (не будет модифицировать структуру уже созданной таблицы).

    Приходится уповать на внешние инструменты. Идемпотентный и конвергентный DDL доступен, например, в системе Celesta. Чтобы для разработчиков и для инструментов разработки (например, визуального редактора ER-диаграмм) выглядеть как обычный DDL-скрипт, в Celesta применяется ключевое слово CREATE, хотя в Celesta оно обладает смыслом именно гипотетического CONVERGE. При каждом старте, Celesta сравнивает актуальную структуру базы данных, к которой она присоединена, с желаемой структурой, описанной в виде DDL-скрипта CelestaSQL, и при необходимости выполняет минимально необходимую серию CREATE/ALTER/DROP-команд. На сегодня поддерживаются четыре типа баз данных: PostgreSQL, Oracle, MS SQL Server и H2 (последняя, в основном, для нужд организации модульного тестирования).

    Идемпотентный скрипт, задающий структуру БД, нельзя просто взять и линейно выполнить. Как известно, одни объекты в базе данных зависят от других объектов — например, таблицы ссылаются друг на друга через внешние ключи, представления и индексы зависят от таблиц и т. д. Поэтому, прежде чем выполнять серию создания / перестроения объектов, их необходимо ранжировать в порядке зависимости друг от друга: говоря формально, выполнить топологическую сортировку графа зависимостей, и далее обрабатывать объекты, начиная с тех, от которых не зависит ничего. Часто бывает необходимо сбросить внешние ключи, чтобы затем восстановить их после модификации соединяемых ими таблиц. Эта задача решена в Celesta, и это позволяет без проблем выполнять апгрейд для абсолютного большинства случаев.

    Миграция данных


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

    Всё это вопросы совершенно корректные, но для начала заметим, что для большинства изменений, которые вы производите в базе данных в процессе развития вашего приложения, никакой миграции не требуется и простого ALTER-скрипта достаточно. Вам не надо мигрировать данные, если вы просто добавляете новую таблицу или новую колонку в таблицу (NULLABLE или с DEFAULT-значением). Вам не надо мигрировать данные, если вы добавляете или перестраиваете индекс. Не нужно ничего мигрировать, если изменяется запрос для view. Практика применения системы Celesta показывает, что подавляющее большинство производимых разработчиками изменений относится именно к такому типу.

    Если же миграция данных действительно необходима, то, да: придётся написать и выполнить одноразовый миграционный скрипт. Вот только сохранять этот скрипт «на века» не нужно и история с его отладкой и применением гораздо проще, чем в системах, построенных на change log.

    Рассмотрим случай добавления внешнего ключа на непустую таблицу, не все записи которой удовлетворяют такому ключу. В процессе обновления Celesta попробует создать такой ключ при помощи команды ALTER TABLE … ADD CONSTRAINT … FOREIGN KEY. Если ей это удаётся – отлично, если нет – система останавливается и Celesta сообщает, что апдейт такого-то объекта она выполнить полностью не сумела по такой-то причине, с таким-то сообщением БД.

    Для changelog-систем нет ничего хуже changeset-а, выполнившегося наполовину и зафиксированного в промежуточном состоянии: в такой ситуации система находится «посередине» между двумя ревизиями и ситуацию можно разрулить лишь вручную. Отсутствие поддержки откатываемых DDL-транзакций во многих базах вносит дополнительные трудности.

    Для «конвергентных» систем, в отличие от «changelog»-систем, апдейты, не выполненные до конца, не являются проблемой, т. к. для генерации ALTER-команд система сравнивает текущее фактическое состояние базы с желаемым, и изменения, не выполненные при одной попытке, она будет пытаться доделать в другой.

    Столкнувшись с ситуацией, когда апдейт не может быть выполнен автоматически (ситуация, повторюсь, довольно редкая), вы можете сделать одноразовый скрипт. Допустим, наполняющий таблицу-справочник нужными данными и тем самым создающий для Celesta условия автоматического выполнения апдейта. Этот скрипт можно отладить на свежей копии production-базы, затем выполнить на production базе, затем произвести Celesta-апдейт.

    После всего этого ваш скрипт можно просто выкинуть, потому что больше он вам не понадобится никогда! Ведь ваши рабочие базы уже находятся в нужном вам состоянии по структуре, а если вы задумаете делать новую базу «с нуля», то тогда вам не надо заставлять базу проходить весь тот путь, который вы прошли, дорабатывая её структуру в процессе разработки.

    Возможно, такой подход покажется кому-то менее надёжным, чем использование changelog-систем, которые заставляют вас думать о необходимых шагах по миграции данных при каждом изменении структуры и о встраивании таких шагов в changeset. Но если подумать, то становится понятно, что надёжность changelog-систем в этом плане – мнимая. Как известно, не бывает программ без потенциальных ошибок, это правило относится и к скриптам модификации данных. Тот факт, что на имеющемся у вас наборе данных скрипт модификации change set-а был отлажен и показал корректную работу, на самом деле не гарантирует со 100% уверенностью, что он выполнится без ошибок на любых данных. В случае с применением идемпотентного DDL мы по крайней мере не объявляем скрипт модификации данных не подлежащим изменению, защищённым контрольной суммой атрибутом ревизии. В случае, если ошибка всё же произойдёт, мы всегда можем повторять попытки апдейта до тех пор, пока не сведём систему к желаемой структуре. Ваши данные не будут потеряны, т. к. Celesta никогда автоматически не выполняет drop столбцов и таблиц, содержащих данные, оставляя эту операцию на выполнение вручную.

    * * *


    К сожалению, область применения CelestaSQL ограничена использованием его в паре с системой Celesta для создания кода бизнес-логики, поэтому именно для вашего проекта на сегодня, пожалуй, я бы порекомендовал Liquibase. Однако Celesta — проект открытый и одно из возможных направлений его развития – создание инструмента миграции структуры БД общего назначения.

    Хотя лично я бы предпочёл, чтобы разработчики БД когда-нибудь реализовали реальную поддержку идемпотентности и конвергентности DDL.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                • 0
                  Что делать, если мы, допустим, захотим добавить в непустую таблицу 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 значением, и сохранение какой-то истории, данных в буфер и т.д. Это так, к размышлению.
                  • 0
                    Относительно дропа: я об этом в статье не написал, но на мой взгляд, самым лучшим решением было бы во время выполнения CONVERGE дропать только такой столбец, который содержит NULL (или DEFAULT-константу) для всех строк. Иначе — ошибка CONVERGE, как и для всякой иной ситуации, когда наличествующие данные не позволяют произвести изменение структуры.

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

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

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.