默认情况下, Spring Data MongoDB不支持对带有@DBRef注释的引用对象的级联操作,如引用所述 :
映射框架不处理级联保存 。 如果更改了Person对象引用的Account对象,则必须单独 保存 Account对象。 在Person对象上调用save 不会自动将Account对象保存在属性帐户中。
这很成问题,因为要实现保存子对象,您需要覆盖父存储库中的save方法或创建其他“服务”? 这里介绍了类似的方法。
在本文中,我将向您展示如何使用AbstractMongoEventListener的通用实现针对所有文档实现此目标。
@CascadeSave批注
由于我们无法通过添加级联属性来更改@DBRef批注,因此可以创建新的批注@CascadeSave,该批注将用于标记保存父对象时应保存哪些字段。
package pl.maciejwalkowiak.springdata.mongodb;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface CascadeSave {
}
CascadingMongoEventListener
下一部分是实现此批注的处理程序。 我们将使用强大的Spring Application Event机制 。 特别是,我们将扩展AbstractMongoEventListener以捕获已保存的对象,然后再将其转换为Mongo的DBObject 。
它是如何工作的? 调用对象MongoTemplate #save方法时,在实际保存对象之前,会将其从MongoDB api转换为DBObject。 下面实现的CascadingMongoEventListener提供了在对象转换之前捕获对象的钩子,并且:
- 仔细检查其所有字段,以检查是否同时有@DBRef和@CascadeSave注释的字段。
- 找到字段时,它将检查是否存在@Id批注
- 子对象正在保存
package pl.maciejwalkowiak.springdata.mongodb;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
public class CascadingMongoEventListener extends AbstractMongoEventListener {
@Autowired
private MongoOperations mongoOperations;
@Override
public void onBeforeConvert(final Object source) {
ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(field);
if (field.isAnnotationPresent(DBRef.class) && field.isAnnotationPresent(CascadeSave.class)) {
final Object fieldValue = field.get(source);
DbRefFieldCallback callback = new DbRefFieldCallback();
ReflectionUtils.doWithFields(fieldValue.getClass(), callback);
if (!callback.isIdFound()) {
throw new MappingException("Cannot perform cascade save on child object without id set");
}
mongoOperations.save(fieldValue);
}
}
});
}
private static class DbRefFieldCallback 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;
}
}
}
映射要求
如您所见,为了使工作正常,您需要遵循一些规则:
- 父类的子级属性必须使用@DBRef和@CascadeSave进行映射
- 子类需要具有以@Id注释的属性,如果应该自动生成该ID,则应按ObjectId的类型
用法
为了在项目中使用级联保存,您只需要在Spring Context中注册CascadingMongoEventListener:
<bean class="pl.maciejwalkowiak.springdata.mongodb.CascadingMongoEventListener" />
让我们测试一下
为了显示一个示例,我制作了两个文档类:
@Document
public class User {
@Id
private ObjectId id;
private String name;
@DBRef
@CascadeSave
private Address address;
public User(String name) {
this.name = name;
}
// ... getters, setters, equals hashcode
}
@Document
public class Address {
@Id
private ObjectId id;
private String city;
public Address(String city) {
this.city = city;
}
// ... getters, setters, equals hashcode
}
在测试中,有一个创建了地址的用户,然后保存了该用户。 测试将仅涵盖积极的情况,并且仅用于表明它确实有效( applcationContext-tests.xml仅包含默认的Spring Data MongoDB Bean和已注册的CascadingMongoEventListener):
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applcationContext-tests.xml"})
public class CascadingMongoEventListenerTest {
@Autowired
private MongoOperations mongoOperations;
/**
* Clean collections before tests are executed
*/
@Before
public void cleanCollections() {
mongoOperations.dropCollection(User.class);
mongoOperations.dropCollection(Address.class);
}
@Test
public void testCascadeSave() {
// given
User user = new User("John Smith");
user.setAddress(new Address("London"));
// when
mongoOperations.save(user);
// then
List<User> users = mongoOperations.findAll(User.class);
assertThat(users).hasSize(1).containsOnly(user);
User savedUser = users.get(0);
assertThat(savedUser.getAddress()).isNotNull().isEqualTo(user.getAddress());
List<Address> addresses = mongoOperations.findAll(Address.class);
assertThat(addresses).hasSize(1).containsOnly(user.getAddress());
}
}
我们也可以在Mongo控制台中进行检查:
> db.user.find()
{ "_id" : ObjectId("4f9d1bab1a8854250a5bf13e"), "_class" : "pl.maciejwalkowiak.springdata.mongodb.domain.User", "name" : "John Smith", "address" : { "$ref" : "address", "$id" : ObjectId("4f9d1ba41a8854250a5bf13d") } }
> db.address.find()
{ "_id" : ObjectId("4f9d1ba41a8854250a5bf13d"), "_class" : "pl.maciejwalkowiak.springdata.mongodb.domain.Address", "city" : "London" }
摘要
通过这种简单的解决方案,我们最终可以通过一个方法调用保存子对象,而无需为每个文档类实现任何特殊的功能。
我相信,将来作为Spring Data MongoDB版本的一部分,我们会在级联删除中找到该功能。 这里介绍的解决方案有效,但:
- 它需要使用其他注释
- 使用反射API遍历字段,这不是最快的方法(但可以根据需要随意实现缓存)
如果这可以是Spring Data MongoDB的一部分,而不是附加的注释,则@DBRef可以具有附加的cascade属性。 除了反射之外,我们可以将MongoMappingContext和MongoPersistentEntity一起使用。 我已经开始准备带有这些更改的请求请求。 我们将看看它是否将被Spring Source团队接受。
参考: Spring Data MongoDB级联保存在我们的JCG合作伙伴 Maciej Walkowiak的“ 软件开发之旅”博客上的DBRef对象上。
翻译自: https://www.javacodegeeks.com/2013/11/spring-data-mongodb-cascade-save-on-dbref-objects.html