Quick Start: Java and MongoDB - Read Operations

Maxime Beugnet

#Java
Quick Start Java and MongoDB

In a previous blog post, I showed you how to create documents in MongoDB using Java. In this blog post, I will show you how to read these documents.

Getting Set Up

Just like in the previous blog post, I will use the same repository. If you don't have a copy of it yet, you can clone it:

git clone https://github.com/mongodb-developer/java-quick-start

If you have previously cloned this repository, just make sure you are using the last version:

git pull

If you didn't set up your free cluster on MongoDB Atlas, now is great time to do so. You have all the instructions in this blog post.

Created Data

In the previous blog post, we created the class Create. This time we will work in the Read class.

We wrote 11 new grades, one for the student with {"student_id": 10000} and 10 for the student with {"student_id": 10001} in the sample_training.grades collection.

As a reminder, here are the grades of the {"student_id": 10000}.

MongoDB Enterprise Cluster0-shard-0:PRIMARY> db.grades.findOne({"student_id":10000})
{
	"_id" : ObjectId("5daa0e274f52b44cfea94652"),
	"student_id" : 10000,
	"class_id" : 1,
	"scores" : [
		{
			"type" : "exam",
			"score" : 39.25175977753478
		},
		{
			"type" : "quiz",
			"score" : 80.2908713167313
		},
		{
			"type" : "homework",
			"score" : 63.5444978481843
		},
		{
			"type" : "homework",
			"score" : 82.35202261582563
		}
	]
}

We also discussed BSON types in the previous blog post and we noted that student_id and class_id are doubles.

MongoDB treats some types as equivalent for comparison purposes. For instance, numeric types undergo conversion before comparison.

So do not be surprised in this blog post if I filter with an integer number and match a document which contains a double number for example. If you want to filter documents by value types, you can use the $type operator.

You can read more about type bracketing and comparison and sort order in our documentation.

Read with a Filter

Let's read the document above. To achieve this, we will use the method find.

Please create a class Read in the com.mongodb.quickstart package with this code:

package com.mongodb.quickstart;

import com.mongodb.client.*;
import org.bson.Document;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.mongodb.client.model.Filters.*;
import static com.mongodb.client.model.Projections.*;
import static com.mongodb.client.model.Sorts.descending;

public class Read {

    private static final Random rand = new Random();

    public static void main(String[] args) {
        Logger.getLogger("org.mongodb.driver").setLevel(Level.WARNING);
        try (MongoClient mongoClient = MongoClients.create(System.getProperty("mongodb.uri"))) {
            MongoDatabase sampleTrainingDB = mongoClient.getDatabase("sample_training");
            MongoCollection<Document> gradesCollection = sampleTrainingDB.getCollection("grades");

            // find one document with new Document
            Document student1 = gradesCollection.find(new Document("student_id", 10000)).first();
            System.out.println("Student 1: " + student1.toJson());
        }
    }
}

Also, make sure you set up your mongodb.uri in your system properties using your IDE if you want to run this code in your favorite IDE.

Alternatively, you can use this maven command line in your root project (where the src folder is):

mvn compile exec:java -Dexec.mainClass="com.mongodb.quickstart.Read" -Dmongodb.uri=mongodb+srv://USERNAME:PASSWORD@cluster0-abcde.mongodb.net/test?w=majority

The standard output should be:

Student 1: {"_id": {"$oid": "5daa0e274f52b44cfea94652"}, 
    "student_id": 10000.0, 
    "class_id": 1.0, 
    "scores": [
        {"type": "exam", "score": 39.25175977753478}, 
        {"type": "quiz", "score": 80.2908713167313}, 
        {"type": "homework", "score": 63.5444978481843}, 
        {"type": "homework", "score": 82.35202261582563}
    ]
}

The MongoDB driver comes with a few helpers to ease the writing of these queries. Here is an equivalent query using the Filters.eq() method.

gradesCollection.find(eq("student_id", 10000)).first();

Of course, I used a static import to make the code as compact and easy to read as possible.

import static com.mongodb.client.model.Filters.eq;

Read a Range of Documents

In the previous example, the benefit of these helpers is not obvious, but let me show you another example where I'm searching all the grades with a student_id greater than or equal to 10,000.

// without helpers
gradesCollection.find(new Document("student_id", new Document("$gte", 10000)));
// with the Filters.gte() helper
gradesCollection.find(gte("student_id", 10000));

As you can see, I'm using the $gte operator to write this query. You can learn about all the different query operators in the MongoDB documentation.

Iterators

The find method returns an object that implements the interface FindIterable which ultimately extends the Iterable interface so we can use an iterator to go through the list of documents we are receiving from MongoDB:

FindIterable<Document> iterable = gradesCollection.find(gte("student_id", 10000));
MongoCursor<Document> cursor = iterable.iterator();
System.out.println("Student list with cursor: ");
while (cursor.hasNext()) {
    System.out.println(cursor.next().toJson());
}

Lists

Lists are usually easier to manipulate than iterators, so we can also do this to retrieve directly an ArrayList<Document>:

List<Document> studentList = gradesCollection.find(gte("student_id", 10000)).into(new ArrayList<>());
System.out.println("Student list with an ArrayList:");
for (Document student : studentList) {
    System.out.println(student.toJson());
}

Consumers

We could also use a Consumer which is a functional interface:

Consumer<Document> printConsumer = document -> System.out.println(document.toJson());
gradesCollection.find(gte("student_id", 10000)).forEach(printConsumer);

Cursors, Sort, Skip, Limit, and Projections

As we saw above with the Iterator example, MongoDB leverages cursors to iterate through your result set.

If you are already familiar with the cursors in the Mongo Shell, you know that transformations can be applied to it. A cursor can be sorted and the documents it contains can be transformed using a projection. Also, once the cursor is sorted, we can choose to skip a few documents and limit the number of documents in the output. This is very useful to implement pagination in your frontend for example.

Let's combine everything we have learnt in one query:

List<Document> docs = gradesCollection.find(and(eq("student_id", 10001), lte("class_id", 5)))
                                                  .projection(fields(excludeId(),
                                                                     include("class_id", 
                                                                             "student_id")))
                                                  .sort(descending("class_id"))
                                                  .skip(2)
                                                  .limit(2)
                                                  .into(new ArrayList<>());

System.out.println("Student sorted, skipped, limited and projected: ");
for (Document student : docs) {
    System.out.println(student.toJson());
}

Here is the output we get:

{"student_id": 10001.0, "class_id": 3.0}
{"student_id": 10001.0, "class_id": 2.0}

I remind you that documents are returned in the natural order, so if you want your output ordered, you need to sort your cursors to make sure there is no randomness in your algorithm.

Indexes

If you want to make these queries (with or without sort) efficient, you need indexes!

To make my last query efficient, I should create this index:

db.grades.createIndex({"student_id": 1, "class_id": -1})

When I run an explain on this query, this is the winning plan I get:

"winningPlan" : {
			"stage" : "LIMIT",
			"limitAmount" : 2,
			"inputStage" : {
				"stage" : "PROJECTION_COVERED",
				"transformBy" : {
					"_id" : 0,
					"class_id" : 1,
					"student_id" : 1
				},
				"inputStage" : {
					"stage" : "SKIP",
					"skipAmount" : 2,
					"inputStage" : {
						"stage" : "IXSCAN",
						"keyPattern" : {
							"student_id" : 1,
							"class_id" : -1
						},
						"indexName" : "student_id_1_class_id_-1",
						"isMultiKey" : false,
						"multiKeyPaths" : {
							"student_id" : [ ],
							"class_id" : [ ]
						},
						"isUnique" : false,
						"isSparse" : false,
						"isPartial" : false,
						"indexVersion" : 2,
						"direction" : "forward",
						"indexBounds" : {
							"student_id" : [
								"[10001.0, 10001.0]"
							],
							"class_id" : [
								"[5.0, -inf.0]"
							]
						}
					}
				}
			}
		}

With this index, we can see that we have no SORT stage so we are not doing a sort in memory as the documents are already sorted "for free" and returned in the order of the index.

Also, we can see that we don't have any FETCH stage so this is a covered query, the most efficient type of query you can run in MongoDB. Indeed, all the information we are returning at the end are already in the index, so the index itself contains everything we need to answer this query.

The Final Code

package com.mongodb.quickstart;

import com.mongodb.client.*;
import org.bson.Document;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.mongodb.client.model.Filters.*;
import static com.mongodb.client.model.Projections.*;
import static com.mongodb.client.model.Sorts.descending;

public class Read {

    private static final Random rand = new Random();

    public static void main(String[] args) {
        Logger.getLogger("org.mongodb.driver").setLevel(Level.WARNING);
        try (MongoClient mongoClient = MongoClients.create(System.getProperty("mongodb.uri"))) {
            MongoDatabase sampleTrainingDB = mongoClient.getDatabase("sample_training");
            MongoCollection<Document> gradesCollection = sampleTrainingDB.getCollection("grades");

            // find one document with new Document
            Document student1 = gradesCollection.find(new Document("student_id", 10000)).first();
            System.out.println("Student 1: " + student1.toJson());

            // find one document with Filters.eq()
            Document student2 = gradesCollection.find(eq("student_id", 10000)).first();
            System.out.println("Student 2: " + student2.toJson());

            // find a list of documents and iterate throw it using an iterator.
            FindIterable<Document> iterable = gradesCollection.find(gte("student_id", 10000));
            MongoCursor<Document> cursor = iterable.iterator();
            System.out.println("Student list with a cursor: ");
            while (cursor.hasNext()) {
                System.out.println(cursor.next().toJson());
            }

            // find a list of documents and use a List object instead of an iterator
            List<Document> studentList = gradesCollection.find(gte("student_id", 10000)).into(new ArrayList<>());
            System.out.println("Student list with an ArrayList:");
            for (Document student : studentList) {
                System.out.println(student.toJson());
            }

            // find a list of documents and print using a consumer
            System.out.println("Student list using a Consumer:");
            Consumer<Document> printConsumer = document -> System.out.println(document.toJson());
            gradesCollection.find(gte("student_id", 10000)).forEach(printConsumer);

            // find a list of documents with sort, skip, limit and projection
            List<Document> docs = gradesCollection.find(and(eq("student_id", 10001), lte("class_id", 5)))
                                                  .projection(fields(excludeId(), include("class_id", "student_id")))
                                                  .sort(descending("class_id"))
                                                  .skip(2)
                                                  .limit(2)
                                                  .into(new ArrayList<>());

            System.out.println("Student sorted, skipped, limited and projected: ");
            for (Document student : docs) {
                System.out.println(student.toJson());
            }
        }
    }
}

Wrapping Up

The MongoDB Java Driver provides many different options to read documents so you can choose which one is the most adapted to your code.

The Java Driver also supports POJOs so you can type directly a MongoCollection with your POJO class.

MongoCollection<Person> collection = database.getCollection("people", Person.class);

I will cover this in a future blog post but not my next one as my next blog post is about the next letter in our CRUD adventure: Updates.

Previous articles in this Quick Start Java and MongoDB series: