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:

Java Program to Implement Dijkstra’s Algorithm using Queue
Registration – Activate a New Account by Email
Shuffling Collections In Java
HashSet trong Java hoạt động như thế nào?
Java Program to Find Median of Elements where Elements are Stored in 2 Different Arrays
Java Program to Implement Karatsuba Multiplication Algorithm
Java Program to Perform Searching in a 2-Dimension K-D Tree
Spring Boot - Logging
Introduction to Liquibase Rollback
Java Program to Implement Pagoda
Jackson Annotation Examples
Tìm hiểu cơ chế Lazy Evaluation của Stream trong Java 8
Cachable Static Assets with Spring MVC
Tính trừu tượng (Abstraction) trong Java
Java Program to Solve the Fractional Knapsack Problem
Java Program to Represent Graph Using Linked List
Java Program to Implement Heap Sort Using Library Functions
Introduction to Spring Data REST
Java Program to Implement Merge Sort on n Numbers Without tail-recursion
Java 8 – Powerful Comparison with Lambdas
Tổng quan về ngôn ngữ lập trình java
Java Program to Implement Sorted Circular Doubly Linked List
Java Program to find the maximum subarray sum using Binary Search approach
Create a Custom Exception in Java
Spring MVC and the @ModelAttribute Annotation
Spring Cloud – Securing Services
Java Program to Implement Quick sort
Java Program to Implement Best-First Search
Java Program to Generate Randomized Sequence of Given Range of Numbers
Java Map With Case-Insensitive Keys
Checking for Empty or Blank Strings in Java
Java Program to Check whether Graph is a Bipartite using 2 Color Algorithm