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, поэтому его должно быть легко импортировать и запускать как есть.