Custom Cascading in Spring Data MongoDB

1. Overview

This tutorial will continue to explore some of the core features of Spring Data MongoDB – the @DBRef annotation and life-cycle 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 to 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 precisely where life cycle events come in handy.

3. Lifecycle Events

Spring Data MongoDB publishes some very useful life cycle 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(BeforeConvertEvent<Object> event) { 
        Object source = event.getSource(); 
        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.maixuanviet.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 particular entity:

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));
    }
}

So we’re using the reflection utility out of Spring, and we’re running our 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 a @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 together, 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, life cycle events and how we can handle cascading intelligently.

The implementation of all these examples and code snippets can be found over on GitHub – this is a Maven based project, so it should be easy to import and run as it is.

Related posts:

Jackson – Change Name of Field
Java Program to Implement Regular Falsi Algorithm
Java Program to Implement Miller Rabin Primality Test Algorithm
Java Program to Search for an Element in a Binary Search Tree
Guava Collections Cookbook
Spring @RequestMapping New Shortcut Annotations
How to Iterate Over a Stream With Indices
Guide to DelayQueue
Giới thiệu Google Guice – Injection, Scope
Lập trình đa luồng trong Java (Java Multi-threading)
Tránh lỗi NullPointerException trong Java như thế nào?
Spring Security Registration – Resend Verification Email
Guide to the Fork/Join Framework in Java
Converting Between Byte Arrays and Hexadecimal Strings in Java
How to Change the Default Port in Spring Boot
Java Program to Check Whether Topological Sorting can be Performed in a Graph
The SpringJUnitConfig and SpringJUnitWebConfig Annotations in Spring 5
Redirect to Different Pages after Login with Spring Security
Jackson – Decide What Fields Get Serialized/Deserialized
Guide to Spring Cloud Kubernetes
Serverless Functions with Spring Cloud Function
Phương thức forEach() trong java 8
Guide to the Java TransferQueue
Java Program to Find the Longest Subsequence Common to All Sequences in a Set of Sequences
Getting Started with Custom Deserialization in Jackson
XML Serialization and Deserialization with Jackson
Hướng dẫn Java Design Pattern – Adapter
Introduction to Liquibase Rollback
Tránh lỗi NullPointerException trong Java như thế nào?
Java Program to Implement D-ary-Heap
Copy a List to Another List in Java
RegEx for matching Date Pattern in Java