Spring Data MongoDB 自定义级联数据
1. 概述
本教程将继续探索 Spring Data MongoDB 的一些核心特性—— @DBRef注解和生命周期事件。
2. @DBRef
映射框架不支持在其他文档中存储父子关系和嵌入文档。不过我们可以做的是——我们可以单独存储它们并使用DBRef来引用文档。
当从 MongoDB 加载对象时,这些引用将被急切地解析,我们将返回一个映射对象,它看起来就像它已嵌入存储在我们的主文档中一样。
让我们看一些代码:
@DBRef
private EmailAddress emailAddress;
EmailAddress看起来像:
@Document
public class EmailAddress {
@Id
private String id;
private String value;
// standard getters and setters
}
请注意,映射框架不处理级联操作。因此——例如——如果我们在父节点上触发save,子节点将不会自动保存——如果我们也想保存子节点,我们需要显式触发对子节点的保存。
这正是生命周期事件派上用场的地方。
3. 生命周期事件
Spring Data MongoDB 发布了一些非常有用的生命周期事件——例如onBeforeConvert、onBeforeSave、onAfterSave、onAfterLoad和onAfterConvert。
要拦截其中一个事件,我们需要注册一个AbstractMappingEventListener的子类并覆盖这里的其中一个方法。当事件被调度时,我们的监听器将被调用并传入域对象。
3.1. 基本级联保存
让我们看一下我们之前的示例——使用emailAddress保存User。我们现在可以监听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="com.blogdemo.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 user = new User();
user.setName("Ann");
EmailAddress emailAddress = new EmailAddress();
emailAddress.setValue("Ann@itcodingman.com");
user.setEmailAddress(emailAddress);
mongoTemplate.insert(user);
让我们检查一下我们的数据库:
{
"_id" : ObjectId("55cee9cc0badb9271768c8b9"),
"name" : "Ann",
"age" : null,
"email" : {
"value" : "Ann@itcodingman.com"
}
}