Patrik has posted 9 posts at DZone. View Full User Profile

Using MongoDB With Sculptor

05.02.2010
| 24738 views |
  • submit to reddit

This article describes what Sculptor provides for efficient development of a enterprise or web application with a rich domain model stored in MongoDB. MongoDB bridges the gap between key-value stores (which are fast and highly scalable) and traditional RDBMS systems (which provide rich queries and deep functionality).

Sculptor is a productivity tool that let you express your design intent in a textual DSL, from which Sculptor generates high quality Java code and configuration.

Sculptor generates data mapper classes that converts domain objects to/from MongoDB data structures, DBObjects. This makes it easy to use a domain model à la DDD with automatic mapping to MongoDB data structures.

Sculptor provides generic repository operations for use with MongoDB. This includes operations such as save, delete, findById, findByKey, findByCondition, and some more. You get CRUD operations, and GUI, for free.

Queries can be expressed with a slick fluent api that support code completion and refactoring.

Rich support for associations. Aggregates are stored as a embedded documents. Other associations are stored with referring ids. In the domain objects there are generated getters that lazily fetch associated objects from the ids. This means that you don't have to work with the ids yourself, you can follow associations as usual.

A sample blog application may look like this when defined in Sculptor textual DSL:

Visualization, generated by Sculptor, of the above model looks like this:

Data Mapper

When working with mongoDB Java API the DBObject plays a central role. It is like a key value Map. Values can be of most types and also collections and other DBObjects for nested documents. Sculptor also adds support for Enums and Joda date time classes.

Implementing mapping of domain objects to DBObjects is a tedious and error prone programming task that is a perfect candidate for automation. Sculptor comes in handy for this. Sculptor generates data mapper classes that converts domain objects to DBObjects. It is useful to not have to write those mappers by hand.

        Entity BlogPost {
            String slug key
            String title
            String body
        }

From this Sculptor generates domain object class in java with appropriate getters, setters, constructors, equals, etc. Sculptor also generates a mapper for converting the BlogPost to/from DBObject. The generated code looks like this:

    public DBObject toData(BlogPost from) {
        if (from == null) {
            return null;
        }

        DBObject result = new BasicDBObject();

        if (from.getId() != null) {
            ObjectId objectId = ObjectId.massageToObjectId(from.getId());
            result.put("_id", objectId);
        }

        result.put("slug", from.getSlug());
        result.put("title", from.getTitle());
        result.put("body", from.getBody());
        
        return result;
    }

and in the other direction:

    public BlogPost toDomain(DBObject from) {
        if (from == null) {
            return null;
        }

        String slug = (String) from.get("slug");

        BlogPost result = new BlogPost(slug);

        if (from.containsField("_id")) {
            ObjectId objectId = (ObjectId) from.get("_id");
            String idString = objectId.toStringMongod();
            IdReflectionUtil.internalSetId(result, idString);
        }

        if (from.containsField("title")) {
            result.setTitle((String) from.get("title"));
        }
        if (from.containsField("body")) {
            result.setBody((String) from.get("body"));
        }

        return result;
    }

I'm happy that I don't have to write and especially maintain that kind of code. The generation is not a one time shot. When you change the model the domain objects and the mappers are regenerated.

Domain objects can of course also have behavior, otherwise it wouldn't be a rich domain model. The behavior logic is always written manually. Separation of generated and manually written code is done by a generated base class and manually written subclass, a gap class. It is in the subclass you add methods to implement the behavior of the domain object. The subclass is also generated, but only once, it will never be overwritten by the generator.

You might need to add some hand written code to customize the mappers. That is easy. In the model you can specify that you need a subclass that you can implement yourself. This is very useful for doing data migration.

It can also be good to know that fields marked with transient are are not stored, but they are loaded if they exist in the retrieved documents. This can also be used for data migration.

By default the names in the data store are the same as the names in the Java domain objects. In case you need to use other names it is possible to define that in model with databaseTable and databaseColumn.

        Entity BlogPost {
            databaseTable="BlogEntries"
            String slug key
            String title
            String body databaseColumn="content"
        }

You have probably selected MongoDB for its low latency. Then you want the data mapper to be fast also. The mappers provided by Sculptor are generated code that runs at full speed. Alternative solutions using reflection or intermediate String JSON format is probably not as fast.

Associations

MongoDB is not a relational database and there is no such thing as joins. You can still use associations between domain objects. Associations can be of two main categories, either embedded or reference by id.

Aggregates

Aggregates are one of the core building blocks in Domain-Driven Design (DDD). MongoDB has perfect support for aggregates, since an associated object can be stored as an embedded document, i.e. it belongs to parent object and cannot be shared between several objects.

Let us repeat what DDD says about aggregates:

An Aggregate is a group of associated objects which are considered as one unit with regard to data changes. The Aggregate is demarcated by a boundary which separates the objects inside from those outside. Each Aggregate has one root. The root is an Entity, and it is the only object accessible from outside. The root can hold references to any of the aggregate objects, and the other objects can hold references to each other, but an outside object can hold references only to the root object. - from DDD Quickly

Sculptor will validate the reference constraints described in the quote above. Repositories are only available for aggregate roots. Aggregates are defined with belongsTo or not aggregateRoot in the owned DomainObjects.

A typical aggregate in the blog sample is that Comment belongs to BlogPost

        Entity BlogPost {
            String slug key
            String title
            String body
            DateTime published nullable
            - List<comment> comments opposite forPost
        }

        ValueObject Comment {
            not aggregateRoot
            - BlogPost forPost opposite comments
            String title
            String body
        }
</comment>

Reference by Id

The other alternative is to store ids of the referred objects. In the domain objects there are generated getters that lazily fetch associated objects from the ids. This means that you don't have to work with the ids yourself, you can follow associations as usual, but be aware that an invocation of such a getter might need to query the database.

    Set<author> writers = blog.getWriters();
</author>

In the same way you can modify unowned associations by working with objects rather than ids.

        Author pn = new Author("Patrik");
        pn = authorService.save(getServiceContext(), pn);
        blog.addWriter(pn);
        Author ak = new Author("Andreas");
        ak = authorService.save(getServiceContext(), ak);
        blog.addWriter(ak);
        blogService.save(getServiceContext(), blog);

Referential integrity of the stored ids are not enforced. It must be handled by your program. Lazy getters of associations will not fail if referred to object is missing, they will return null for single value references and ignore missing objects for collection references. This means that you can cleanup dangling references by fetching objects, populate associations by invoking the getters and then save the object.

Inheritance

The MongoDB feature of Sculptor has full support for inheritance. It is even possible to do polymorphic queries.

        abstract Entity Media {
            String title !changeable
            
            Repository MediaRepository {
                List<@Media> findByTitle(String title);
                protected findByCondition;
            }
        }
        
        Entity Book extends @Media {
            String isbn key length="20"
        }
        
        Entity Movie extends @Media {
            String urlIMDB key
            Integer playLength
            - Genre category nullable
        }

 

Repository

In the visualization of the blog sample above you maybe noticed that there are Services with findById, findAll, save and delete even though they were not explicitly defined in the model. They come from that the domain objects are marked with with scaffold. That automatically generates some predefined CRUD operations in the Repository and corresponding Service.

    Entity BlogPost {
            scaffold
            - Blog inBlog
            String slug key
            String title
            String body
            DateTime published nullable
            Set<String> tags
            - Author writtenBy
            - List<Comment> comments opposite forPost
    }

You can also define the needed operations explicitly like this:

    Entity BlogPost {
            - Blog inBlog
            String slug key
            String title
            String body
            DateTime published nullable
            Set<String> tags
            - Author writtenBy
            - List<Comment> comments opposite forPost

            Repository BlogPostRepository { 
                save;
                delete;
                findAll(PagingParameter pagingParameter);
                findById;
                findByKey;
                List<@BlogPost> findPostsInBlog(@Blog blog);
                List<@BlogPost> findPostsWithComments => AccessObject;
                List<@BlogPost> findPostsWithTags(Set<String> tags);
                protected findByCondition;
            }

You don't need to do any manual coding for the built in operations, such as save, delete, findAll, findById, findByKey. findByKey is for the natural key, i.e. the attributes marked with key (slug above).

There is good support for pagination and sorting.

As you see in the above sample it is also possible to define your own repository operations, such as findPostsWithTags.

findByCondition

The most interesting finder is findByCondition. Queries can expressed with an internal DSL that support code completion and refactoring. It is best illustrated with a few samples.

    public List<BlogPost> findPostsInBlog(Blog blog) {
        List<ConditionalCriteria> condition = criteriaFor(BlogPost.class)
            .withProperty(inBlog()).eq(blog)
            .orderBy(published()).descending()
            .build();
        return findByCondition(condition);
    }
    public List<BlogPost> findPostsWithGreatComments() {
        List<ConditionalCriteria> condition = criteriaFor(BlogPost.class)
            .withProperty(comments().title()).ignoreCaseLike(".*great.*")
            .and().withProperty(published()).isNotNull()
            .orderBy(published()).descending().build();
        return findByCondition(condition);
    }

The ConditionalCriteriaBuilder has support for defining queries with eq, like, between, lessThan, greaterThan, in, and, not, orderBy, and some more.

You can of course also work directly with the underlaying MongoDB DBCollection and DBObject for doing advanced queries that might not be supported by findByCondition.

Try It

More hands on instructions of how to use Sculptor is available in the Sculptor MongoDB Tutorial.

Published at DZone with permission of its author, Patrik Nordwall.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Piero Sartini replied on Sun, 2010/05/02 - 2:35pm

Looks interesting, but personally I don't like to express my domain in anything other than Java. It's just another syntax to learn and IDE support is lost as well. Refactorings become much more pain and there is no syntax highlighting. Also I need to run the code generator and explain all this to my team.

For people like me, Morphia may be a good alternative:

-> http://code.google.com/p/morphia

Morphia is a Object Mapper for MongoDB. It also provides DAOs which make using it plain fun.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.