Loading TOC...
Java Application Developer's Guide (PDF)

Java Application Developer's Guide — Chapter 5

POJO Data Binding Interface

You can use the Java Client API to persist POJOs (Plain Old Java Objects) as documents in a MarkLogic database. This feature enables you to apply the rich MarkLogic Server search and data management features to the Java objects that represent your application domain model without explicitly converting your data to documents.

This chapter includes the following topics:

Data Binding Interface Overview

The data binding feature of the Java Client API enables your data to flow seamlessly between application-level Java objects and JSON documents stored in a MarkLogic server. With the addition of minimal annotations to your class definitions, you can store POJOs in the database, search them with the full power of MarkLogic Server, and recreate POJOs from the stored objects.

The Java Client API data binding interface uses the data binding capabilities of Jackson to convert between Java objects and JSON. You can leverage Jackson annotations to fine tune the representation of your objects in the database, but generally you should not need to. Not all Jackson annotations are compatible with the Java Client API data binding capability. For details, see Limitations of the Data Binding Interface.

The data binding capabilities of the Java Client API are primarily exposed through the com.marklogic.client.pojo.PojoRepository interface. To get started with data binding, follow these basic steps:

  • For each Java class you want to bind to a database representation, add source code annotations to your class definition that call out the Java property to be used as the object id.
  • Use a PojoRepository to save your objects in the database. You can create, read, update, and delete persisted objects.
  • Search your object data using a string (StringQueryDefinition) or structured query (PojoQueryDefinition). You can use search to identify and retrieve a subset of the stored POJOs.

The object id annotation is required. Additional annotations are available to support more advanced features, such as identifying properties on which to create database indexes and latitude and longitude identifiers for geospatial search. For details, see Annotating Your Object Definition.

Limitations of the Data Binding Interface

You should be aware of the following restrictions and limitations of the data binding feature:

  • The Data Bind interface is intended for use in situations where the in-database representation of objects is not as important as using a POJO-first Java API.

    If you have strict requirements for how your objects must be structured in the database, use JacksonDatabindHandle with JSONDocumentManager and StructuredQueryBuilder instead of the Data Binding interface.

  • You can only persist and restore objects of consistent type.

    That is, if you persist objects of type T, you must restore them and search them as type T. For example, you cannot persist an object as type T and then restore it as a some type T' that extends T, or vice versa.

  • You cannot use the data binding interface with classes that contain inner classes.
  • The object property you chose as the object id must not contain values that do not form valid database URIs when serialized. You should choose object properties that have atomic type, such as Integer, String, or Float, rather than a complex object type such as Calendar.
  • Though the Java Client API uses Jackson to convert between POJOs and JSON, not all Jackson features are compatible with the Java Client API data binding capability. For example, you can add Jackson annotations to your POJOs that result in objects not being persisted or restored properly.

Annotating Your Object Definition

The data binding interface in the Java Client API is driven by simple annotations in your class definitions. Annotations are of the form @annotationName. You can attach an annotation to a public class field or a public getter or setter method.

Every bound class requires at least an @Id annotation to define the object property that holds the object id. A bound POJO class must contain exactly one @Id annotation. Each object must have a unique id.

Additional, optional annotations support powerful search features such as range and geospatial queries.

For example, the following annotation says the object id should be derived from the getter MyClass.getMyId. If you rely on setters and getters for object identity, your setters and getters should follow the Java Bean convention.

import com.marklogic.client.pojo.annotation.Id;
public class MyClass {
  Long myId;

  @Id
  public Long getMyId() {
    return myId;
  }
}

Alternatively, you can associated @Id with a member. The following annotation specifies that the myId member holds the object id for all instances of myClass:

import com.marklogic.client.pojo.annotation.Id;
public class MyClass {
  @Id
  public Long myId;
}

Annotations can be associated with a member, a getter or a setter because an annotation decorates a logical property of your POJO class.

The following table summarizes the supported annotations. For a complete list, see com.marklogic.pojo.annotation in the JavaDoc.

Annotation Description
@Id
The object identifier. The value in the @Id property or the value returned by the @Id method is used to generate a unique database URI for each persistent object of the class. Each object must have a unique id. Each POJO class may have only one @Id.
@PathIndexProperty
Identifies a property for which a path range index is required. Any property on which you perform range queries must be indexed. For details, see Creating Indexes from Annotations.
@GeospatialLatitude
Identifies the property that contains the geospatial latitude value, in support of a geospatial element pair index. For details, see Creating Indexes from Annotations.
@GeospatialLongitude
Identifies the property that contains the geospatial longitude value, in support of a geospatial element pair index. For details, see Creating Indexes from Annotations.
@GeoSpatialPathIndexProperty
Identifies a property for which a geospatial path range index is required. Any property on which you perform geospatial range queries must be indexed. For details, see Creating Indexes from Annotations.

Saving POJOs in the Database

Use PojoRepository.write to insert or update POJOs in a MarkLogic database. Your POJO class definition must include at least an @Id annotation and each object must have a unique id.

The class whose objects you want to persist must be serializable by Jackson. For details, see Testing Your POJO Class for Serializability.

Use the following procedure to persist POJOs in the database:

  1. Ensure the class you want to work with includes at least an @Id annotation, as described in Annotating Your Object Definition.
  2. If you have not already done so, create a com.marklogic.client.DatabaseClient object.
    DatabaseClient client = DatabaseClientFactory.newClient(
      host, port, user, password, authType);
  3. Create a PojoRepository object associated with the class you want to bind. For example, if you want to bind the class named MyClass and the @Id annotation in MyClass identifies a field or method return type of type Long, create a repository as follows:
    PojoRepository myClassRepo = 
        client.newPojoRepository(MyClass.class, Long.class);
  4. Call PojoRepository.write to save objects to the database. For example:
    MyClass obj = new MyClass();
    myClass.setId(42);
    
    myClassRepo.write(obj);
  5. When you are finished with the database, release the connection.
    client.release();

For a working example, see Example: Saving and Restoring POJOs.

To load POJOs from the database into your application, use PojoRepository.read or PojoRepository.search. For details, see Retrieving POJOs from the Database By Id and Searching POJOs in the Database

Retrieving POJOs from the Database By Id

Use PojoRepository.read to load POJOs from the database into your application. You should only use PojoRepository.read on objects created using PojoRepository.write.

Use the following procedure to load POJOs from the database by object id:

  1. Ensure the class you want to work with includes at least an @Id annotation , as described in Annotating Your Object Definition.
  2. If you have not already done so, create a com.marklogic.client.DatabaseClient object.
    DatabaseClient client = DatabaseClientFactory.newClient(
      host, port, user, password, authType);
  3. Create a PojoRepository object associated with the class you want to work with. For example, if you want to restore objects of the class named MyClass and the @Id annotation in MyClass identifies a field or method return type of type Long, create a repository as follows:
    PojoRepository myClassRepo = 
        client.newPojoRepository(MyClass.class, Long.class);
  4. Call PojoRepository.read to restore one or more objects from the database. For example:
    MyClass obj = myClassRepo.read(42);
    
    PojoPage<MyClass> objs = myClassRepo.read(new Long[]{1,3,5});
  5. When you are finished with the database, release the connection.
    client.release();

For a working example, see Example: Saving and Restoring POJOs.

To restore POJOs from the database using criteria other than object id, see Searching POJOs in the Database.

Example: Saving and Restoring POJOs

The following example saves several objects of type MyType to the database, recreates them as POJOs by reading them by id from the database, and then prints out the contents of the restored objects.

The objects are written to the database by calling PojoRepository.write and read back using PojoRepository.read. In this example, the objects are read back by id. You can retrieve objects by searching for a variety of object features. For details, see Searching POJOs in the Database.

package examples;

import com.marklogic.client.DatabaseClient;
import com.marklogic.client.DatabaseClientFactory;
import com.marklogic.client.DatabaseClientFactory.Authentication;
import com.marklogic.client.pojo.PojoPage;
import com.marklogic.client.pojo.PojoRepository;
import com.marklogic.client.pojo.annotation.Id;


public class PojoExample {
  private static DatabaseClient client = DatabaseClientFactory.newClient(
    "localhost", 8000, user, password, Authentication.DIGEST);

  // The POJO class
  static public class MyClass {
    Integer myId;
    String otherData;

    public MyClass() { myId = 0; otherData = ""; }
    public MyClass(Integer id) { myId = id; otherData = ""; }
    public MyClass(Integer id, String data) { 
      myId = id; otherData = data; 
    }

    @Id 
    public int getMyId() { return myId; }
    public void setMyId(int id) { myId = id; }

    public String getOtherData() { return otherData; }
    public void setOtherData(String data) { otherData = data; }
    
    public String toString() {
      return "myId=" + getMyId() + " " +  
          "otherData=\"" + getOtherData() + "\"";
    }
  }

  static void tryPojos() {
    PojoRepository<MyClass,Integer> repo = 
        client.newPojoRepository(MyClass.class, Integer.class);
    Integer ids[] = {1, 2, 3};
    String data[] = {"a", "b", "c"};
    
    // Save objects in the database
    for (int i = 0; i < ids.length; i++) {
      repo.write(new MyClass(ids[i], data[i]));
    }

    // Restore objects from the database by id
    PojoPage<MyClass> outputObjs = repo.read(ids);
    while (outputObjs.hasNext()) {
      MyClass obj = outputObjs.next();
      System.out.println(obj.toString());
    }
  }

  public static void main(String[] args) {
    tryPojos();
    client.release();
  }
}

Searching POJOs in the Database

You can use PojoRepository.search to search POJOs in the database that match a query. A rich set of query capabilities is available, including full text search using a simple string query grammar and more finely controllable search using structured query.

This section covers concept and procedural information on searching POJOs. For a complete example, see Example: Searching POJOs.

This section covers the following topics:

Basic Steps for Searching POJOs

This section describes the basic process for searching POJOs. The variations are in how you express your search criteria.

You should only use PojoRepository.search on objects created using PojoRepository.write. Using it to search JSON documents created in a different way can lead to errors.

  1. Ensure the class you want to work with includes at least an @Id annotation, as described in Annotating Your Object Definition.
  2. If you have not already done so, create a com.marklogic.client.DatabaseClient object.
    DatabaseClient client = DatabaseClientFactory.newClient(
      host, port, user, password, authType);
  3. Create a PojoRepository object associated with the class you want to work with. For example, if you want to restore objects of the class named MyClass and the @Id annotation in MyClass identifies a field or method return type of type Long, create a repository as follows:
    PojoRepository<MyClass, Long> myClassRepo = 
        client.newPojoRepository(MyClass.class, Long.class);
  4. Optionally, set the limit on the number of matching objects to return. The default is 10 objects.
    myClassRepo.setPageLength(5);
  5. Create a StringQueryDefinition or StructuredQueryDefinition that represents the objects you want to find.
    1. For a string query, create a StringQueryDefinition using a QueryManager object. For details, see Full Text Search with String Query. For example, the following query performs a full text search for the phrase 'dog':
      QueryManager qm = client.newQueryManager();
      StringQueryDefinition query =
          qm.newStringDefinition().withCriteria("dog");
    2. For a structured query, use PojoRepository.getQueryBuilder to create a query builder, and then use the query builder to create your query. For details, see Search Using Structured Query. For example, the following query matches objects whose 'otherData' property value is 'dog':
      StructuredQueryDefinition query =
          myClassRepo.getQueryBuilder().value("otherData", "dog");
  6. Call PojoRepository.search to find matching objects in the database. Set the start parameter to 1 to retrieve results beginning with the first match, or set it to higher value to return subsequent pages of results, as described in Retrieving POJOs Incrementally.
    PojoPage<MyClass> matchingObjs = repo.search(query,1);
    while (matchingObjs.hasNext()) {
        MyClass ojb = matchingObjs.next();
        ...
    }
  7. When you are finished with the database, release the connection.
    client.release();

Matching objects are returned as a PojoPage, which represents a limited number of results. You may not receive all results in a single page if you read a large number objects. You can fetch the matching objects in batches, as described in Retrieving POJOs Incrementally. You can configure the page size using PojoRepository.setPageLength.

Full Text Search with String Query

A string query is a plain text search string composed of terms, phrases, and operators that can be easily composed by end users typing into an application search box. For example, 'cat AND dog' is a string query for finding documents that contain both the term 'cat' and the term 'dog'. For details, see The Default String Query Grammar in the Search Developer's Guide.

Using a string query to search POJOs performs a full text search. That is, matches can occur anywhere in an object.

For example, if the sample data contains an object whose 'title' property is 'Leaves of Grass' and another object whose 'author' property is 'Munro Leaf', then the following search matches both objects. (The search term 'leaf' matches 'leaves' because string search uses stemming by default.)

QueryManager qm = client.newQueryManager();
StringQueryDefinition query =
    qm.newStringDefinition().withCriteria("leaf");
PojoPage<Book> matches = repo.search(query, 1);

For a complete example, see Searching POJOs in the Database.

Search Using Structured Query

A structured query is an Abstract Syntax Tree representation of a search expression. You can use structured query to build up a complex query from a rich set of sub-query types. For example, structured query enables you to search specific object properties.

Use PojoQueryBuilder to create structured queries over your persisted POJOs. Though you can create structured queries in other ways, using a PojoQueryBuilder enables you to create queries without knowing the details of how your objects are persisted in the database or the syntax of a structured query. Also, PojoQueryBuilder exposes only those structured query capabilities that are applicable to POJOs.

To create a PojoQueryBuilder, use PojoRepository.getQueryBuilder to create a builder. For example:

PojoQueryBuilder<Person> qb = repo.getQueryBuilder();

Use the methods of PojoQueryBuilder to create complex, compound queries on your objects, equivalent to structured query constructs such as and-query, value-query, word-query, range-query, container-query, and geospatial queries. For details, see Structured Query Concepts in the Search Developer's Guide.

To match data in objects nested inside your top level POJO class, use PojoQueryBuilder.containerQuery (or PojoQueryBuilder.containerQueryBuilder) to constrain a query or sub-query to a particular sub-object.

For example, suppose your objects have the following structure:

public class Person {
  public Name name;
}
public class Name {
  public String firstName;
  public String lastName;
}

The the following search matches the term 'john' in Person objects only when it appears somewhere in the name object. It matches occurrences in either firstName or lastName.

PojoQueryBuilder qb = repo.getQueryBuilder();
PojoPage<Person> matches = repo.search(
    qb.containerQuery("name", qb.term("john")), 1);

The following query further constrains matches to occurrences in the lastName property of name.

qb.containerQuery("name", 
    qb.containerQuery("lastName", qb.term("john")))

For a complete example, see Searching POJOs in the Database.

How Indexing Affects Searches

You can search POJOs with many query types without defining any indexes. This enables you to get started quickly. However, indexes are required for range queries (PojoQueryBuilder.range) and can significantly improve search performance by enabling unfiltered search, as described below.

A filtered search uses available indexes, if any, but then checks whether or not each candidate meets the query requirements. This makes a filtered search accurate, but much slower than an unfiltered search. An unfiltered search relies solely on indexes to identify matches, which is much faster, but can result in false positives. For details, see Fast Pagination and Unfiltered Searches in Query Performance and Tuning Guide.

By default, a POJO search is an unfiltered search. To force use of a filtered search, wrap your query in a call to PojoQueryBuilder.filteredQuery. For example:

repo.search(builder.filteredQuery(builder.word("john")))

Unless your database is small or your query produces only a small set of pre-filtering results, you should define an index over any object property used in a word, value, or range query. If your search includes a range query, you must either have an index configured on each object property used in the range query, or you must wrap your query in a call to PojoRepository.filteredQuery to force a filtered search.

The POJO interfaces of the Java API include the ability to annotate object properties that should be indexed, and then generate an index configuration from the annotation. For details, see Creating Indexes from Annotations.

Creating Indexes from Annotations

As described in How Indexing Affects Searches, you should usually create indexes on object properties used in range queries. Though no automatic index creation is provided, the POJO interface can simplify index creation for you by generating index configuration information from annotations.

Use the following procedure to create an index on an object property.

  1. Attach an @PathIndexProperty annotation to each object property you want to index. You can attach the annotation to a member, setter, or getter. Set scalarType to a value compatible with the type of your object property. For example:
    import com.marklogic.client.pojo.annotation.PathIndexProperty;
    
    public class Person {
      ...
    
      @PathIndexProperty(scalarType=PathIndexProperty.ScalarType.INT)
      public int getAge() {
        return age;
      }
      ...
    }
  2. Run the com.marklogic.client.pojo.util.GenerateIndexConfig tool to generate an index configuration for your application. For example, if you run the following command against the example code in Example: Searching POJOs:
    $ java com.marklogic.client.pojo.util.GenerateIndexConfig \
        -classes "examples.Person examples.Name"
        -file personIndexes.json

    Then the following index configuration is saved to the file personIndexes.json.

    {
      "range-path-index" : [ {
        "path-expression" : "examples.Person/age",
        "scalar-type" : "int",
        "collation" : "",
        "range-value-positions" : "false",
        "invalid-values" : "ignore"
      } ],
      "geospatial-path-index" : [ ],
      "geospatial-element-pair-index" : [ ]
    }
  3. Use the generated index configuration to add the required indexes to the database in which you store your POJOs. See below for details.

You can use the output from GenerateIndexConfig to add the required indexes to your database in several ways, including the Admin Interface, the XQuery Admin API, and the Management REST API.

The output from GenerateIndexConfig is suitable for immediate use with the REST Management API method PUT /manage/v2/databases/{id|name}/properties. However, be aware that this interface overwrites all indexes in your database with the configuration in the request.

To use the output of GenerateIndexConfig to create indexes with the REST Management API, run a command similar to the following. This example assumes you are using the Documents database for your POJO store and that the file personIndexes.json was generated by GenerateIndexConfig.

The following command will replace all indexes in the database with the indexes in personIndexes.json. Do not use this procedure if your database configuration includes other indexes that should be preserved.

$ curl --anyauth --user user:password -X PUT -i 
    -H "Content-type: application/json" -d @./personIndexes.json \
    http://localhost:8002/manage/LATEST/databases/Documents/properties

To create the required indexes with the REST Manangement API while preserving existing indexes follow this procedure:

  1. Use GET /manage/v2/databases/{id|name}/properties to retrieve the current database properties. For example, the following command saves the properties of the Documents database to the file allProperties.json:
    $ curl --anyauth --user user:password -X GET \
        -H "Accept: application/json" -o allProperties.json     http://localhost:8002/manage/LATEST/databases/Documents/properties
  2. Locate the indexes of the same types as those generated by GenerateIndexConfig in the output from Step.
    1. If there are no indexes of the same type as those generated by GenerateIndexConfig, you can safely apply the generated configuration directly.
    2. If there are existing indexes of the same type as those generated by GeneratedIndexConfig, extract the existing indexes of that type from the output of Step 1 and combine this configuration information with the output from GenerateIndexConfig. See the example below.
  3. Use PUT /manage/v2/databases/{id|name}/properties to install the merged index configuration. For example:
    $ curl --anyauth --user user:password -X PUT -i 
        -H "Content-type: application/json" -d @./comboIndex.json \
        http://localhost:8002/manage/LATEST/databases/Documents/properties

For example, suppose GenerateIndexConfig generates the following output, which includes one path range index on Person.age and no geospatial indexes.

{
  "range-path-index" : [ {
    "path-expression" : "examples.Person/age",
    "scalar-type" : "int",
    "collation" : "",
    "range-value-positions" : "false",
    "invalid-values" : "ignore"
  } ],
  "geospatial-path-index" : [ ],
  "geospatial-element-pair-index" : [ ]
}

Further suppose retrieving the current database properties reveals an existing range-path-index setting such as the following:

$ curl --anyauth --user user:password -X GET \
    -H "Accept: application/json" -o allProperties.json     http://localhost:8002/manage/LATEST/databases/Documents/properties

==> Properties saved to allProperties.json include the following:
{
  "database-name": "Documents",
  "forest": [
    "Documents"
  ],
  "security-database": "Security",
  ...
  "range-path-index": [
    {
      "scalar-type": "string",
      "collation": "http://marklogic.com/collation/",
      "path-expression": "/some/other/data",
      "range-value-positions": false,
      "invalid-values": "reject"
    }
  ],
  ...
}

Then combining the existing index configuration with the generated POJO index configuration results in the following input to PUT /manage/v2/databases/{id|name}/properties. (You can omit the generated geospatial-path-index and geospatial-element-pair-index configurations in this case because they are empty.)

{ "range-path-index" : [ 
    {
    "path-expression" : "examples.Person/age",
    "scalar-type" : "int",
    "collation" : "",
    "range-value-positions" : "false",
    "invalid-values" : "ignore"
    },
    {
      "scalar-type": "string",
      "collation": "http://marklogic.com/collation/",
      "path-expression": "/some/other/data",
      "range-value-positions": false,
      "invalid-values": "reject"
    }
] }

As shown above, it is not necessary to merge the generated index configuration into the entire properties file and reapply all the property settings. However, you can safely do so if you know that none of the other properties have changed since you retrieved the properties.

For more information on the REST Management API, see the Monitoring MarkLogic Guide and the Scripting Administrative Tasks Guide.

Example: Searching POJOs

The example in this section demonstrates using string and structured queries to search POJOs, as well as pagination of search results. The following topics are covered:

Overview of the Example

The example uses Person objects as POJOs. Each Person contains data such as name, age, gender, unique id, and birthplace. The name is represented by a Name object that contains the first and last name. Age is an integer value. Gender is an enumeration. The remaining properties are strings. Thus, the data availble for a person has the following conceptual structure:

name: 
  firstName: John
  lastName: Doe
gender: MALE
age: 27
id: 123-45-6789
birthplace: Hometown, NY

The id object property is used as the unique POJO identifier.

The example is driven by the PeopleSearch class. Running PeopleSearch.main loads Person objects into the database, performs several searches using string and structured queries, and then removes the objects from the database.

The following methods are the operations of PeopleSearch.

  • dbInit: Load Person objects into the database
  • dbTeardown: Remove all Person objects from the database
  • stringQuery: Perform a string query and print the first page of results
  • doQuery: Perform a structured query and print the first page of results

The PeopleSearch class uses the helper methods stringQuery and doQuery to abstract the invariant mechanics of the search from the query construction.

The stringQuery and doQuery helper methods simply encapsulate the invariant parts of performing a search and displaying the results in order to make it easier to focus on query construction.

Source Code

This section contains the full source code for the example. Copy this code to files in order run the example.

Person Class Definition

Person is the top level POJO class used by the example. Person.getId is annotated as the object id. Additional annotations call out the need for an index on the age property so it can be used in range queries.

Copy the following code into a file with the relative pathname examples/Person.java.

package examples;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.marklogic.client.pojo.annotation.Id;
import com.marklogic.client.pojo.annotation.PathIndexProperty;

public class Person {
    public Person() {}
    public Person(String first, String last, Gender gender, 
                  int age, String id, String birthplace) {
        this.name = new Name(first, last);
        this.age = age;
        this.id = id;
        this.gender = gender;
        this.birthplace = birthplace;
    }
    
    public Name getName() {
        return name;
    }
    public void setName(Name name) {
        this.name = name;
    }
    
    @PathIndexProperty(scalarType=PathIndexProperty.ScalarType.INT)
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
    @Id
    public String getSSN() {
        return id;
    }
    public void setSSN(String ssn) {
        this.id = ssn;
    }
    
    public Gender getGender() {
        return gender;
    }
    public void setGender(Gender gender) {
        this.gender = gender;
    }
    
    @JsonIgnore
    public String getFullName() {
        return this.name.getFullName();
    }
    
    public String getBirthplace() {
        return birthplace;
    }
    public void setBirthplace(String birthplace) {
        this.birthplace = birthplace;
    }
    
    enum Gender {MALE, FEMALE}
    
    private Name name;
    private Gender gender;
    private int age;
    private String id;
    private String birthplace;
}
Name Class Definition

The Name class exists to demonstrate searching sub-objects of your top level POJO class. Each Person object contains a Name.

Copy the following code into a file with the relative pathname examples/Name.java.

package examples;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.marklogic.client.pojo.annotation.PathIndexProperty;

public class Name {
    public Name() { }
    public Name(String first, String last) {
        this.firstName = first;
        this.lastName = last;
    }
    
    @PathIndexProperty(scalarType=PathIndexProperty.ScalarType.STRING)
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    
    @PathIndexProperty(scalarType=PathIndexProperty.ScalarType.STRING)
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    
    @JsonIgnore
    public String getFullName() {
        return this.firstName + " " + this.lastName;
    }
    
    private String firstName;
    private String lastName;
}
PeopleSearch Class Definition

PeopleSearch is the class that drives the examples. The main method loads Person POJOs into the database, performs some searches, and then removes the POJOs from the database.

Copy the following code into a file with the relative path examples/PeopleSearch.java. Modify the call to DatabaseClientFactory.newClient to use your connection information. You will need to change at least the username and password parameter values.

package examples;

import com.marklogic.client.DatabaseClient;
import com.marklogic.client.DatabaseClientFactory;
import com.marklogic.client.DatabaseClientFactory.Authentication;
import com.marklogic.client.pojo.PojoPage;
import com.marklogic.client.pojo.PojoQueryBuilder;
import com.marklogic.client.pojo.PojoQueryBuilder.Operator;
import com.marklogic.client.pojo.PojoQueryDefinition;
import com.marklogic.client.pojo.PojoRepository;
import com.marklogic.client.query.QueryManager;
import com.marklogic.client.query.StringQueryDefinition;

import examples.Person.Gender;


public class PeopleSearch {
  private static DatabaseClient client = DatabaseClientFactory.newClient(
    "localhost", 8000, "user", "password", Authentication.DIGEST);
  private static PojoRepository<Person,String> repo = 
    client.newPojoRepository(Person.class, String.class);

  
  // The pojos to be stored in the database for searching
  private static Person people[] = {
    new Person("John", "Doe", Gender.MALE, 27, "123-45-6789", "Albany, NY"),
    new Person("John", "Smith", 
               Gender.MALE, 41, "234-56-7891", "Las Vegas, NV"),
    new Person("Mary", "John", 
               Gender.FEMALE, 19, "345-67-8912", "Norfolk, VA"),
    new Person("Jane", "Doe", 
               Gender.FEMALE, 72, "456-78-9123", "St. John, FL"),
    new Person("Sally", "St. John", Gender.MALE, 34, 
               "567-89-1234", "Reno, NV"),
    new Person("Kate", "Peters", 
               Gender.FEMALE, 17, "678-91-2345", "Denver, CO")
  };

  // Save the example pojos to the database
  static void dbInit() {
    // Save objects to the database
    for (int i = 0; i < people.length; i++) {
      repo.write(people[i]);
    }
  }
  
  // Remove the pojos from the database
  static void dbTeardown() {
    repo.deleteAll();
  }
  
  // Print one page of results
  static void printResults(PojoPage<Person> matchingObjs) {
    if (matchingObjs.hasContent()) {
      while (matchingObjs.hasNext()) {
        Person person = matchingObjs.next();
        System.out.println("  " + person.getFullName() + " from "
            + person.getBirthplace());
      }
    } else {
      System.out.println("  No matches");
    }
    System.out.println();
  }
  
  // Perform a structured query and print the first page of results
  public void doQuery(PojoQueryDefinition query) {
    printResults(repo.search(query,1));
  }
  
  // Perform a full text search and print first page of results
  public void stringQuery(String qtext) {      
    QueryManager qm = client.newQueryManager();
    StringQueryDefinition query = qm.newStringDefinition().withCriteria(qtext);
    printResults(repo.search(query,1));
  }
  
  // Fetch all matches, one page at a time
  public void fetchAll(PojoQueryDefinition query) {
    PojoPage<Person> matches;
    int start = 1;
    do {
      matches = repo.search(query, start);
      System.out.println("Results " + start + 
                         " thru " + (start + matches.size() - 1));
      printResults(matches);
      start += matches.size();
    } while (matches.hasNextPage());
  }
  
  public static void main(String[] args) {
    PeopleSearch ps = new PeopleSearch();
    
    // load the POJOs
    dbInit();
    
    // Perform a string query
    System.out.println("Full text search for 'john'");
    ps.stringQuery("john");
    
    System.out.println(
      "Full text search for 'john' only where there is no 'NV'");
    ps.stringQuery("john AND -NV");
    
    // Perform structured queries    
    PojoQueryBuilder<Person> qb = repo.getQueryBuilder();

    System.out.println("'john' appears anywhere in the person record");
    ps.doQuery(qb.term("john"));
    
    System.out.println("name contains 'john'");
    ps.doQuery(qb.containerQuery("name", qb.term("john")));

    System.out.println("last name exactly matches 'John'");
    ps.doQuery(qb.value("lastName","John"));
    
    System.out.println("last name contains the term 'john'");
    ps.doQuery(qb.word("lastName", "john"));
    
    System.out.println("First name or last name contains 'john'");
    ps.doQuery(
      qb.containerQuery("name", 
        qb.or(qb.value("firstName", "John"), 
              qb.value("lastName", "John"))));
    
    System.out.println("'john' occurs in lastName property of name");
    ps.doQuery(
      qb.containerQuery("name", 
        qb.containerQuery("lastName", qb.term("john"))));
    
    System.out.println("find all females");
    ps.doQuery(qb.value("gender", "FEMALE"));
    
    // This query requires the existence of a range index on age
    System.out.println("all persons older than 30");
    ps.doQuery(qb.range("age", Operator.GT, 30));
    
    // Demonstrate retrieving successive pages of results.
    // Page length is set artificially low to force multiple pages of results.
    repo.setPageLength(2);
    System.out.println("Retrieve multiple pages of results");
    ps.fetchAll(qb.range("age", Operator.GT, 30));
    
    // comment this line out to leave the objects in the database between runs
    dbTeardown();
    client.release();
  }
}

Exploring the Example Queries

This section provides an overview of the queries performed by the PeopleSearch example. The searches are driven by the helper functions stringSearch and doQuery. These are simply wrappers around PojoRepository.search to abstract the invariant parts of each search, such as displaying the results. For example, the following call to doQuery:

ps.doQuery(qb.value("gender", "FEMALE"));

Is equivalent to the following code, fully unrolled. Additional calls to doQuery in the example vary only by the query that is passed to PojoRepository.search.

PojoPage<Person> matchingObjs = 
  repo.search(qb.value("gender", "FEMALE"),1));
if (matchingObjs.hasContent()) {
  while (matchingObjs.hasNext()) {
    Person person = matchingObjs.next();
    System.out.println("  " + person.getFullName() + " from " +
                       person.getBirthplace());
  }
} else {
  System.out.println("  No matches");
}
System.out.println();

The example begins with some simple string queries. The table below describes the interesting features of these queries.

Query Text Description
"john"
Match the term "john" wherever it appears in the Person objects. The match is not case-sensitive and will match portions of values, such as 'St. John'.
"john AND -NV"
Match Person objects that contain the phrase "john" and do not contain "NV". The '-' operator is a NOT operator in string queries. Since the search term 'NV' is capitalized, that term is matched in a case-sensitive manner. By contrast, the term '-nv' is a case-insensitive match that would match 'nv', 'NV', 'nV', and 'nV'.

The default treatment of case sensistivity in string queries is that phrases that are all lower-case are matched case-insensitive. Upper case or mixed case phrases are handled in a case-sensitive manner. You can control this behavior through the term query option; for details, see term in the Search Developer's Guide.

The remaining queries in the example are structured queries. The table below describes the key characteristics of these queries.

Query Description
qb.term("john")
Match the phrase "john" anywhere in the Person objects. The match is not case-sensitive and will match portions of values, such as 'St. John'.
qb.containerQuery(
  "name", 
  qb.term("john"))
Match the phrase "john" only in the value of the name object property. Matches can be at any level within name.
qb.value("lastName","John")
Match objects whose lastName object property has the exact value "John". Values such as "john" or "St. John' do not match.
qb.word("lastName", "john")

Match objects whose lastName object property value includes the phrase "john". The match is not case-sensitive and will match portions of values, such as 'St. John'.

The search does not recurse through sub-objects. For example, since Person.name is an object, qb.word("name", "john") finds no matches because it will not look into the values of lastName and firstName object properties.

The lastName object property can appear at any level. That is, it is not restricted to occurrences within name.

qb.containerQuery(
  "name",
  qb.or(
    qb.value("firstName","John"),
    qb.value("lastName","John")))
Match objects whose lastName or firstName object property is exactly "John". You can combine arbitrarily complex queries together.
qb.containerQuery(
  "name",
  qb.containerQuery(
    "lastName",
    qb.term("john")))
Match objects whose name property contains a lastName property that includes the phrase "john" at any level.
qb.value("gender", "FEMALE")
Match objects whose gender property is exactly the value "FEMALE". The match must be exact.
qb.range("age", Operator.GT, 30)
Match objects whose age property value is greater than 30. The database configuration must include a path range index on age of type int. If a matching index is not found, a XDMP-PATHRIDXNOTFOUND error occurs. For details, see How Indexing Affects Searches.

The final query in the example demonstrates pagination of query results, using the PeopleSearch.fetchAll helper function. The query result page length is first set to 2 to force pagination to occur on our small results. After this call, each call to PojoRepository.search or PojoRepository.readAll will return at most 2 results.

repo.setPageLength(2);

The fetchAll helper function below repeatedly call PojoRepository.search (and prints out the results) until there are no more pending matches. Each call to search includes the starting position of the first match to return. This parameter starts out as 1, to retrieve the first match, and is incremented each time by the number of matches on the fetched page (PojoPage.size). The loop terminates when there are no more results (PojoPage.hasNextPage returns false).

public void fetchAll(PojoQueryDefinition query) {
    PojoPage<Person> matches;
    int start = 1;
    do {
      matches = repo.search(query, start);
      System.out.println("Results " + start + 
                         " thru " + (start + matches.size() - 1));
      printResults(matches);
      start += matches.size();
    } while (matches.hasNextPage());
  }

Retrieving POJOs Incrementally

By default, when you retrieve POJOs using PojoRepository.read or PojoRepository.search, the number of results returned is limited to one 'page'. Paging results enables you to retrieve large result sets without consuming undue resources or bandwidth.

The number of results per page is configurable on PojoRepository. The default page length is 10, meaning at most 10 objects are returned. You can change the page length using PojoRepository.setPageLength. When you're reading POJOs by id, you can also retrieve an unconstrained number of results by calling PojoRepository.readAll.

All PojoRepository methods for retrieving POJOs include a 'start' parameter you can use to specify the 1-based index of the first object to return from the result set. Use this parameter in conjunction with the page length to iteratively retrieve all results.

For example, the following function fetches successive groups of Person objects matching a query. For a runnable example, see Example: Searching POJOs.

public void fetchAll(PojoQueryDefinition query) {
    PojoPage<Person> matches;
    int start = 1;
    do {
      matches = repo.search(query, start);
      // ...do something with the matching objects...
      start += matches.size();
    } while (matches.hasNextPage());
  }

Both PojoRepository.search and PojoRepository.read return results in a PojoPage. Use the same basic strategy whether fetching objects by id or by query.

A PojoPage can container fewer than PojoRepository.getPageLength objects, but will never contain more.

Removing POJOs from the Database

You can delete POJOs from the database in two ways:

  • By id, using PojoRepository.delete. You can specify one or more object ids.
  • By POJO class, using PojoRepository.deleteAll.

Since a PojoRepository is bound to a specific POJO class, calling PojoRepository.deleteAll removes all POJOs of the bound type from the database.

Testing Your POJO Class for Serializability

You can only use the data binding interfaces with Java POJO classes that can be serialized and deserialized by Jackson. You can use a test such as the following to check whether or not your POJO class is serializable.

try {
  String value = objectMapper.writeValueAsString(
    new MyClass(42,"hello"));
  MyClass newobj = objectMapper.readValue(value, MyClass.class);
  // class is serializable if no exception is raised by objectMapper
} catch (Exception e) {
  e.printStackTrace();
}

Troubleshooting

This section contains topics for troubleshooting errors and surprising behaviors you might encounter while working with the POJO interfaces. The following topics are covered:

Error: XDMP-UNINDEXABLEPATH

If you see an error similar to the following:

search failed: Internal Server Error. Server Message: XDMP-UNINDEXABLEPATH: examples.PojoSearch$Person/id

Then you are probably using an object property of a nested class as the target of your @Id annotation. You cannot use the POJO interfaces with nested classes.

Nested class names serialize with a '$' in their name, such as examples.PojoSearch$Person, above. Path expressions with such symbols in them cannot be indexed.

Error: XDMP-PATHRIDXNOTFOUND

If you see an error similar to the following:

search failed: Bad Request. Server Message: XDMP-PATHRIDXNOTFOUND: cts:search(...)

Then you need to configure a supporting index in the database in which you store your POJOs. For details, see How Indexing Affects Searches and Creating Indexes from Annotations.

Unexpected Search Results

If your POJO search does not return the results you expect, you can dump out the serialization of the query produced by PojoQueryBuilder to see if the resulting structured query is what you expect. For example:

System.out.println(qb.range("age", Operator.GT, 30).serialize());
==>
<query xmlns="http://marklogic.com/appservices/search"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:search="http://marklogic.com/appservices/search"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <range-query type="xs:int">
    <path-index>examples.Person/age</path-index>
    <value>30</value>
    <range-operator>GT</range-operator>
  </range-query>
</query>

If your query looks as you expect, the surprising results might be the result of using unfiltered search. Search on POJOs are unfiltered by default, which makes the search faster, but can produce false positives. For details, see How Indexing Affects Searches.

« Previous chapter
Next chapter »