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:
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:
PojoRepository
to save your objects in the database. You can create, read, update, and delete persisted objects.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.
You should be aware of the following restrictions and limitations of the data binding feature:
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.
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.
Integer
, String
, or Float
, rather than a complex object type such as Calendar
.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 coordinate 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 coordinate value, in support of a geospatial element pair index. For details, see Creating Indexes from Annotations. |
@GeoSpatialPathIndexProperty |
Identifies a property for which a geospatial point path range index is required. Any property on which you perform geospatial point queries must be indexed. For details, see Creating Indexes from Annotations. |
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:
@Id
annotation, as described in Annotating Your Object Definition.com.marklogic.client.DatabaseClient
object. For example, if using digest authentication:DatabaseClient client = DatabaseClientFactory.newClient( host, port, new DigestAuthContext(username, password));
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);
PojoRepository.write
to save objects to the database. For example:MyClass obj = new MyClass(); myClass.setId(42); myClassRepo.write(obj);
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
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:
@Id
annotation , as described in Annotating Your Object Definition.com.marklogic.client.DatabaseClient
object. For example, if using digest authentication:DatabaseClient client = DatabaseClientFactory.newClient( host, port, new DigestAuthContext(username, password));
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);
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});
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.
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.DigestAuthContext; 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, new DigestAuthContext(user, password)); // 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(); } }
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:
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.
@Id
annotation, as described in Annotating Your Object Definition.com.marklogic.client.DatabaseClient
object. For example, if using digest authentication:DatabaseClient client = DatabaseClientFactory.newClient( host, port, new DigestAuthContext(username, password));
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);
myClassRepo.setPageLength(5);
StringQueryDefinition
or StructuredQueryDefinition
that represents the objects you want to find. 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");
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");
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(); ... }
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
.
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.
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 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.
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.
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.
@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; } ... }
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" : [ ] }
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 Management API while preserving existing indexes follow this procedure:
allProperties.json
:$ curl --anyauth --user user:password -X GET \ -H "Accept: application/json" -o allProperties.json http://localhost:8002/manage/LATEST/databases/Documents/properties
$ 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-region-path-indexes" : [ ], "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,
geospatial-region-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.
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:
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 available 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 databasedbTeardown
: Remove all Person
objects from the databasestringQuery
: Perform a string query and print the first page of resultsdoQuery
: Perform a structured query and print the first page of resultsThe 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.
This section contains the full source code for the example. Copy this code to files in order run the example.
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; }
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
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.DigestAuthContext; 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, new DigestAuthContext(USER, PASSWORD)); 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(); } }
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.
The default treatment of case sensitivity 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 The search does not recurse through sub-objects. For example, since The |
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()); }
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.
You can delete POJOs from the database in two ways:
PojoRepository.delete
. You can specify one or more object ids.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.
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(); }
This section contains topics for troubleshooting errors and surprising behaviors you might encounter while working with the POJO interfaces. The following topics are covered:
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.
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.
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.