GrabDuck

spring data - mongodb @DBRef

:

mongoTemplate query by DBRef

db.user.find()

{... , "department" : DBRef("department", ObjectId("51f941bb3580d5d8b1ae97f9"))}

Query q = new Query(where("department.$id").is(new ObjectId("51f941bb3580d5d8b1ae97f9")));


Posted by Vijay Rawat on 

Recently I was watching a 

presentation titled Why I Chose MongoDB for guardian.co.uk by Mat Wall. I thought of giving a look to mongoDB. Those who dont know what is mongoDB, well, its a document oriented database with some advantages over other relational databases. For more info you can have a look at MongoDb website. While I was researching on how to access mongoDB easily and more efficiently I found out Spring Data. I also found some useful blogs explaining integration of both. But I was still not able to find a concrete working example which explains how to handle class relationship with MongoDB(DBRefs). So I thought of writing a complete working Spring MVC + Spring Data + MongoDB application handling DBRefs.

I have created a simple example with three domain classes User, Post and Comment. User can create Post and Post can have Comment(keeping it as simple as that). Below are the domain classes(of the example),

1

2

3

4

5

6

7

8

9

10

11

12

import org.springframework.data.annotation.Id;

import org.springframework.data.mongodb.core.mapping.Document;

 

@Document

public class User {

     @Id

     private String id;

     private String userName;

     private String password;

 

         //getters & setters

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

import org.springframework.data.annotation.Id;

 

@org .springframework.data.mongodb.core.mapping.Document

public class Post {

 

     @Id

     private String id;

     private String subject;

     private String content;

     @org .springframework.data.mongodb.core.mapping.DBRef

     private User user;

 

         //getters and setters

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import org.springframework.data.annotation.Id;

 

@org .springframework.data.mongodb.core.mapping.Document

public class Comment {

 

     @Id

     private String id;

     private String content;

     @org .springframework.data.mongodb.core.mapping.DBRef

     private User user;

     @org .springframework.data.mongodb.core.mapping.DBRef

     private Post post;

 

         //getters and setters

}

You can see how the classes are linked with each other using @DBRef annotation. Now in order to access MongoDB and we need to create some DAO classes. Well thats a piece of cake with Spring Data's MongoRepository. Just extend MongoRepository<T, X> with you DAO where T is class name and X is class used for creating the key.

1

2

3

4

5

6

7

import in.xebia.mongodb.blog.domain.User;

import org.springframework.transaction.annotation.Transactional;

 

@Transactional

public interface UserDao extends org.springframework.data.mongodb.repository.MongoRepository<User, String> {

 

}

1

2

3

4

5

6

7

import in.xebia.mongodb.blog.domain.Post;

import org.springframework.transaction.annotation.Transactional;

 

@Transactional

public interface PostDao extends org.springframework.data.mongodb.repository.MongoRepository<Post, String> {

 

}

1

2

3

4

5

6

7

import in.xebia.mongodb.blog.domain.Comment;

import org.springframework.transaction.annotation.Transactional;

 

@Transactional

public interface CommentDao extends org.springframework.data.mongodb.repository.MongoRepository<Comment, String>{

 

}

MongoRepository provides basic CRUD operations like findOne(),findALL(),save(),delete(), etc. but in order to handle DBRefs we need to make custom use of MongoTemplate as shown below in the CommentService class:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

import static org.springframework.data.mongodb.core.query.Criteria.where;

import in.xebia.mongodb.blog.api.CommentDao;

import in.xebia.mongodb.blog.api.CommentService;

import in.xebia.mongodb.blog.domain.Comment;

import in.xebia.mongodb.blog.domain.Post;

import in.xebia.mongodb.blog.domain.User;

import java.util.List;

import java.util.UUID;

import javax.annotation.Resource;

import org.springframework.data.mongodb.core.MongoTemplate;

import org.springframework.data.mongodb.core.query.Query;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

 

@Service ( "commentService" )

@Transactional

public class CommentServiceImpl implements CommentService {

     @Resource

     private CommentDao commentDao;

     @Resource

     private MongoTemplate mongoTemplate;

     public List<Comment> getAll() {

         List<Comment> comments = commentDao.findAll();

         return comments;

     }

     public Comment get(String id) {

         Comment comment = commentDao.findOne(id);

         return comment;

     }

     public Boolean add(Comment comment) {

         try {

             if (comment.getId()== null || comment.getId().length()== 0 )

                 comment.setId(UUID.randomUUID().toString());

            commentDao.save(comment);

            return true ;

           } catch (Exception e) {

               e.printStackTrace();

               return false ;

           }

     }

     public List<Comment> get(Post post) {

         Query query = new Query(where( "post.$id" ).is(post.getId()));

         List<Comment> comments = mongoTemplate.find(query, Comment. class );

         return comments;

     }

     public List<Comment> get(User user) {

         Query query = new Query(where( "user.$id" ).is(user.getId()));

         List<Comment> comments = mongoTemplate.find(query, Comment. class );

         return comments;

     }

 

}

Last but not the least here is the config file for mongo:

Thats all I have for this blog you can download the complete RESTful web-app from the following git repository: HandlingDBRefsInMongoDBWithSpringData. In order to use the web app you will require some tools like soapUI or Poster(mozilla addon) as there is no UI for application. Just publish the app and start making json requests(see example in readme.txt)

 

Have a nice day.

출처 - http://xebee.xebia.in/index.php/2011/09/20/handling-dbrefs-in-mongodb-with-spring-data/


Spring Data MongoDB by default does not support cascade operations on referenced objects with @DBRefannotations 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 ofAbstractMongoEventListener.

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

1

2

3

4

5

6

7

8

9

10

11

12

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’sDBOBject.

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

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:

1

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

Let’s test it

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@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

}

1

2

3

4

5

6

7

8

9

10

11

12

@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):