Перейти к основному содержимому

Пользовательское каскадирование в Spring Data MongoDB

· 4 мин. чтения

1. Обзор

В этом руководстве мы продолжим изучение некоторых основных функций Spring Data MongoDB — аннотации @DBRef и событий жизненного цикла.

2. @DBRef

Платформа сопоставления не поддерживает сохранение отношений родитель-потомок и встроенных документов в других документах. Что мы можем сделать, так это хранить их отдельно и использовать DBRef для ссылки на документы.

Когда объект загружается из MongoDB, эти ссылки будут быстро разрешены, и мы вернем сопоставленный объект, который выглядит так же, как если бы он был сохранен в нашем мастер-документе.

Давайте посмотрим на некоторый код:

@DBRef
private EmailAddress emailAddress;

Адрес электронной почты выглядит так:

@Document
public class EmailAddress {
@Id
private String id;

private String value;

// standard getters and setters
}

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

Именно здесь и пригодятся события жизненного цикла .

3. События жизненного цикла

Spring Data MongoDB публикует некоторые очень полезные события жизненного цикла, такие как onBeforeConvert, onBeforeSave, onAfterSave, onAfterLoad и onAfterConvert.

Чтобы перехватить одно из событий, нам нужно зарегистрировать подкласс AbstractMappingEventListener и переопределить один из методов здесь. Когда событие будет отправлено, будет вызван наш слушатель и передан объект домена.

3.1. Базовое каскадное сохранение

Давайте посмотрим на пример, который у нас был ранее — сохранение пользователя с адресом электронной почты . Теперь мы можем прослушивать событие onBeforeConvert , которое будет вызвано до того, как объект домена попадет в преобразователь:

public class UserCascadeSaveMongoEventListener extends AbstractMongoEventListener<Object> {
@Autowired
private MongoOperations mongoOperations;

@Override
public void onBeforeConvert(BeforeConvertEvent<Object> event) {
Object source = event.getSource();
if ((source instanceof User) && (((User) source).getEmailAddress() != null)) {
mongoOperations.save(((User) source).getEmailAddress());
}
}
}

Теперь нам просто нужно зарегистрировать слушателя в MongoConfig :

@Bean
public UserCascadeSaveMongoEventListener userCascadingMongoEventListener() {
return new UserCascadeSaveMongoEventListener();
}

Или как XML:

<bean class="org.foreach.event.UserCascadeSaveMongoEventListener" />

И у нас есть каскадная семантика, хотя и только для пользователя.

3.2. Общая каскадная реализация

Теперь давайте улучшим предыдущее решение, сделав каскадную функциональность универсальной. Начнем с определения пользовательской аннотации:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CascadeSave {
//
}

Давайте теперь поработаем над нашим пользовательским слушателем , чтобы он обрабатывал эти поля в общем и не приводил их к какому-либо конкретному объекту:

public class CascadeSaveMongoEventListener extends AbstractMongoEventListener<Object> {

@Autowired
private MongoOperations mongoOperations;

@Override
public void onBeforeConvert(BeforeConvertEvent<Object> event) {
Object source = event.getSource();
ReflectionUtils.doWithFields(source.getClass(),
new CascadeCallback(source, mongoOperations));
}
}

Итак, мы используем утилиту отражения из Spring и выполняем наш обратный вызов для всех полей, которые соответствуют нашим критериям:

@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(field);

if (field.isAnnotationPresent(DBRef.class) &&
field.isAnnotationPresent(CascadeSave.class)) {

Object fieldValue = field.get(getSource());
if (fieldValue != null) {
FieldCallback callback = new FieldCallback();
ReflectionUtils.doWithFields(fieldValue.getClass(), callback);

getMongoOperations().save(fieldValue);
}
}
}

Как видите, мы ищем поля, которые имеют как аннотацию DBRef , так и CascadeSave . Как только мы находим эти поля, мы сохраняем дочернюю сущность.

Давайте посмотрим на класс FieldCallback , который мы используем, чтобы проверить, есть ли у дочернего элемента аннотация @Id :

public class FieldCallback implements ReflectionUtils.FieldCallback {
private boolean idFound;

public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(field);

if (field.isAnnotationPresent(Id.class)) {
idFound = true;
}
}

public boolean isIdFound() {
return idFound;
}
}

Наконец, чтобы все это работало вместе, нам, конечно же, нужно, чтобы поле emailAddress теперь было корректно аннотировано:

@DBRef
@CascadeSave
private EmailAddress emailAddress;

3.3. Каскадный тест

Давайте теперь посмотрим на сценарий — мы сохраняем пользователя с помощью emailAddress , и операция сохранения автоматически каскадируется на этот встроенный объект:

User user = new User();
user.setName("Brendan");
EmailAddress emailAddress = new EmailAddress();
emailAddress.setValue("b@gmail.com");
user.setEmailAddress(emailAddress);
mongoTemplate.insert(user);

Давайте проверим нашу базу данных:

{
"_id" : ObjectId("55cee9cc0badb9271768c8b9"),
"name" : "Brendan",
"age" : null,
"email" : {
"value" : "b@gmail.com"
}
}

4. Вывод

В этой статье мы проиллюстрировали некоторые интересные функции Spring Data MongoDB — аннотацию @DBRef , события жизненного цикла и то, как мы можем разумно обрабатывать каскадирование.

Реализацию всех этих примеров и фрагментов кода можно найти на GitHub — это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.