Java, Scala, .NET, Lisp, Python, IDE's, Hibernate, MATLAB, Mathematica, Physics & Other

воскресенье, 17 мая 2015 г.

Liquibase для начальной инициализации базы данных

Допустим стоит задача инициализации структуры реляционной базы данных и заливки в нее неких первоначальных данных. Плюс хотим поддерживать несколько типов баз, скажем для тестов нужна база в памяти H2 а для продакшена что-то более серьезное (типа Oracle / PostgreSQL / MySQL etc). Решая данную задачу в лоб, можно под кажду базу написать отдельно скрипты инициализации ее структуры и команды загрузки данных. Решение простое, но не красивое, потому что в большинстве своем эти скрипты будут себя дублировать примерно полностью, за исключением небольших различий в синтаксисе для разных баз.

Эту задачу можно красиво решить с помощью тулзы Liquibase. И вот каким образом. Вкратце - она позволяет описывать структуру базы в неком своем формате xml. Потом этот xml транслируется в инструкции для выбранной базы, причем поддерживаются множество
реляционных баз (возможно все?). Причем изменения описываются в виде ченжсетов (changeset) и ликвибейз ведет учет уже примененных ченджсетов. Таким образом, можно
в процессе разработки дописывать новые ченжсеты (скажем по 1 на задачу) и ликвибейз сам будет применять только новые на ваших тестовых энвах, без нужды не пересоздавая базу.

Это все теория, а теперь перейдем к практике. Допустим у нас есть типичное Maven, Spring приложение. Несколько связанных таблиц и начальные данные для этих таблиц.

Добавляем зависимость на ликвибейз.

<dependency>
   <groupId>org.liquibase</groupId>
   <artifactId>liquibase-core</artifactId>
   <version>3.3.3</version>
</dependency>

Проверьте только и возьмете самую последнюю версию.

Добавляем бин ликвибейза в спринговый контекст:

<bean id="liquibase" class="liquibase.integration.spring.SpringLiquibase">
   <property name="dataSource" ref="dataSource" />
   <property name="changeLog" value="classpath:liquibase/db.changelog-master.xml" />
</bean>

В файле db.changelog-master.xml:
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

    <include file="liquibase/init.xml"/>
</databaseChangeLog>
В файле init.xml:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

    <changeSet id="Create/Update tables" author="brunneng" runOnChange="true">

        <!-- Drop tables if exists -->
        <sql splitStatements="true">
            DROP TABLE IF EXISTS "Region" CASCADE;
            DROP TABLE IF EXISTS "City" CASCADE;
        </sql>

        <!-- Create tables -->
        <createTable tableName="Region">
            <column name="nID" type="int">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="sName" type="varchar(50)">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <addAutoIncrement columnName="nID" tableName="Region" columnDataType="int" incrementBy="1" startWith="1000"/>

        <createTable tableName="City">
            <column name="nID" type="int">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="sName" type="varchar(50)">
                <constraints nullable="false"/>
            </column>
            <column name="nID_Region" type="int">
                <constraints nullable="false"
                             foreignKeyName="FK_Region_City"
                             referencedTableName="Region" referencedColumnNames="nID" deleteCascade="true"/>
            </column>
        </createTable>
        <addAutoIncrement columnName="nID" tableName="City" columnDataType="int" incrementBy="1" startWith="1000"/>
      
        <!-- Loading of Data -->
        <loadData encoding="UTF-8" file="data/Region.csv" tableName="Region" separator=";">
            <column name="nID" type="NUMERIC"/>
            <column name="sName" type="STRING"/>
        </loadData>
        <loadData encoding="UTF-8" file="data/City.csv" tableName="City" separator=";">
            <column name="nID" type="NUMERIC"/>
            <column name="sName" type="STRING"/>
            <column name="nID_Region" type="NUMERIC"/>
        </loadData>       
    </changeSet>


</databaseChangeLog>
В файле Region.csv:

nID;sName
1;Дніпропетровська
2;Львівська
3;Івано-Франківська
4;Миколаївська
5;Київська
6;Херсонська

И в файле City.csv:

nID;sName;nID_Region
1;Дніпропетровськ;1
2;Кривий Ріг;1
3;Львів;2
4;Івано-Франківськ;3
5;Калуш;3
6;Київ;5
7;Херсон;6

Все эти файлы пакуются вместе с приложением и должны быть доступны как ресурсы в classpath по своим относительным путям.

Как можно сразу заметить, ликвибейз позволяет загружать данные из csv файлов, что согласитесь, намного удобнее чем писать insert-ы вручную.
А теперь как это работает... Тут объявлен 1 ченжсет в котором делаются 3 вещи:

1. Удаляются все таблицы если они существуют
2. Создаются все таблицы
3. Заливаются данные

Теперь, если мы впервые запустим наше приложение, то ликвибейз сразу определит какие ченсеты не были применены (пока ни одного) и применит наш только что объявленный ченсет. При повторном запуске приложения на той же базе, если в определении ченсета ничего не менялось, то ликвибейз не будет ничего делать. Если же что либо изменилось, а именно:

- добавилась новая таблица
- изменилось определение существующей таблицы
- изменились данные в csv файлах

То для этого у нас была выставлен параметр: runOnChange="true", а значит ликвибейз перезапустит наш ченсет и вся база переинициализируется.

Единственная небольшая проблема тут - это удаление таблицы, если она существует. К сожалению у ликвибейза нету инстукции (более подходящее название для того что у них называется change) для этого. А то, что есть dropTable упадет с исключением при попытке удалить не существующую таблицу. Поэтому была использована инструкция sql в котором можно выполнить любой sql какой вам захочется. В нашем случае синтаксис удаления таблицы если она существует совпал для H2 и PostgreSQL - повезло. Но если у вас не совпадет, то у инструкции sql можно задать параметр dbms - для какой базы этот sql, и создать 2 блока sql для разных баз с разными значениями этого параметра.

Постоянные читатели