Миграции БД для .NET

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


    Проблема


    База данных — это неотъемлемая часть вашего приложения. Если развернуть версию 2.0 этого самого приложения на базе данных версии 1.0, то, в общем случае, в итоге получится неработоспособная программа. Именно поэтому БД должна находится в системе контроля версий непосредственно рядом с исходниками программы.

    Вся соль проблемы заключается в том, что условие «непосредственно рядом» в случае с БД довольно непросто выполнить. Встает вопрос о том, что именно хранить в системе контроля версий. Всю БД? Совершенно бессмысленное занятие. Всю заскриптованную схему БД? Как тогда делать инкрементальные изменения существующих структур? Скрипты с инкрементальными изменениями? Очевидно, это и будет самым правильным решением, однако же как удостовериться в том, что скрипты применяются в правильной последовательности, ровно столько раз, сколько требуется, и ровно на тех базах данных, на которых это необходимо?

    Решение


    Вот в этот момент на сцену выходят специальные средства.

    Одно из них (начинается бессовестный пиар, ради которого все и затевалось) — octalforty Wizardby. Этот инструмент позволяет автоматизировать доведение схемы БД «до кондиции», сводя все необходимые манипуляции к двум вещам: написанию миграций и работе с консольным приложением, обрабатывающим упомянутые миграции.

    По сути, «миграция» — это инструкция, говорящая о том, как из схемы БД версии N сделать схему версии N + 1. Другими (совсем другими) словами:

    migration "Oxite" revision => 1:  <br>    version 20090323103239:<br>        add table oxite_Language:<br>            add column LanguageID type => Guid, nullable => false, primary-key => true<br>            add column LanguageName type => AnsiString, length => 8, nullable => false<br>            add column LanguageDisplayName type => String, length => 50, nullable => false<br><br>* This source code was highlighted with Source Code Highlighter.


    и далее:



    то в итоге:



    А вот если потом:

        version 20090330170528:<br>        oxite_User:<br>            UserID type => PK, primary-key => true<br>            Username type => LongName, unique => true<br>            DisplayName type => LongName<br>            Email type => LongName<br>            HashedEmail type => ShortName<br>            Password type => MediumName<br>            PasswordSalt type => MediumName<br>            DefaultLanguageID references => oxite_Language<br>            Status type => Byte, nullable => false<br>            <br>        oxite_UserLanguage:<br>            UserID references => oxite_User<br>            LanguageID references => oxite_Language<br><br>            index "" columns => [UserID, LanguageID], unique => true, clustered => true<br><br>* This source code was highlighted with Source Code Highlighter.


    то Wizardby все равно все сделает самостоятельно:


    Проект


    Страница проекта (лицензированного под MIT License) находится здесь. Исходный код доступен либо в виде ZIP-архива, либо в SVN репозитории. Кроме того, есть ZIP-архив с уже скомпилированной версией. Документация же (на английском) — тут. А здесь можно посмотреть, как Wizardby используется в «реальной жизни».

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 46

      0
      Не этот ли проект был недавно обсуждаем на РСДН?
        0
        Ага, он самый. Мнения?
          +4
          Ruby, какие еще мнения :)

          С одной стороны — более удобно, чем SQL/DDL, с другой — еще один DSL учить. Ну ни данные, ни объекты типа хранимок, констрейнтов и триггеров насколько я понимаю, мигрировать он не умеет. Или я что-то пропустил?
            +3
            Верно, ничего этого пока не умеет. Но научу.
            • UFO just landed and posted this here
        +1
        Не сильно вижу удобства пользования. Лично мне подобная тузла видится как генератор описаний структуры баз + аналог diff, но завязанный на структуру этих описаний. И дальше — обычный svn/любая другая VCS.

        Насколько я понимаю, именно diff-функционала у вас и нет :)

        А так я могу и ALTER-код писать, зато в родном, привычном SQL. Зачем мне учить ваш язык?
          0
          Первые два предложения недопонял. Diff-а действительно нет, да он и не нужен: вы, похоже, не с той стороны смотрите. Дифф — это вы, и именно вы описываете отличие N-й версии схемы от N+1-й.

          Что касается SQL DDL, то проблемы вот в чем: вам придется писать скрипты как для применения изменений, так и для их отмены. Кроме того, вручную, в тех же скриптах, проверять версию схемы чтобы ненароком не применить то, чего применять не следует. Да и ручное отслеживание версий (а-ля insert into TableVersion (Table, Version) values ('Foo', 23)) сильно захламляет код.
            0
            Исчо раз формулирую суть своих мыслей:

            1. Я при разработке не хочу задумываться о том, что у меня и где изменится. Я просто меняю и пишу код. Потом уже, на стадии сбора и коммита изменений — автоматика разбирается что где изменилось и что с этим делать, в спорных случаях — предлагает вам вручную резолвить конфликты.
            Вы же предлагаете подход, когда я задумываюсь об изменениях и пишу эти изменения — проецируя этот подход на код программы — можете представить, что вместо написания кода вы пишете diff'ы. Не кажется ли вам это несколько абсурдным?

            2. Отмена изменений нужна очень редко и в основном внедренцам. Для этого проще использовать бекапы и не заморачиваться. Для разработчиков же в принципе возникает лишь задача получить нужную ревизию (кода/базы), что легко решается созданием новой БД с накатом всех изменений вплоть до нужной ревизии. Тогда мы храним лишь изначальные CREATE'ы и кучу ревизионных ALTER'ов к ним. Даже тулзу отката в принципе можно таким образом сделать — анализировать все ALTER'ы между заданными ревизиями и генерить отменяющие ALTER'ы.
              0
              1. Если бы я писал диффы для кода — то да, это было бы посильнее «Фауста» Гёте. В случае с кодом мы просто записываем новое «состояние» (поскольку код по своей натуре stateless), а дифф создается автоматически и этот процесс детерминированный.

              В случае с БД мы просто обязаны описывать переходы между версиями. Это требует много дополнительной работы. Чаще всего требуется несколько итераций, и каждая итерация требует ручного написания кода — а это очевидные накладные расходы и работа, которую в принципе можно не делать. В случае с Wizardby это будет выглядеть так: написал миграцию, выполнил «wizardby u», попробовал; не понравилось — «wizardby ro» и пишем миграцию заново. Понравилось — коммитимся.

              2. Отмена изменений чаще всего нужна именно при разработке — выше я уже описал, как именно это может пригодиться.
                0
                Опять же — состояние БД жёстко завязано на состояние кода, соответственно ревизия кода = ревизии базы. Новую ревизию кода я могу писать не день и не два, и частично изменённый код нужен в разработке вместе с частично изменённой БД. Я пишу дифф на БД, другой программист пишет дифф на БД, я пишу ещё пять диффов к БД, потом выясняется, что я пошёл не тем путём и всё надо отменить. Как предлагается совмещать это с диффами другого программиста? Я же могу накатывать/откатывать только линейно. А если часть моего первого диффа на БД уже используется другим программистом?
                  0
                  Вообще говоря, это замечание имеет смысл и при «классической» схеме работы со схемой БД — с написанными врукопашную DDL-скриптами будут те же проблемы. А вообще, вот тут рассматривается простой пример совместной работы.

                  В итоге, по-моему, проблемы получаются идентичные. И все из-за того, что БД — она stateful и с этим надо считаться.
                    0
                    Проблемы идентичные, а зачем тогда новый язык? :)

                    Это я всё к тому, что нужен инструмент «снятия» состояния базы в текстовое описание (те же CREATE и т.д.) и ещё один инструмент для получения диффов из таких состояний. Стандартный diff тут не подойдёт, ибо на выходе надо генерить ALTER'ы. И тогда, на мой взгляд, всё будет просто, прозрачно и понятно :)
                      0
                      Ну ежели на то пошло, то вполне можно было бы ограничится одним-единственным Тюринг-полным языком — остановиться на каком-нибудь Лиспе и успокоиться.

                      Это, конечно, демагогия. Практически все новые средства в IT создаются с простой целью: повышение уровня абстракции, чтобы меньше времени и ресурсов тратить на бесполезные с точки зрения каждой конкретной проблемы действия (например, управление памятью в случае, когда не пишутся драйвера устройств и подобное низкоуровневое ПО) и сосредотачиваться на самом вопросе.

                      Вот собственно это средство и было создано с такой целью — пусть ненамного, но повысить уровень абстракции (к тому же, это всего лишь альфа-версия).

                      Что касается диффов схемы, то ситуация следующая: Visual Studio Team System 2008 Database Edition (традиционное для Microsoft название) пошла практически по этому пути. Но чистые диффы не получились даже у них: средство рефакторинга записывает последовательность действий, и потом воспроизводит их на целевой БД (это необходимо для, например, переименований). Но эта телега и стоит совершенно запредельно, и работает только с SQL Server'ом.
                        0
                        Если уж говорить о наращивании уровней абстракции, тогда уж лучше снимать все данные со слоя объектно-реляционного маппинга.

                        Вы не подумайте, я не против вашего решения, я просто до сих пор не могу представить себе ситуации, где его можно удобно применить, и при этом бы не существовало более удобных решений. Т.е. не могу ответить на вопрос «зачем?».
                          0
                          ORMы с диффами (на их счет, я надеюсь, мы пришли к консенсусу, что автоматические диффы для БД — практически утопия), насколько я знаю, дружат плохо.

                          А «зачем?»… Лично мне удобнее, в Рельсах подобное есть давно и разработчики не жалуются. То есть, жалуются, конечно, но в основном на то, что тамошние миграции слишком обобщенные — у DHH, к примеру, свой особый взгляд на ссылочную целостность и поэтому Foreign Key'и там не в фаворе.
                            0
                            В общем, я просто для себя понял, что лично мне оно бесполезно :)
                            0
                            Похожая утилита миграции оказалась очень удобной (я бы сказал, незаменимой), когда возникла необходимость обновлять десятки инсталляций разных версий системы с разными наборами плагинов на разных БД.
                            Автоматика справляется с генерацией скриптов вообще ужасно, потому что вместе с изменениями структуры таблиц нужно еще аккуратно (и максимально быстро) мигрировать сами данные.
                              0
                              да, но вам же фактически пришлось самому написать скрипт миграции. Аналогично можно было бы написать соответствующий DDL-код…
                                0
                                Да, приходится писать аналоги DDL-скриптов, но движок миграции решает кучу проблем. Как минимум, генерит правильный DDL для конкретной БД (а их использовалось штук 5 разных). Еще приятная фича — автоматические проверки позволяют корректно продолжить скрипт миграции, если при предыдущем запуске что-то помешало ему отработать до конца.
                                  0
                                  О, вот это уже преимущество. Хотя, многие субд позволяют и DDL выполнять в транзакции…
                                    0
                                    Да, но только не Oracle :(
                                      0
                                      Фигассе! Там правда этого нельзя?
                                        +1
                                        Угу, каждая DDL-операция в Oracle выполняется в отдельной транзакции, независимо от желания пользователя.
                                        • UFO just landed and posted this here
                                            0
                                            В смысле, завернуть внутрь транзакции? Не помогает: Oracle принудительно коммитит текущую транзакцию перед выполнением DDL.
                                            • UFO just landed and posted this here
                                                0
                                                Макс, и оставайся при нем.
                                                Каждую ддл, можно завернуть в транзакцию.
                                                А выше молодому человеку, я бы посоветовал воздержаться, если не в курсе.
                                                • UFO just landed and posted this here
            0
            Для подобных целей использую «dbForge Studio for MySQL»:
            www.devart.com/ru/dbforge/mysql/studio/

            Программа отлично сравнивает базы данных и генерирует скрипт для «обновления». Сравнения могут быть как по структуре (таблицы, процедуры, триггеры), так и по самим данным из таблиц. Русская версия бесплатная для частного использования.

            Сама же разработка и обновления ведутся с использованием «инкрементальных» обновлений. На сервере находится рабочая база, а также тестовая — для разработок. Как только приходит время, с помощью dbForge генерируется скрипт обновления рабочей базы до «тестовой» (т.е. новой версии). Сам скрипт сохраняется в отдельный файл вида «Обновление с версии 10 до 11.sql». Затем проверяется, для какой и до какой версии базы необходимо провести обновления — и вручную текст из соответствующих скриптов копируется в один файл для установки (получается что-то вроде «Обновление с версии 9 до 12.sql»).
              0
              Программа отлично сравнивает базы данных и генерирует скрипт для «обновления»
              Как она справляется с переименованиями?
              и вручную текст из соответствующих скриптов копируется в один файл для установки
              Залог успешной разработки ПО: сборка и развертывание должны производиться без ручного вмешательства, желательного нажатием красной кнопки в чемоданчике.
                0
                Переименования? Никак. Программа не может определить, что именно эту колонку или таблицу переименовали в другую. Максимум что можно сделать — после анализа различий исключить соответствующие объекты для генерации кода и вручную отредактировать получившийся скрипт.
              0
              Софтина хороша, но вот те кому посчасливилось иметь VS Team Suite может использовать Database Project GDR который войдёт в состав VS 2010, но в рамках программы лояльности MSFT выпустило обновление к VS 2008 но только для редакций Team Suite & Database Edition.

              Microsoft® Visual Studio Team System 2008 Database Edition GDR
                0
                Советую посмотреть на code.google.com/p/migratordotnet/ — миграции пишутся на C#
                  0
                  Видел его. Всё, что есть подобного для .NET, уже посмотрел. «Его писали не они», отчасти. Но в основном — слишком длительный цикл работы получается, нет той интерактивности. Пока в Студии класс добавишь, напишешь, скомпилируешь — сбивает с мысли.
                    0
                    Мы используем на проекте довольно успешно. Часто приходится добавлять кастомную логику для миграции, например изменить формат поля и т.п. Не представляю как это сделать с Wizardby. Также бесплатно получаем intellisense, знакомый синтаксис, возможность запуска миграций прямо в коде (из юнит-тестов, например)
                      0
                      Что есть «формат поля»? Если тип, то alter column Foo type => Int64, nullable => true. Дополнительную логику можно реализовывать с помощью дополнительного же SQL'я, но тогда теряется платформонезависимость.
                        0
                        Если эта логика пишется на C#, то ничего не теряется. Но сделать что-то вроде FullName = FirstName + " " + LastName приходится довольно часто и это самое простое, что может понадобиться.
                  0
                  Поддержка Oracle планируется? Когда? =)
                    0
                    Oracle будет точно. В релизной первой версии будет.
                    0
                    liquibase.org — не без недостатков софтина, но из всего что я видел (за исключением migrate-а на RoR) — самая стабильная.
                      0
                      Это Java (что, в принципе, терпимо), да еще и XML, от которого у меня в глазах рябит, используемый как язык программирования (а вот этого я терпеть не намерен).
                      0
                      Меня вот всегда интересовало в таких утилитах: а как делать миграции, сложнее добавления колонки/таблицы? Например объединение/разделение таблиц с переносом данных. Даже переименование столбца или изменение типа его данных не ясно как сделать.

                      Я считаю что самым правильным решением будет простая утилитка, умеющая прогонять в нужном порядке sql-скрипты, и хранящая прямо в базе — что уже прогоняли.

                      Собственно мы так и делаем. Делаешь какие-то изменения — кладешь на SVN в спец-папку sql-diff-скрипт. Ну и потом всякие ccnet этими дифами нужную базу поднимают до нужной ревизии.

                      Ну и там у нее всякие еще плюшки могут быть, типа хранимки по отдельным файликам держать, справочники всякие из CSV-файлов для тестов заливать и прочее. И можно даже, для простых случаев, какой-то DSL вроде твоего, вместо sql-скриптов.

                      Собственно есть вот статья с основными мыслями по теме: www.codinghorror.com/blog/archives/001050.html
                        0
                        И Атвуда и Скотт Аллена (и не только их) я читал и не осуждаю.

                        По поводу «сложнее добавления колонки»: всегда можно выполнить платформозависимый SQL (версия Wizardby в транке уже это позволяет) и в нем все сложные действия и сотворить. При желании можно написать и скрипт для отката.

                        Проблема с SQL скриптами следующая: лично я при разработке постоянно меняю структуру БД (функциональность добавляю постепенно, небольшими пачками) и то же пересоздание структуры (например, дропнуть таблицу с парой уникальных индексов, парой default constraint'ов и несколькмим foreign key'ами, а потом поменять порядок столбцов или целиком ее переделать) превращается либо в унылое дёргание мышкой и ожидание, пока там всё это произойдет, либо в быстрое «wizardby ro» в консоли, правку MDL-скрипта и потом «wizardby u».
                          0
                          Примеряя к моим лично задачам, я вижу что оно работало бы только на начальном этапе разработки (т.к. не умеет мигрировать данные) и только ограниченно: тестовые данные не зальешь, view-хи и прочие объекты делать не умеет, и т.п).

                          Ну а если уж кому-то повезло и у него вот такими вот простыми операциями все обходится, я бы посоветовал ему, например, апдейтить схему прямо при запуске приложения, по отмепленным классам.
                          Старая схема — в базе. Новая схема — из классов. Смотришь разницу и делаешь дифф-скрипт. Делов-то.
                            0
                            тестовые данные не зальешь, view-хи и прочие объекты делать не умеет, и т.п
                            С данными и представлениями можно работать обычными средствами — SQL-скриптами.

                            В планах есть идеи насчет импорта данных из CSV-файлов в таблицы и комплексные «рефакторинги» схемы БД. Например, на текущем месте работы именно такими рефакторингами поддерживаются таблицы аудита, которые, по сути, являются полными копиями исходных таблиц, но все поля — nullable и лежат они в другой схеме. То есть, при добавлении поля в исходную таблицу оно автоматически добавляется и в таблицу аудита.

                            Смотришь разницу и делаешь дифф-скрипт
                            Дифф-скрипты для схем БД без ручного вмешательства работать не будут.
                              0
                              Понятно что дифф-скрипты автоматически в общем случае сгенерить нельзя. Но вот для удаления/добавления полей/таблиц/ключей, т.е. для того, что сейчас умеет твой DSL — вполне можно их сгенерить автоматом.
                              Если будет импорт данных и рефакторинги — тогда уже другое дело будет. А пока мне удобнее и привычнее написать:
                              alter table oxite_Language add LanguageName nvarchar(8) null
                              вместо:
                              add column LanguageName type => AnsiString, length => 8, nullable => false

                              Первое, к слову, даже короче выходит.

                              И вот так еще можно:
                              sp_rename «dbo.oxite_Language.LanguageName» «LangName» «Column»

                      Only users with full accounts can post comments. Log in, please.