I usually post about Persistence on Twitter - you can follow me there:
1. Overview
This tutorial will continue to explore some of the core features of Spring Data MongoDB – the @DBRef annotation and lifecycle events.
2. @DBRef
The mapping framework doesn’t support storing parent-child relations and embedded documents within other documents. What we can do though is – we can store them separately and use a DBRef to refer between the documents.
When the object is loaded from MongoDB, those references will be eagerly resolved and we’ll get back a mapped object that looks the same as if it had been stored embedded within our master document.
Let’s look at some code:
@DBRef private EmailAddress emailAddress;
EmailAddress looks like:
@Document public class EmailAddress { @Id private String id; private String value; // standard getters and setters }
Note that the mapping framework doesn’t handle cascading operations. So – for instance – if we trigger a save on a parent, the child won’t be saved automatically – we’ll need to explicitly trigger the save on the child if we want to save it as well.
This is exactly where lifecycle events really come in handy.
3. Lifecycle Events
Spring Data MongoDB publishes some very useful lifecycle events – such as onBeforeConvert, onBeforeSave, onAfterSave, onAfterLoad and onAfterConvert.
To intercept one of the events, we need to register a subclass of AbstractMappingEventListener and override one of the methods here. When the event is dispatched, our listener will be called and domain object passed in.
3.1. Basic Cascade Save
Let’s look at the example we had earlier – saving the user with the emailAddress. We can now listen to the onBeforeConvert event which will be called before a domain object goes into the converter:
public class UserCascadeSaveMongoEventListener extends AbstractMongoEventListener<Object> { @Autowired private MongoOperations mongoOperations; @Override public void onBeforeConvert(final Object source) { if (source instanceof User && ((User) source).getEmailAddress() != null) { mongoOperations.save(((User) source).getEmailAddress()); } } }
Now we just need to register the listener into MongoConfig:
@Bean public UserCascadeSaveMongoEventListener userCascadingMongoEventListener() { return new UserCascadeSaveMongoEventListener(); }
Or as xml:
<bean class="org.baeldung.event.UserCascadeSaveMongoEventListener" />
And we have cascading semantics all done – albeit only for the user.
3.2. A Generic Cascade Implementation
Let’s now improve the previous solution by making the cascade functionality generic. Let’s start by defining a custom annotation:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface CascadeSave { // }
Let’s now work on our custom listener to handle these fields generically and not have to cast to any specific entity:
public class CascadeSaveMongoEventListener extends AbstractMongoEventListener<Object> { @Autowired private MongoOperations mongoOperations; @Override public void onBeforeConvert(Object source) { ReflectionUtils.doWithFields(source.getClass(), new CascadeCallback(source, mongoOperations)); } }
So we’re using the reflection utility out of Spring and we’re running our own callback on all fields that meet our criteria:
@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); } } }
As you can see, we’re looking for fields that have both the DBRef annotation as well as CascadeSave. Once we find these fields, we save the child entity.
Let’s look at the FieldCallback class which we’re using to check if the child has an @Id annotation:
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; } }
Finally, to make it all work togather, we of course need to emailAddress field to now be correctly annotated:
@DBRef @CascadeSave private EmailAddress emailAddress;
3.3. The Cascade Test
Let’s now have a look at a scenario – we save a User with emailAddress and the save operation cascades to this embedded entity automatically:
User user = new User(); user.setName("Brendan"); EmailAddress emailAddress = new EmailAddress(); emailAddress.setValue("b@gmail.com"); user.setEmailAddress(emailAddress); mongoTemplate.insert(user);
Let’s check our database:
{ "_id" : ObjectId("55cee9cc0badb9271768c8b9"), "name" : "Brendan", "age" : null, "email" : { "value" : "b@gmail.com" } }
4. Conclusion
In this article we illustrated some cool features of Spring Data MongoDB – the @DBRef annotation, lifecycle events and how we can handle cascading intelligently.
The implementation of all these examples and code snippets can be found in my github project – this is an Eclipse based project, so it should be easy to import and run as it is.