Spring Data MongoDB cascade save on DBRef objects

Spring Data MongoDB by default does not support cascade operations on referenced objects with @DBRef annotations as reference says:

The mapping framework does not handle cascading saves. If you change an Account object that is referenced by a Person object, you must save the Account object separately. Calling save on the Person object will not automatically save the Account objects in the property accounts.

That’s quite problematic because in order to achieve saving child objects you need to override save method in repository in parent or create additional “service” methods like it is presented in here.

In this article I will show you how it can be achieved for all documents using generic implementation of AbstractMongoEventListener.

@CascadeSave annotation

Because we can’t change @DBRef annotation by adding cascade property lets create new annotation @CascadeSave that will be used to mark which fields should be saved when parent object is saved.

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

Next part is to implement handler for this annotation. We will use for that powerful Spring Application Event mechanism. In particular we will extend AbstractMongoEventListener to catch saved object before it is converted to Mongo’s DBObject.

How does it work? When object MongoTemplate#save method is called, before object is actually saved it is being converted into DBObject from MongoDB api. CascadingMongoEventListener implemented below provides hook that catches object before its converted and:

  • goes through all its fields to check if there are fields annotated with @DBRef and @CascadeSave at once.
  • when field is found it checks if @Id annotation is present
  • child object is being saved
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;
      }
  }
}

Mapping requirements

As you can see in order to make thing work you need to follow some rules:

  • parent’s class child property has to be mapped with @DBRef and @CascadeSave
  • child class needs to have property annotated with @Id and if that id is supposed to be autogenerated it should by type of ObjectId

Usage

In order to use cascade saving in your project you need just to register CascadingMongoEventListener in Spring Context:

<bean class="pl.maciejwalkowiak.springdata.mongodb.CascadingMongoEventListener" />

Let’s test it

In order to show an example I made two document classes:

@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
}

In test there is one user with address created and then user is saved. Test will cover only positive scenario and its just meant to show that it actually works (applcationContext-tests.xml contains only default Spring Data MongoDB beans and CascadingMongoEventListener registered):

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

We can check that also in Mongo console:

> 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" }

Summary

With this simple solution we can finally save child objects with one method call without implementing anything special for each document class.

I believe that we will find this functionality together with cascade delete as part Spring Data MongoDB release in the future. Solution presented here works but:

  • it requires to use additional annotation
  • uses reflection API to iterate through fields which is not the fastest way to do it (but feel free to implement caching if needed)

If that could be part of Spring Data MongoDB instead of additional annotation @DBRef could have additional property cascade. Instead of reflection we could use MongoMappingContext together with MongoPersistentEntity. I’ve started already to prepare pull request with those changes. We will see if it will be accepted by Spring Source team.
 

Related Whitepaper:

Professional NoSQL

A hands-on guide to leveraging NoSQL databases!

NoSQL databases are an efficient and powerful tool for storing and manipulating vast quantities of data. Most NoSQL databases scale well as data grows. In addition, they are often malleable and flexible enough to accommodate semi-structured and sparse data sets. This comprehensive hands-on guide presents fundamental concepts and practical solutions for getting you ready to use NoSQL databases. Expert author Shashank Tiwari begins with a helpful introduction on the subject of NoSQL, explains its characteristics and typical uses, and looks at where it fits in the application stack. Unique insights help you choose which NoSQL solutions are best for solving your specific data storage needs.

Get it Now!  

Leave a Reply


7 − = four



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use | Privacy Policy
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.

Sign up for our Newsletter

20,709 insiders are already enjoying weekly updates and complimentary whitepapers! Join them now to gain exclusive access to the latest news in the Java world, as well as insights about Android, Scala, Groovy and other related technologies.

As an extra bonus, by joining you will get our brand new e-books, published by Java Code Geeks and their JCG partners for your reading pleasure! Enter your info and stay on top of things,

  • Fresh trends
  • Cases and examples
  • Research and insights
  • Two complimentary e-books