Каждый разработчик рано или поздно встречается с необходимостью миграции данных в базе данных. На нашем проекте мы используем mongoDB в качестве базы данных. Мы подходили к миграции данных разными способами:


  • писали js скрипты и запускали непосредственно в базе данных
  • использовали Mongobee ?-? инструмент для автоматических миграций


    Mongobee работал прекрасно до тех пор, пока мы не столкнулись с ситуацией, когда мы хотели добавить новое поле с уникальным индексом. Допустим, у нас имеется класс:


    @Document
    @Data
    public class TestDocument {
    
    @Id
    private String id;
    private String text;
    }

    Теперь мы добавляем новое поле:


    @Document
    @Data
    public class TestDocument {
    
    @Id
    private String id;
    private String text;
    @Indexed(unique = true)
    private String text2;
    }

    Мы написали миграцию, которая должна заполнить поле text2 во всех документах уникальными значениями:


    @ChangeLog
    public class TestDocumentChangelog {
    
    @ChangeSet(order = 1, id = "change1", author = "Stepan")
    public void changeset(MongoTemplate template) {
        template.findAll(Document.class, "testDocument").forEach(document -> {
            document.put("text2", UUID.randomUUID().toString());
            template.save(document, "testDocument");
        });
    }
    }

    Но когда мы запустили приложение, то увидели, что Spring запустил компоненты Mongo Data раньше, чем запустилась наша миграция, и поэтому при автоматическом создании индекса над полем text2 произошла ошибка, так как это поле еще не было заполнено ни у одного документа.
    После этого случая мы приняли решение отказаться от Mongobee и попробовать написать свой инструмент, который бы позволял также легко писать миграции, но в дополнение имел бы такие функции, как:


  • Запуск раньше Mongo Data
  • Поддержка транзакций, которые добавили в MongoDB 4.0
  • Внедрение зависимостей в классы ChangeLog

В итоге получилась библиотека под названием Mongration, которая поддерживает весь функционал, описанный выше.


Поддержка жизненного цикла Spring Boot


Первая функция реализована с помощью авто конфигурации, которая запускается после создания MongoClient и до сканирования репозиториев:


@Configuration
@AutoConfigureAfter(MongoAutoConfiguration.class)
@AutoConfigureBefore(MongoDataAutoConfiguration.class)
@Import(MongrationConfiguration.class)
public class MongrationAutoConfiguration {

}

Но проблема авто конфигурации в том, что если вы используете для активации репозиториев аннотацию @EnableMongoRepositories, то компоненты Mongo Data инициализируются раньше нашей авто конфигурации. Чтобы избежать этой проблемы, необходимо использовать аннотацию @EnableMongration вместе с @EnableMongoRepositories:


@Configuration
@EnableMongoRepositories
@EnableMongration
public class MongoRepositoriesConfiguration {

}

Аннотация @EnableMongration делает ни что иное, как запуск той же самой конфигурации, только позволяет запустить ее раньше:


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MongrationConfiguration.class)
public @interface EnableMongration {
}

Поддержка транзакций


Для поддержки транзакций необходимо сконфигурировать Replica Set MongoDB. Также необходимо объявить бин MongoTransationManager (если Mongration не найдет этот бин в контексте, то он создаст его самостоятельно). Mongration позволяет использовать транзакции двумя способами:


  • Используя @Transactional
    @Transactional
    @ChangeSet(order = 1, id = "change1", author = "Stepan")
    public void changeset(MongoTemplate template) {
    template.findAll(Document.class, "testDocument").forEach(document -> {
        document.put("text2", UUID.randomUUID().toString());
        template.save(document, "testDocument");
    });
    }
  • Используя TransactionTemplate
    @ChangeSet(order = 1, id = "change1", author = "Stepan")
    public void migration(MongoTemplate template, TransactionTemplate txTemplate) {
    template.createCollection("entity");
    txTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            template.save(new Document("index", "1").append("text", "1"), "entity");
            template.save(new Document("index", "2").append("text", "2"), "entity");
            template.save(new Document("index", "3").append("text", "3"), "entity");
        }
    });
    }


Второй способ полезен тем, что позволяет в одной миграции использовать как DDL операции, которые нельзя запустить в транзакции, так и DML операции, запускаемые в транзакции.


Внедрение зависимостей в ChangeLog классы


Этот функционал возможен благодаря тому, что каждый ChangeLog класс сам является обычным бином:


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ChangeLog {
}

Стоит заметить, что инжектить бины, у которых есть зависимости на компоненты Mongo Data нельзя, потому что к моменту выполнения миграций они еще не инициализированы.


Исходный код библиотеки доступен на Github.

Комментарии (2)


  1. viartemev
    16.05.2019 11:13

    А почему решили написать свою библиотеку вместо того чтобы добавить эти фичи в Mongobee?


    1. kuligin_stepan Автор
      16.05.2019 11:15

      Сначала и правда планировал так сделать, но после клонирования репозитория mongobee и простого обновления версий зависимостей столкнулся с кучей конфликтов, поэтому принял решение, что быстрее будет с нуля написать.