Каждый разработчик рано или поздно встречается с необходимостью миграции данных в базе данных. На нашем проекте мы используем 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.