Hibernate OGM Querying MongoDB
In previous chapters, we accomplished several tasks in order to organize and store our data in NoSQL MongoDB stores. Now we’ll make use of this data by applying different querying techniques to extract only the information we need from a NoSQL MongoDB store.
As I noted in Chapter 1, querying a NoSQL database is a delicate and complex task—there are different situations, and different approaches depending on the native support for NoSQL querying. For MongoDB, there are a number of querying options; it’s up to you to choose the one that meets your needs, depending on your queries’ complexity, performance parameters, and so on:
Note Currently, Hibernate OGM via Hibernate Native API doesn’t provide support for Hibernate Criteria. Moreover, it doesn’t, via JPA, provide support for native and named queries.
We are going to delve into each of these querying possibilities and try to see how it works. We will focus on Hibernate OGM and discuss MongoDB from this perspective. For the sake of completeness, however, we’ll start this journey about querying MongoDB by first looking at basic MongoDB querying capabilities, and reserve the subject of Hibernate OGM till the second part of the chapter. In this way, you’ll get a complete picture of querying MongoDB and you’ll be better able to choose the appropriate querying solution for your needs.
As you probably know, MongoDB natively provides interactive support through the mongo shell (a full interactive JavaScript environment with a database interface for MongoDB), and programmatic support through the MongoDB driver (which is available for multiple programming languages, such as Java, Ruby, and PHP). In this section, we will skip the shell and concentrate on querying a MongoDB store using the MongoDB driver for Java. You’ll need the 2.8.0 version of this driver, which is available for download as a JAR named mongo-java-driver-2.8.0.jar at www.docs.mongodb.org/manual/applications/drivers/.
Before executing any query, you need to configure a MongoDB connection and create a database, then create a collection and populate it with data. For this, please go back to the section in Chapter 4 called “Java SE and Mongo DB—the HelloWorld Example.” Once you know how to connect and persist documents to a MongoDB store, you’re ready to perform queries.
We’ll create a collection called players and try some queries against it. Each document stores some tennis player data: name, surname, age, and birth date (and duplicate documents are allowed). After populating the collection with several documents, you can start with the well-known “select all” query. You can use the find method, which returns a cursor that contains a number of documents. As you can see, it’s very easy to iterate the results. This chunk of code uses find to extract all documents:
...
Mongo mongo = new Mongo("127.0.0.1", 27017);
DB db = mongo.getDB("players_db");
DBCollection dbCollection = db.getCollection("players");
...
System.out.println("Find all documents in collection:");
try (DBCursor cursor = dbCollection.find() ) {
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
}
...
The result of this query is shown in Figure 6-1.
Figure 6-1. All documents of the players collection
Note You can count how many documents are in a collection by calling the getCount method, like this: dbCollection.getCount();.
You can find a single document using the findOne method; this method doesn’t return a cursor. The snipped code is:
...
System.out.println("Find the first document in collection:");
DBObject first = dbCollection.findOne() ;
System.out.println(first);
...
The result will be the first document from the players collection, as shown in Figure 6-2.
Figure 6-2. Extracting the first document of the players collection
You can also execute conditional queries. For example, we can extract the documents corresponding to the player Rafael Nadal using the find method, like this:
...
System.out.println("Find Rafael Nadal documents:");
BasicDBObject query = new BasicDBObject("name", "Nadal").append("surname", "Rafael");
try (DBCursor cursor = dbCollection.find(query) ) {
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
}
...
The results are shown in Figure 6-3.
Figure 6-3. Extracting only documents containing Rafael Nadal
The find method combined with the $gt (greater than) operator lets you extract all players whose age is greater than 25:
...
System.out.println("Find players with age > 25:");
BasicDBObject query = new BasicDBObject("age", new BasicDBObject("$gt", 25));
try (DBCursor cursor = dbCollection.find(query) ) {
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
}
...
You can see the results in Figure 6-4.
Figure 6-4. Extracting only documents with age greater than 25
The find method combined with the $lt (less than) operator lets you extract all players whose age is less than 28:
...
System.out.println("Find players with age < 28:");
BasicDBObject query = new BasicDBObject("age", new BasicDBObject("$lt", 28));
try (DBCursor cursor = dbCollection.find(query) ) {
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
}
...
The results are shown in Figure 6-5.
Figure 6-5. Extracting only documents with age less than 28
Extracting data that falls within (or outside of) an interval of values can be accomplished using the $gt and $lt, or $gte (greater than or equal) and $lte (less than or equal) operators and the find method. For example, you can obtain all players born between 1 January, 1982 and 31 December, 1985 like this:
...
System.out.println("JAVA - Find players with birthday between 1 January, 1982 - 31 December, 1985:");
Calendar calendar_begin = GregorianCalendar.getInstance();
calendar_begin.clear();
calendar_begin.set(1982, Calendar.JANUARY, 1);
Calendar calendar_end = GregorianCalendar.getInstance();
calendar_end.clear();
calendar_end.set(1985, Calendar.DECEMBER, 31);
BasicDBObject query = new BasicDBObject("birth", new BasicDBObject("$gte",
calendar_begin.getTime()).append("$lte", calendar_end.getTime()));
try (DBCursor cursor = dbCollection.find(query) ) {
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
}
...
The results are shown in Figure 6-6:
Figure 6-6. Extracting only documents with births between 1 January, 1982 and 31 December, 1985
If you prefer to use Joda Time (a replacement for the Java date and time classes, available at http://joda-time.sourceforge.net), you can write the query like this:
System.out.println("JODA - Find players with birthday between 1 January, 1982 - 31 December, 1985:");
DateTime joda_calendar_begin = new DateTime(1982, 1, 1, 0, 0);
DateTime joda_calendar_end = new DateTime(1985, 12, 31, 0, 0);
query = new BasicDBObject("birth", new BasicDBObject("$gte", joda_calendar_begin.toDate()).append("$lte", joda_calendar_end.toDate()));
try (DBCursor cursor = dbCollection.find(query) ) {
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
}
You can also extract data with specific values using the $in operator and the find method. For example, you can obtain all players with the ages 25, 27, and 30, like this:
...
System.out.println("Find players with ages: 25, 27, 30");
List<Integer> list = new ArrayList<>();
list.add(25);
list.add(27);
list.add(30);
BasicDBObject query = new BasicDBObject("age", new BasicDBObject("$in", list));
try (DBCursor cursor = dbCollection.find(query) ) {
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
}
...
The result are shown in Figure 6-7.
Figure 6-7. Extracting only documents with age equal to 25, 27, or 30
When you need to extract data by negation, you can use the $ne (not equal) operator and the find method. For example, you can easily obtain all players with ages not equal to 27, like this:
...
System.out.println("Find players with ages different from: 27");
BasicDBObject query = new BasicDBObject("age", new BasicDBObject("$ne", 27));
try (DBCursor cursor = dbCollection.find(query) ) {
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
}
...
The results are shown in Figure 6-8:
Figure 6-8. Extracting only documents with age different from 27
In the previous examples, we created (inserted) and retrieved (read) data from MongoDB using MongoDB Java driver. You can accomplish an update accomplish by calling the save method. For example, you can replace Rafael Nadal with Rafael Nadal Parera, like this:
...
System.out.println("UPDATING ...");
BasicDBObject query = new BasicDBObject("name", "Nadal").append("surname", "Rafael");
try (DBCursor cursor = dbCollection.find(query)) {
while (cursor.hasNext()) {
DBObject item = cursor.next();
item.put("name", "Nadal Parera");
dbCollection.save(item);
}
}
...
And you can delete data by calling the remove method. For example, you can delete all occurrences of Roger Federer, like this:
...
System.out.println("DELETING ...");
BasicDBObject query = new BasicDBObject("name", "Federer").append("surname", "Roger");
try (DBCursor cursor = dbCollection.find(query)) {
while (cursor.hasNext()) {
DBObject item = cursor.next();
dbCollection.remove(item);
}
}
...
Note For advanced queries using MongoDB drivers, see The Definitive Guide to MongoDB by Eelco Plugge, Tim Hawkins, and Peter Membrey (Apress, 2010). Visit www.apress.com/9781430230519.
The complete application containing the preceding snippets of code is available in the Apress repository and is named MONGODB_QUERY. It comes as a NetBeans project and was tested under Java 7.
Hibernate OGM and CRUD Operations
The four essential operations performed against a NoSQL database—Create, Read, Update and Delete—are available in Hibernate OGM out of the box. Actually, independently of JPA or the Hibernate Native API, Hibernate ORM delegates persistence and load queries to the OGM engine, which delegates CRUD operations to DatastoreProvider and GridDialect, and these interact with the NoSQL store.
In Chapters 3 and 4 you saw how to develop applications based on Hibernate OGM via the Hibernate Native API and Java Persistence API. It should be a piece of cake, therefore, to wrap the Players entity in Listing 6-1 into such an application.
Listing 6-1. The Players Entity
package hogm.hnapi.entity;
import java.io.Serializable;
...
@Entity
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {
@Id
@GeneratedValue(generator = "mongodb_uuidgg")
private String id;
@Column(name = "player_name")
private String name;
@Column(name = "player_surname")
private String surname;
@Column(name = "player_age")
private int age;
@Column(name = "player_birth")
@Temporal(javax.persistence.TemporalType.DATE)
private Date birth;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Once that’s done, you have access to CRUD operations. Suppose we have an instance of Players, named player
.Using Hibernate OGM via Hibernate Native API, you can obtain the Hibernate session with the getCurrentSession or openSession methods.
HibernateUtil.getSessionFactory().getCurrentSession().persist(player);
HibernateUtil.getSessionFactory().getCurrentSession().merge(player);
HibernateUtil.getSessionFactory().getCurrentSession().get(Players.class, id );
HibernateUtil.getSessionFactory().getCurrentSession().delete(player);
You can try all of these methods in a sample application named HOGM_MONGODB_HNAPI_CRUD, available in the Apress repository. It comes as a NetBeans project and was tested under GlassFish 3 AS. The interface application looks like Figure 6-9.
Figure 6-9. Testing Hibernate OGM and CRUD operations
Using Hibernate OGM via the Java Persistence API (em stands for EntityManager):
em.persist(player);
em.merge(player);
em.find(Players.class, id );
em.delete (player);
You can try all of these methods in a sample application named HOGM_MONGODB_JPA_CRUD, available in the Apress repository. It comes as a NetBeans project and was tested under GlassFish 3 AS. The interface application looks like the one in Figure 6-9.
Hibernate Search and Apache Lucene
Basically, Hibernate/JPA and Apache Lucene deal with the same area—querying data. They both provide CRUD operations, a basic data unit (an entity in Hibernate, a document in Lucene) and the same programming concepts. The main difference lies in the fact that Hibernate/JPA promotes domain model-oriented programming, while Lucene deals with only a single, built-in data model—the Document class, which is too simple to describe complex associations. Combined, however, the two yield a higher-level API, named Hibernate Search.
Both Hibernate Search and Apache Lucene are powerful, robust technologies. While Apache Lucene is a full-text indexing and query engine with excellent query performance, Hibernate Search brings its power to the persistence domain model. The symbiosis works fairly well: Hibernate Search “squeezes” the query capabilities of Apache Lucene while providing support for the domain model and the synchronization of databases and indexes, and converting free text queries back to managed objects. Because our focus is on Hibernate OGM and MongoDB, I won’t provide a Hibernate Search or Apache Lucene tutorial. Instead we’ll get quickly to developing examples, and I’ll supply sufficient information for you to understand the new Hibernate Search/Apache Lucene annotations and classes, without going into detail. We are going to combine Hibernate ORM, OGM, and Search with Apache Lucene and MongoDB into applications with query capabilities so you can explore the complexity of the querying process. Once you have a functional application, you’ll be able to try a wide range of queries.
We will develop two applications. The first will be a Hibernate OGM/ via Hibernate Native API application and the second Hibernate OGM via JPA (details in Chapters 3 and 4). Both applications will follow a common, straightforward scenario: we’ll create an entity (and the corresponding POJO, specific only to Hibernate Native API), persist several instances to a MongoDB collection, and execute some query samples through Hibernate Search and Apache Lucene.
The POJO is named Players and is shown in Listing 6-2 (this POJO is mapped in an hbm.xml file).
Listing 6-2. The Players Class
public class Players {
private String id;
private String name;
private String surname;
private int age;
private Date birth;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
And thePlayers.hbm.xml fileis shown in Listing 6-3.
Listing 6-3. Players.hbm.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" " http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd ">
<hibernate-mapping>
<class name="hogm.hnapi.pojo.Players" table="atp_players">
<id name="id" type="string">
<column name="id" />
<generator class="uuid2" />
</id>
<property name="name" type="string">
<column name="player_name"/>
</property>
<property name="surname" type="string">
<column name="player_surname"/>
</property>
<property name="age" type="int">
<column name="player_age"/>
</property>
<property name="birth" type="date">
<column name="player_birth"/>
</property>
</class>
</hibernate-mapping>
Or, if you prefer the entity version, the POJO becomes what’s shown in Listing 6-4. (This entity is used in both applications.)
Listing 6-4. The Entity Version of Players
import java.io.Serializable;
...
@Entity
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {
@Id
@GeneratedValue(generator = "mongodb_uuidgg")
private String id;
@Column(name = "player_name")
private String name;
@Column(name = "player_surname")
private String surname;
@Column(name = "player_age")
private int age;
@Column(name = "player_birth")
@Temporal(javax.persistence.TemporalType.DATE)
private Date birth;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Common Steps
No matter which application type (OGM via the Hibernate Native API or via JPA), there are a few common steps to add Hibernate Search or Apache Lucene support:
Listing 6-5. The Players POJO with Annotations
package hogm.hnapi.pojo;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.NumericField;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;
...
@Indexed
public class Players {
@DocumentId
private String id;
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String name;
@Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
private String surname;
@NumericField
@Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
private int age;
@Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
@DateBridge(resolution = Resolution.YEAR)
private Date birth;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Or, if we apply these annotations to the Players entity, we get what’s shown in Listing 6-6.
Listing 6-6. The Entity Version of Players with Annotations
package hogm.hnapi.entity;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.NumericField;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;
...
@Entity
@Indexed
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {
private static final long serialVersionUID = 1L;
@DocumentId
@Id
@GeneratedValue(generator = "mongodb_uuidgg")
private String id;
@Column(name = "player_name")
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String name;
@Column(name = "player_surname")
@Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
private String surname;
@Column(name = "player_age")
@NumericField
@Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
private int age;
@Column(name = "player_birth")
@Field(index=Index.YES, analyze=Analyze.NO, store=Store.NO)
@DateBridge(resolution = Resolution.YEAR)
@Temporal(javax.persistence.TemporalType.DATE)
private Date birth;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
We have to specify the directory provider; for Apache Lucene, a directory represents the type and place to store index files, and it comes bundled with a file system (FSDirectoryProvider) and an in-memory implementation (RAMDirectoryProvider), though it also supports custom implementations. Hibernate Search is responsible for the configuration and initialization of Lucene resources, including the directory via DirectoryProviders. We want easy access to index files (with the ability to physically inspect indexes with external tools, like Luke), so we’ll use the file system to store them by setting the hibernate.search.default.directory_provider property as filesystem. Besides the directory provider, we also have to specify the default base directory for all indexes via the hibernate.search.default.indexBase property. Finally, we can specify the locking strategy (in this case, the filesystem-level lock) by setting the hibernate.search.default.locking_strategy property to single; this is a Java object lock held in memory. Add these configurations in hibernate.cfg.xml (or in HibernateUtil) for OGM via the Hibernate Native API, or in persistence.xml for OGM via JPA, like this:
//in hibernate.cfg.xml
<property name="hibernate.search.default.directory_provider">filesystem</property>
<property name="hibernate.search.default.indexBase">./Indexes</property>
<property name="hibernate.search.default.locking_strategy">single</property>...
Or:
//in HibernateUtil
OgmConfiguration cfgogm = new OgmConfiguration();
...
cfgogm.setProperty("hibernate.search.default.directory_provider","filesystem");
cfgogm.setProperty("hibernate.search.default.indexBase","./Indexes");
cfgogm.setProperty("hibernate.search.default.locking_strategy", "single");
...
Or:
...
//in persistence.xml
<property name="hibernate.search.default.directory_provider" value="filesystem"/>
<property name="hibernate.search.default.indexBase" value="./Indexes"/>
<property name="hibernate.search.default.locking_strategy" value="single"/>
...
Finally, everything is configured and we are ready to start writing Lucene queries. But, from this point on, the code will be specific to each of the two applications. So let’s start with the OGM via Hibernate Native API application.
Hibernate Search/Apache Lucene Querying—OGM via Native API
The first goal is to write a “select all” query that will help you become familiar with Lucene style in an OGM via Native API application. Following a step-by-step approach, we can write such a query, like this:
FullTextSession fullTextSession =
Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.all().createQuery();
org.apache.lucene.search.Sort sort = new Sort(new SortField("name", SortField.STRING));
FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID);
fullTextQuery.setSort(sort);
List<Players> results = fullTextQuery.list();
fullTextSession.clear();
We can put these nine steps in a method named selectAllAction to create our first Hibernate Search/Lucene query. You can find this method in a session bean, named SampleBean, in the package hogm.hnapi.ejb shown in Listing 6-7.
Listing 6-7. The selectAllAction Method
package hogm.hnapi.ejb;
...
public class SampleBean {
...
public List<Players> selectAllAction() {
log.info("Select all Players instance ...");
FullTextSession fullTextSession =
Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.all().createQuery();
org.apache.lucene.search.Sort sort = new Sort(new SortField("name", SortField.STRING));
FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
fullTextQuery.setSort(sort);
List<Players> results = fullTextQuery.list();
fullTextSession.clear();
log.info("Search complete ...");
return results;
}
...
}
The nine steps can be used as a quick guide for writing many other kinds of queries. Now let’s see how to write some common queries:
Listing 6-8. The selectByYearAction Method
package hogm.hnapi.ejb;
...
public class SampleBean {
...
public List<Players> selectByYearAction() {
log.info("Search only Players instances 'born in 1987' ...");
Calendar calendar = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.clear();
calendar.set(Calendar.YEAR, 1987);
FullTextSession fullTextSession =
Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query =
queryBuilder.keyword().onField("birth").matching(calendar.getTime()).createQuery();
FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
List<Players> results = fullTextQuery.list();
fullTextSession.clear();
log.info("Search complete ...");
return results;
}
}
Listing 6-9. The selectRafaelNadalAction Method
package hogm.hnapi.ejb;
...
public class SampleBean {
...
public List<Players> selectRafaelNadalAction() {
log.info("Search only Players instances that have the name 'Nadal' and surname 'Rafael' ...");
FullTextSession fullTextSession =
Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.bool().must(queryBuilder.keyword()
.onField("name").matching("Nadal").createQuery()).must(queryBuilder.keyword()
.onField("surname").matching("Rafael").createQuery()).createQuery();
FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
List<Players> results = fullTextQuery.list();
fullTextSession.clear();
log.info("Search complete ...");
return results;
}
}
Listing 6-10. The selectJAction Method
package hogm.hnapi.ejb;
...
public class SampleBean {
...
public List<Players> selectJAction() {
log.info("Search only Players that surnames begins with 'J' ...");
FullTextSession fullTextSession =
Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.keyword().wildcard()
.onField("surname").matching("J*").createQuery();
FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
List<Players> results = fullTextQuery.list();
fullTextSession.clear();
log.info("Search complete ...");
return results;
}
}
Listing 6-11. The select25To28AgeAction Method
package hogm.hnapi.ejb;
...
public class SampleBean {
...
public List<Players> select25To28AgeAction() {
log.info("Search only Players that have ages between 25 and 28, excluding limits ...");
FullTextSession fullTextSession =
Search.getFullTextSession(HibernateUtil.getSessionFactory().getCurrentSession());
QueryBuilder queryBuilder = fullTextSession.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.range()
.onField("age").from(25).to(28).excludeLimit().createQuery();
FullTextQuery fullTextQuery = fullTextSession.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
List<Players>results = fullTextQuery.list();
fullTextSession.clear();
log.info("Search complete ...");
return results;
}
}
Note As you can see, you can easy model a range using the from, to, and excludeLimit methods. Beside these, Lucene provides the below and above methods. Using them in a logical approach, you can obtain the well-known operators “<” (less than), “>” (greater than), “<=” (less than or equal to)”, and “>=” greater than or equal to).
There are many other kinds of queries you can write, you just have to explore more documentation about Hibernate Search and Apache Lucene. For the queries mentioned, I developed a complete application that’s available in the Apress repository and is named HOGM_MONGODB_HNAPI_HS. It comes as a NetBeans project and was tested under GlassFish 3 AS. Figure 6-10 shows this application.
Figure 6-10. The HOGM_MONGODB_HNAPI_HS application
Note You can rebuild the index (deleting it and then reloading all entities from the database) by calling the startAndWait method: fullTextSession.createIndexer().startAndWait();
When you have associations (or embedded objects), you need to provide a few more annotations. Associated objects (and embedded objects) can be indexed as part of the root entity index. For this, the association is marked with @IndexedEmbedded. When the association is bidirectional, the other side must be annotated with @ContainedIn. This helps Hibernate Search keep up to date the associations indexing process.
For example, let’s suppose that the Players entity is in a many-to-many association with the Tournaments entity (each player participates in multiple tournaments and each tournament contains multiple players). (And keep in mind that POJOs annotations are specified in .hbm.xml files.) The annotated POJOs are shown in Listing 6-12 and Listing 6-13.
Listing 6-12. The Players POJO
package hogm.hnapi.pojo
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;
...
@Indexed
public class Players {
@DocumentId
private String id;
@Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
private String name;
@Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
private String surname;
@Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
private int age;
@Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
@DateBridge(resolution = Resolution.YEAR)
private Date birth;
@IndexedEmbedded
Collection<Tournaments> tournaments = new ArrayList<Tournaments>(0);
//getters and setters
...
}
Listing 6-13. The Tournaments POJO
package hogm.hnapi.pojo
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.ContainedIn;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;
@Indexed
public class Tournaments {
@DocumentId
private String id;
@Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
private String tournament;
@ContainedIn
Collection<Players> players = new ArrayList<Players>(0);
//getters and setters
...
}
Now wrap these POJOs into entities, as shown in Listing 6-14 and Listing 6-15.
Listing 6-14. The Players Entity
package hogm.hnapi.entity;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;
@Entity
@Indexed
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {
@DocumentId
@Id
@GeneratedValue(generator = "mongodb_uuidgg")
private String id;
@Column(name = "player_name")
@Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
private String name;
@Column(name = "player_surname")
@Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
private String surname;
@Column(name = "player_age")
@Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
private int age;
@Column(name = "player_birth")
@Field
@DateBridge(resolution = Resolution.YEAR)
@Temporal(javax.persistence.TemporalType.DATE)
private Date birth;
@ManyToMany(cascade = CascadeType.PERSIST,fetch=FetchType.EAGER)
@IndexedEmbedded
private Collection<Tournaments> tournaments= new ArrayList<Tournaments>(0);
//getters and setters
...
}
Listing 6-15. The Tournaments Entity
package hogm.hnapi.entity;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.ContainedIn;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;
@Entity
@Indexed
@Table(name = "atp_tournaments")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Tournaments implements Serializable {
@DocumentId
@Id
@GeneratedValue(generator = "mongodb_uuidgg")
private String id;
@Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
private String tournament;
@ManyToMany(mappedBy = "tournaments", fetch = FetchType.EAGER)
@ContainedIn
private Collection<Players> players = new ArrayList<Players>(0);
//getters and setters
...
}
Now you can write Hibernate Search/Apache Lucene queries. (The official documentation can be a good place to start testing queries for associations.) For testing purposes, I’ve integrated the preceding POJOs and entities into an application named HOGM_MONGODB_HNAPI_ASSOCIATIONS_HS that can be downloaded from the Apress repository (there are two queries involved). It comes as a NetBeans project and was tested under GlassFish 3 AS. Figure 6-11 shows this application.
Figure 6-11. The HOGM_MONGODB_HNAPI_ASSOCIATIONS_HS application
Note You can easily drop a MongoDB database from the shell by typing the command db.dropDatabase();.
Hibernate Search/Apache Lucene Querying—OGM via JPA
Remember the “select all” query we wrote earlier? This time, we’ll write the same query for an application based on OGM via JPA. The steps for accomplishing this task are:
FullTextEntityManager fullTextEntityManager =
org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.all().createQuery();
org.apache.lucene.search.Sort sort = new Sort(new SortField("name", SortField.STRING));
FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID);
fullTextQuery.setSort(sort);
...
List<Players> results = fullTextQuery.getResultList();
...
fullTextEntityManager.clear();
Now, you can put these nine steps in a method named selectAllAction to obtain the Hibernate Search/Lucene query shown in Listing 6-16.
Listing 6-16. The selectAllAction Method
package hogm.jpa.ejb;
...
public class SampleBean {
...
public List<Players> selectAllAction() {
log.info("Select all Players instance ...");
FullTextEntityManager fullTextEntityManager =
org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Sort sort = new Sort(new SortField("name", SortField.STRING));
org.apache.lucene.search.Query query = queryBuilder.all().createQuery();
FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
fullTextQuery.setSort(sort);
List<Players> results = fullTextQuery.getResultList();
fullTextEntityManager.clear();
log.info("Search complete ...");
return results;
}
}
The nine steps can be used as a quick guide for writing many other kinds of queries. In addition, you can see how to write some common queries (these are the same queries from the section “Hibernate Search/Apache Lucene Querying OGM via Native API,” rewritten for the OGM via JPA case).
Listing 6-17. The selectByYearAction Method
package hogm.jpa.ejb;
...
public class SampleBean {
...
public List<Players> selectByYearAction() {
log.info("Search only Players instances born in 1987 ...");
Calendar calendar = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.clear();
calendar.set(Calendar.YEAR, 1987);
FullTextEntityManager fullTextEntityManager =
org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.keyword()
.onField("birth").matching(calendar.getTime()).createQuery();
FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
List<Players> results = fullTextQuery.getResultList();
fullTextEntityManager.clear();
log.info("Search complete ...");
return results;
}
}
Listing 6-18. The selectRafaelNadalAction Method
package hogm.jpa.ejb;
...
public class SampleBean {
...
public List<Players> selectRafaelNadalAction() {
log.info("Search only Players instances that have the name 'Nadal' and surname 'Rafael' ...");
FullTextEntityManager fullTextEntityManager =
org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().
buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.bool().must(queryBuilder.keyword()
.onField("name").matching("Nadal").createQuery()).must(queryBuilder.keyword()
.onField("surname").matching("Rafael").createQuery()).createQuery();
FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
List<Players> results = fullTextQuery.getResultList();
fullTextEntityManager.clear();
log.info("Search complete ...");
return results;
}
}
Listing 6-19. The selectJAction Method
package hogm.jpa.ejb;
...
public class SampleBean {
...
public List<Players> selectJAction() {
log.info("Search only Players that surnames begins with 'J' ...");
FullTextEntityManager fullTextEntityManager =
org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory()
.buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.keyword().wildcard()
.onField("surname").matching("J*").createQuery();
FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
List<Players> results = fullTextQuery.getResultList();
fullTextEntityManager.clear();
log.info("Search complete ...");
return results;
}
}
Listing 6-20. The select25To28AgeAction Method
package hogm.jpa.ejb;
...
public class SampleBean {
...
public List<Players> select25To28AgeAction() {
log.info("Search only Players that have ages between 25 and 28, excluding limits ...");
FullTextEntityManager fullTextEntityManager =
org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory()
.buildQueryBuilder().forEntity(Players.class).get();
org.apache.lucene.search.Query query = queryBuilder.range().onField("age")
.from(25).to(28).excludeLimit().createQuery();
FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(query, Players.class);
fullTextQuery.initializeObjectsWith(ObjectLookupMethod.SKIP,
DatabaseRetrievalMethod.FIND_BY_ID);
List<Players> results = fullTextQuery.getResultList();
fullTextEntityManager.clear();
log.info("Search complete ...");
return results;
}
}
There are many other kinds of queries you can write, you just have to delve into the available documentation about Hibernate Search and Apache Lucene. For the queries covered, I developed a complete application that’s available in the Apress repository and is named HOGM_MONGODB_JPA_HS. It comes as a NetBeans project and was tested under GlassFish 3 AS. Figure 6-12 shows this application.
Figure 6-12. The HOGM_MONGODB_JPA_HS application
Note You can rebuild the index (deleting it and then reloading all entities from the database) by calling the startAndWait method: fullTextEntityManager.createIndexer().startAndWait();
When you have associations (or embedded objects), you need to provide a few more annotations. Associated objects (and embedded objects) can be indexed as part of the root entity index. For this, the association is marked with @IndexedEmbedded. When the association is bidirectional, the other side must be annotated with @ContainedIn. This helps Hibernate Search keep the associations indexing process up to date.
For example, let’s suppose that the Players entity is in a many-to-many association with the Tournaments entity (each player participates in multiple tournaments and each tournament contains multiple players). The annotated Players entity listing is shown in Listing 6-21.
Listing 6-21. The Annotated Players Entity
package hogm.jpa.entity;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;
@Entity
@Indexed
@Table(name = "atp_players")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Players implements Serializable {
@DocumentId
@Id
@GeneratedValue(generator = "mongodb_uuidgg")
private String id;
@Column(name = "player_name")
@Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
private String name;
@Column(name = "player_surname")
@Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
private String surname;
@Column(name = "player_age")
@Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
private int age;
@Column(name = "player_birth")
@Field
@DateBridge(resolution = Resolution.YEAR)
@Temporal(javax.persistence.TemporalType.DATE)
private Date birth;
@ManyToMany(cascade = CascadeType.PERSIST,fetch=FetchType.EAGER)
@IndexedEmbedded
private Collection<Tournaments> tournaments= new ArrayList<Tournaments>(0);
//getters and setters
...
}
And the Tournaments entity is shown in Listing 6-22.
Listing 6-22. The Annotated Tournaments Entity
package hogm.jpa.entity;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.ContainedIn;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;
@Entity
@Indexed
@Table(name = "atp_tournaments")
@GenericGenerator(name = "mongodb_uuidgg", strategy = "uuid2")
public class Tournaments implements Serializable {
@DocumentId
@Id
@GeneratedValue(generator = "mongodb_uuidgg")
private String id;
@Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
private String tournament;
@ManyToMany(mappedBy = "tournaments", fetch = FetchType.EAGER)
@ContainedIn
private Collection<Players> players = new ArrayList<Players>(0);
//getters and setters
...
}
Now you can write Hibernate Search/Apache Lucene queries. (The official documentation can be a good place to start testing queries for associations.) For testing purposes, I’ve integrated the preceding entities into an application named HOGM_MONGODB_JPA_ASOCIATIONS_HS that can be downloaded from the Apress repository (there are two queries involved). It comes as a NetBeans project and was tested under GlassFish 3 AS. Figure 6-13 shows this application.
Figure 6-13. The HOGM_MONGODB_JPA_ASSOCIATIONS_HS application
We stop here, but this may be just the beginning of your exploration of the amazing power of Hibernate Search and Apache Lucene combined. I’ve given you a starting point for querying MongoDB collections via OGM and Hibernate Search/Apache Lucene. From this point forward, it’s up to you how much you go in the Hibernate Search/Apache Lucene territory.
According to the Hibernate OGM documentation, version 4.0.0Beta1 includes a JP-QL basic parser capable of converting simple queries using Hibernate Search. Currently, there are several limitations in using it, iincluding:
I tried to work around these limitations, but have not been able to develop a functional application to exploit the JP-QL parser for simple queries. I tried, for the Players entity annotated with @Indexed, @Field, and so on, a simple query, like this:
Query query = HibernateUtil.getSessionFactory().getCurrentSession().createQuery("from Players p");
Unfortunately, my multiple approaches failed with one single and annoying error: java.lang.NullPointerException. The indexing process seems to work fine, but the query results list is always null.
Anyway, this is not such a big issue, since the JP-QL parser is very young and, by the time you read this section, this information may well be obsolete. The JP-QL parser may be more generous with its query support by then. For now, you can use the MongoDB Java driver and, of course, Hibernate Search and Apache Lucene.
Summary
After all the hard work of the previous chapters, in this chapter we gathered the fruits. We were able to work with the stored data by writing queries against MongoDB databases. In particular, in this chapter, you learned how to write queries using Hibernate Search/Apache Lucene and the MongoDB Java driver. My aim was to provide the basic information about writing a pure MongoDB Java driver application and an OGM via Native API and/or via JPA application ready to query a MongoDB database.