MongoDB For Java Developers - Sample Chapter
MongoDB For Java Developers - Sample Chapter
Francesco Marchioni
P U B L I S H I N G
pl
C o m m u n i t y
$ 34.99 US
22.99 UK
Sa
m
MongoDB for
Java Developers
ee
E x p e r i e n c e
D i s t i l l e d
MongoDB for
Java Developers
Design, build, and deliver efficient Java applications using
the most advanced NoSQL database
Francesco Marchioni
Preface
The NoSQL movement is growing in relevance, and it is attracting more and more
developers. The MongoDB database is a well-recognized rising star in the NoSQL
world. It is a document database that allows data to persist and query data in a
nested state without any schema constraint and complex joins between documents.
Understanding when it is appropriate to use MongoDB against a relational database
and the interfaces to be used to interact with it requires some degree of experience.
This book provides all the knowledge to make MongoDB fit into your application
schema, at the best of its capabilities. It starts from a basic introduction to the driver
that can be used to perform some low level interaction with the storage. Then it
moves on to use different patterns for abstracting the persistence layer into your
applications, starting from the flexible Google JSON library, to the Hibernate OGM
framework, and finally landing on the Spring Data framework.
Preface
Chapter 5, Managing Data Persistence with MongoDB and JPA, covers the development
of a Java Enterprise application using Hibernate Object/Grid Mapper (OGM), which
provides Java Persistence API (JPA) support for NoSQL databases.
Chapter 6, Building Applications for MongoDB with Spring Data, teaches you how
to use Spring Data and Spring Boot to leverage micro services using MongoDB
as the storage.
Chapter 3
MongoDB CRUD
Beyond the Basics
The previous chapter of this book took you through the first green bar in connecting
Java and MongoDB. You learned how to perform some basic create, read, update,
and delete operations using simple Java classes. It is now time to address some
advanced concerns, which are part of every real work application. Here is what we
are going to discuss in this chapter in detail:
[ 57 ]
When you move from the basics to a more complex project, you will find that this
approach requires writing lots of code and it is prone to runtime errors.
There are some solutions to this problem with different degrees of complexity. In this
chapter, we will account for some simple ones that require a minimal learning curve.
Later on, in Chapter 5, Managing Data Persistence with MongoDB and JPA, we will
describe how to use some frameworks that can let you persist Java objects directly
into MongoDB as documents, at the price of some enhanced complexity.
Here is what we are going to learn in the next section:
Using a Java library to translate Mongo documents into Java objects (and vice
versa) via JSON.
As you can see from the preceding diagram, the first choice is more flexible;
however, you need to provide some default implementation for the basic methods of
the collections mapped by your POJO. (For the sake of simplicity, only the two most
common methods, put and get, are indicated in the diagram.)
[ 58 ]
Chapter 3
[ 59 ]
[ 60 ]
Chapter 3
As you can see, we had to provide a default implementation for the methods
specified in the com.mongodb.DBObject interface. Now we'll insert our SimplePojo
class directly into our collection, as follows:
DB db = mongoClient.getDB( "sampledb" );
DBCollection coll = db.getCollection("pojo");
SimplePojo obj = new SimplePojo();
obj.put("user", "user1");
obj.put("message", "message");
obj.put("date", new Date());
coll.insert(obj);
Retrieving the Java class from the database is straightforward as well. First you need
to call setObjectClass on your collection to state that you are going to retrieve
objects of that type. Then you can use the finder methods of the collection as usual:
coll.setObjectClass(SimplePojo.class);
SimplePojo tw = (SimplePojo)coll.findOne();
System.out.println(tw.get("user"));
The major downside of this approach is that you have to provide some boilerplate
code with a default implementation of the com.mongodb.DBObject class. As an
alternative, you can consider extending the class com.mongodb.BasicDBObject,
which already contains a com.mongodb.DBObject default implementation. This will
avoid writing boilerplate code, at the price of lack of flexibility in your code. As a
matter of fact, you will not be able to extend any other class from your code.
Here is a rewritten version of SimplePojo that extends com.mongodb.
BasicDBObject and merely contains a business method to return an uppercased
version of a key requested:
package com.packtpub.mongo.chapter3;
import com.mongodb.BasicDBObject;
public class SimplePojo extends BasicDBObject {
public String getUpperCaseKey(String key) {
String value = (String) super.get(key);
if (value != null)
return value.toUpperCase();
[ 61 ]
In terms of implementation, nothing will change, and you can pass your Java classes
to the insert method of your collection, as you already know:
SimplePojo pojo = new SimplePojo();
pojo.put("user", "user2");
pojo.put("message", "msg");
pojo.put("date", new Date());
coll.insert(pojo);
[ 62 ]
Chapter 3
If you are using Maven, then you can include the following dependency in your pom.
xml file (see the next chapter for more details about using Maven in your projects):
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3.1</version>
</dependency
As evident from the following document query, a document has been created in the
javastuff collection:
> db.javastuff.find().pretty()
{
"_id" : ObjectId("55266979d3d5368080f97f92"),
"name" : "owen",
"age" : 25,
"email" : "[email protected]",
"phone" : "321-456-778"
}
Now let's create a simple Java bean that will be able to contain this document
structure:
public class Customer {
String name;
[ 63 ]
Being based on simple Java types such as string and int, mapping the MongoDB
document to the Customer Java class is a piece of cake. Let's see how to do it:
Gson gson = new Gson();
DBObject doc = new BasicDBObject("name", "owen");
DBObject obj = coll.findOne(doc);
Customer c = gson.fromJson(obj.toString(), Customer.class);
System.out.println("Found customer " + c);
The expected output will be the toString() method of the Customer class that
dumps the fields contained in the class:
Customer [name=owen, age=25, [email protected],
phone=321-456-778]
[ 64 ]
Chapter 3
In the above example, the JSON string mapping the Customer class is stored in the
string JSON. You can then use the static parse method of the com.mongodb.util.
JSON class to convert the JSON string into a DBObject type.
The inserted structure will be as follows:
> db.javastuff.find({"name":"john"}).pretty()
{
"_id" : ObjectId("55268359d3d51c80bdb231b5"),
"name" : "john",
"age" : 22,
"email" : "[email protected]",
"phone" : "777-666-555"
}
[ 65 ]
[ 66 ]
Chapter 3
public Info(int age, String email, String phone) {
super();
this.email = email;
this.phone = phone;
this.age = age;
}
public Info( ) {
}
String email;
String phone;
int age;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Info [email=" + email + ", phone=" + phone + ",
age=" + age + "]";
}
}
}
[ 67 ]
In the new class called CustomerInfo, we have highlighted the fields that will be
mapped by MongoDB keys.
As you are aware, creating embedded documents in MongoDB can be done by setting
a key to a DBObject structure containing the embedded document. In our case, we
will structure the info key to contain the embedded document's information:
BasicDBObject doc = new BasicDBObject("name",
"owen").append("info", new BasicDBObject("age",
25).append("email", "[email protected]").append("phone",
"321-456-778"));
coll.insert(doc);
DBObject obj = coll.findOne(doc);
CustomerInfo c = gson.fromJson(obj.toString(),
CustomerInfo.class);
System.out.println("Found customer " + c);
The expected output should match the following query executed through the
mongo shell:
> db.javastuff.find({"name":"owen"}).pretty()
{
"_id" : ObjectId("5526888bd3d56a86cea8ea12"),
"name" : "owen",
"info" : {
"age" : 25,
"email" : "[email protected]",
"phone" : "321-456-778"
}
}
Chapter 3
import com.google.gson.annotations.SerializedName;
public class Customer {
@SerializedName("name")
String userName;
@SerializedName("age")
int userAge;
@SerializedName("email")
String userEmail;
@SerializedName("phone")
String userPhone;
}
In the next section, we will deal with a more complex concern, that is, mapping
complex BSON types used by MongoDB to store entries.
You might think that adding the _id field to the Customer class will do the job of
mapping Mongo's _id key:
public Customer(Object _id, String name, int age, String email,
String phone) {
super();
this._id = _id;
this.name = name;
this.age = age;
[ 69 ]
Let's see what happens if we try to deserialize the preceding document by using the
fromJson method:
Customer c = gson.fromJson(obj.toString(), Customer.class);
System.out.println(c);
What we are trying to achieve is the following representation of the Customer class:
{_id=558c1007578ef44c4cbb0eb8, name=owen, age=25,
[email protected], phone=321-456-778}
However, as you can see from the following output, the _id object was not correctly
parsed as we expected:
_id={$oid=5527b117d3d511091e1735e2}, name=owen, age=22,
[email protected], phone=777-666-555
Chapter 3
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
public class CustomAdapter implements JsonDeserializer<Customer> {
public Customer deserialize(JsonElement json,
java.lang.reflect.Type typeOfT, JsonDeserializationContext
context) throws JsonParseException {
JsonObject jObj = json.getAsJsonObject();
String id =
jObj.get("_id").toString().replaceAll(".*\"(\\w+)\"}",
"$1");
String name = jObj.get("name") != null ?
jObj.get("name").getAsString() : "";
String email = jObj.get("email")!= null ?
jObj.get("email").getAsString() : "";
String phone = jObj.get("phone")!= null ?
jObj.get("phone").getAsString() : "";
int age = jObj.get("age")!= null ? jObj.get("age").getAsInt()
: 0;
return new Customer(id,name,age,email,phone);
}
}
As you can see, this class contains the deserialization logic in the method
deserialize, where each field is parsed according to your custom parsing rules.
In this case, the value of the _id field is extracted using a regular expression, which
scans for the identifier contained in the parentheses.
Please note that using a schemaless database implies a lack of
control over the data contained in your collection. As you can
see, we had to check against null on each field of our document.
Some changes will also be required in your main Java class, so that you register the
adapter on the Gson class, by means of the registerTypeAdapter method contained
in the com.google.gson.GsonBuilder class:
GsonBuilder builder=new GsonBuilder();
Gson gson = new GsonBuilder().registerTypeAdapter(Customer.class, new
CustomAdapter()).create();
[ 71 ]
Now the toString output of the Customer class reveals that you have been able to
parse the $id field correctly:
_id=5527b117d3d511091e1735e2, name=owen, age=22,
[email protected], phone=777-666-555
[ 72 ]
Chapter 3
coll.insert(doc);
}
Here, we are inserting 1,00,000 documents in one go. Once the insertion completes,
we can move to the mongo shell and execute the explain function to see what
happens behind the scenes when mongo performs a query:
> db.indextest.find({userid: 50000}).explain("executionStats")
{
"queryPlanner":{
"plannerVersion":1,
"namespace":"sampledb.indextest",
"indexFilterSet":false,
"parsedQuery":{
"userid":{
"$eq":"1111"
}
},
"winningPlan":{
"stage":"COLLSCAN",
"filter":{
"userid":{
"$eq":"1111"
}
},
"direction":"forward"
},
"rejectedPlans":[
]
},
"executionStats":{
"executionSuccess":true,
"nReturned":0,
"executionTimeMillis":6,
"totalKeysExamined":0,
"totalDocsExamined":100000,
"executionStages":{
[ 73 ]
]
},
"serverInfo":{
. . . .
}
}
[ 74 ]
Chapter 3
Besides this, you might instruct the Mongo cursor to stop looking through other
documents once an occurrence is found using the limit(1) operator (which is also
available through the Java driver). That could be helpful but may not be exactly what
you are looking for in your search.
In the next section, we will see how to use an index to limit the number of documents
to be scanned.
If you have been using the MongoDB Java driver in its earlier version,
you might have used the ensureIndex method to create an index if
that is not available. This method is now deprecated and you have to
use createIndex as shown.
Now, let's execute the explain plan query again and examine the result (we will
show you just the relevant part of the statistics):
> db.indextest.find({userid: 5000}).explain("executionStats")
{
. . . .
"executionStats":{
"executionSuccess":true,
"nReturned":0,
"executionTimeMillis":0,
"totalKeysExamined":1,
"totalDocsExamined":1,
"executionStages":{
"stage":"FETCH",
"nReturned":0,
"executionTimeMillisEstimate":0,
"works":1,
[ 75 ]
[ 76 ]
Chapter 3
"seenInvalidated":0,
"matchTested":0
}
},
"allPlansExecution":[
]
},
. . .
The explain() output is now a bit more complex; let's focus on the fields we are
interested in. The number of totalDocsExamined documents is just one and the
query is now instantaneous as the index named userid_1 has been used. However,
everything has its flip side. In this case, we will have super-fast queries at the price
of slower inserts/updates as indexes have to be rewritten too. More storage has to
be planned also since indexes will need it. However, that is now a peculiarity of
MongoDB, but it is a clear assumption that is true on every database.
For the sake of completeness, we will mention that the explain function is also
available on the Java side, by calling the explain method directly on a search string:
BasicDBObject doc = new BasicDBObject("userid", "1111");
DBObject explainObject = coll.find(doc).explain();
System.out.println(explainObject) ;
In this case, having defined the index on the userid field, this is not helping our
query too much, as the index will come into play only after scanning the first key,
that is, code. A solution, in this case, could be to create a compound index that is a
handy solution if your search contains multiple criteria.
[ 77 ]
The following sample diagram illustrates a compound index using two fields, such
as userid and code:
As you can see from the preceding figure, in a Compound Index, a single index
structure holds references to multiple fields (userid and code) within a collection's
documents.
Creating a Compound Index is not very different from creating a single index field.
Using the fluent API provided by the BasicDBObject, you can append the keys and
then create the index with that object:
DBObject obj = new BasicDBObject();
obj.put("userid", 1);
obj.put("code", 1);
coll.createIndex(obj);
You can run the Compound Index creation and verify that the search cursor is using
the Compound Index and scanning only one document:
> db.indextest.find({userid: 5000, code:5000}).explain("executionStats")
{
. . . .
"executionStats":{
[ 78 ]
Chapter 3
"executionSuccess":true,
"nReturned":0,
"executionTimeMillis":0,
"totalKeysExamined":1,
"totalDocsExamined":1,
"executionStages":{
"stage":"FETCH",
"nReturned":0,
"executionTimeMillisEstimate":0,
"works":1,
"advanced":0,
"needTime":0,
"needFetch":0,
"saveState":0,
"restoreState":0,
"isEOF":1,
"invalidates":0,
"docsExamined":0,
"alreadyHasObj":0,
"inputStage":{
"stage":"IXSCAN",
"nReturned":0,
"executionTimeMillisEstimate":0,
"works":1,
"advanced":0,
"needTime":0,
"needFetch":0,
"saveState":0,
"restoreState":0,
"isEOF":1,
"invalidates":0,
"keyPattern":{
"userid":1,
"code":1
[ 79 ]
In order to perform queries using the text index, you need to use the $text query
operator. In the following example, we are creating a text index on the content key:
MongoClient mongoClient = new MongoClient("localhost", 27017);
DB db = mongoClient.getDB("sampledb");
[ 80 ]
Chapter 3
DBCollection coll = db.getCollection("textitems");
coll.createIndex(new BasicDBObject("content", "text"));
coll.insert(new BasicDBObject().append("content", "mytext other
content"));
DBObject search = new BasicDBObject("$search", "mytext");
DBObject textSearch = new BasicDBObject("$text", search);
int count = coll.find(textSearch).count();
System.out.println("Found text search matches: " + count);
Once the index has been created, we will use the $text operator to perform a text
search on the collection, using the string of words contained in the $search operator:
MongoClient mongoClient = new MongoClient("localhost", 27017);
DB db = mongoClient.getDB("sampledb");
DBCollection coll = db.getCollection("textitems");
coll.insert(new BasicDBObject("_id", 1).append("text", "mytext"));
List<DBObject> list = coll.getIndexInfo();
for (DBObject obj:list)
System.out.println(obj);
}
The method getIndexInfo returns a list of the indexes for this collection
as DBObject. This information is printed on the console, which in our case,
outputs the following:
{ "v" : 1 , "key" : { "_id" : 1} , "name" : "_id_" , "ns" :
"sampledb.textitems"}
[ 81 ]
Here is a full example that shows how to restrict your searches only to English words
by means of the $language operator:
MongoClient mongoClient = new MongoClient("localhost", 27017);
DB db = mongoClient.getDB("sampledb");
DBCollection coll = db.getCollection("textitems");
coll.createIndex(new BasicDBObject("textcontent", "text"));
coll.insert(new BasicDBObject("_id", 0).append("textcontent",
"Some data"));
coll.insert(new BasicDBObject("_id", 1).append("textcontent",
"Other data"));
coll.insert(new BasicDBObject("_id", 2).append("textcontent", "Not
important"));
BasicDBObject search = new BasicDBObject("$search", "data");
DBObject textSearch = new BasicDBObject("$text",
search.append("$language", "english"));
int matchCount = coll.find(textSearch).count();
System.out.println("Found lanuguagage matches: " + matchCount);
The following example shows how to return the score in a search by means of the
metadata associated with the query:
DBObject scoreSearch = new BasicDBObject("score", new
DBObject("$meta", "textScore"));
[ 82 ]
Chapter 3
DBObject doc = coll.findOne(textSearch, scoreSearch);
System.out.println("Highest scoring document: "+ doc);
Ordered bulk operations: Every operation will be executed in the order they
are added to the bulk operation, halting when there's an error
As you can see, an instance of the BulkWriteOperation class is created using the
initializeOrderedBulkOperation method of the collection class. Operations are
added using the fluent API available in the BulkWriteOperation.
[ 83 ]
Finally, the BulkWriteResult is used as a wrapper that contains the results of the
Bulk.execute() method.
The same code written using an unordered bulk operation can be coded this way:
BulkWriteOperation builder =
collection.initializeUnorderedBulkOperation();
builder.insert(new BasicDBObject("item", "A1"));
builder.insert(new BasicDBObject("item", "A2"));
builder.insert(new BasicDBObject("item", "A3"));
builder.find(new BasicDBObject("item", "A1")).updateOne(new
BasicDBObject("$set", new BasicDBObject("A1", "AX")));
BulkWriteResult result = builder.execute();
Chapter 3
By running the above example, 10,000 inserts are performed in 7421 ms. Let's
reengineer the code to use bulk operations:
long l1 = System.currentTimeMillis();
BulkWriteOperation builder = coll.initializeOrderedBulkOperation();
for (int ii=0;ii<10000;ii++) {
DBObject doc = new BasicDBObject("name","frank")
.append("age", 31)
.append("info", new BasicDBObject("email",
"[email protected]").append("phone", "222-111-444"));
builder.insert(doc);
}
BulkWriteResult result = builder.execute();
long l2 = System.currentTimeMillis();
System.out.println(l2-l1);
This will bring down the time to even less, that is, 1446 ms to insert 10,000 records.
Summary
In this chapter, we have gone through some advanced features available in the
MongoDB Java driver, and we examined some strategies for mapping Java objects
to MongoDB documents. The solutions we have covered will fit into many practical
scenarios. We now need to change the perspective of our examples a bit as we
move into an enterprise context. In the next chapter, we will show you how to use
the knowledge we have built so far to create a Java Enterprise application using
MongoDB as the storage.
[ 85 ]
www.PacktPub.com
Stay Connected: