Spring Data for Tanzu GemFire 2.0

Spring Data for Tanzu GemFire Repositories

Last Updated February 19, 2025

Spring Data for Tanzu GemFire provides support for using the Spring Data Repository abstraction to easily persist entities into GemFire along with executing queries. For a general introduction to the Repository programming model, see Working with Spring Data Repositories in the Spring Data Commons Reference Documentation.

Spring Java-based Configuration

You can use Spring’s Java-based container configuration. For more information about this configuration, see Java-based Container Configuration in Core Technologies in the Spring product documentation.

Using this approach, you can bootstrap Spring Data Repositories by using the Spring Data for Tanzu GemFire @EnableGemfireRepositories annotation, as the following example shows:

Example 2. Bootstrap Spring Data for Tanzu GemFire Repositories with @EnableGemfireRepositories

@SpringBootApplication
@EnableGemfireRepositories(basePackages = "com.example.acme.repository")
class SpringDataApplication {
  ...
}

You can use the type-safe basePackageClasses attribute instead of using the basePackages attribute. The basePackageClasses lets you specify the package that contains all your application Repository classes by specifying only one of your application Repository interface types. Consider creating a special no-op marker class or interface in each package that serves no purpose other than to identify the location of application Repositories referenced by this attribute.

In addition to the basePackages and basePackageClasses attributes, like Spring’s @ComponentScan annotation, the @EnableGemfireRepositories annotation provides include and exclude filters, based on Spring’s ComponentScan.Filter type. You can use the filterType attribute to filter by different aspects, such as whether an application Repository type is annotated with a particular annotation or extends a particular class type. For more details, see the FilterType Javadoc.

The @EnableGemfireRepositories annotation also lets you specify the location of named OQL queries, which reside in a Java Properties file, by using the namedQueriesLocation attribute. The property name must match the name of a Repository query method and the property value is the OQL query you want executed when the Repository query method is called.

The repositoryImplementationPostfix attribute can be set to an alternate value (defaults to Impl) if your application requires one or more custom repository implementations. This feature is commonly used to extend the Spring Data Repository infrastructure to implement a feature not provided by the data store (for example, Spring Data for Tanzu GemFire).

One example of where custom repository implementations are needed with GemFire is when performing joins. Joins are not supported by Spring Data for Tanzu GemFire Repositories. With a GemFire PARTITION Region, the join must be performed on collocated PARTITION Regions, since GemFire does not support “distributed” joins. In addition, the Equi-Join OQL Query must be performed inside a GemFire Function. For more information about GemFire Equi-Join Queries, see Performing an Equi-Join Query on Partitioned Regions in the GemFire product documentation.

You can customize many other aspects of the The Spring Data for Tanzu GemFire Repository infrastructure extension. For details about all configuration settings, see @EnableGemfireRepositories.

Spring XML Configuration

To bootstrap Spring Data Repositories, use the <repositories/> element from the Spring Data for Tanzu GemFire Data namespace, as the following example shows:

Example 1. Bootstrap Spring Data for Tanzu GemFire Repositories in XML

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:gfe-data="{spring-data-access-schema-namespace}"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
    http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
    {spring-data-access-schema-namespace} {spring-data-access-schema-location}
">

  <gfe-data:repositories base-package="com.example.acme.repository"/>

</beans>

The preceding configuration snippet looks for interfaces below the configured base package and creates Repository instances for those interfaces backed by a SimpleGemFireRepository.

Note: The bootstrap process fails unless you have correctly mapped your application domain classes to configured Regions.

Executing OQL Queries

Spring Data for Tanzu GemFire Repositories enable the definition of query methods to easily execute GemFire OQL queries against the Region the managed entity maps to, as the following example shows:

Example 3. Sample Repository

@Region("People")
public class Person { ... }
public interface PersonRepository extends CrudRepository<Person, Long> {

  Person findByEmailAddress(String emailAddress);

  Collection<Person> findByFirstname(String firstname);

  @Query("SELECT * FROM /People p WHERE p.firstname = $1")
  Collection<Person> findByFirstnameAnnotated(String firstname);

  @Query("SELECT * FROM /People p WHERE p.firstname IN SET $1")
  Collection<Person> findByFirstnamesAnnotated(Collection<String> firstnames);
}

The first query method listed in the preceding example causes the following OQL query to be derived: SELECT x FROM /People x WHERE x.emailAddress = $1. The second query method works the same way except it returns all entities found, whereas the first query method expects a single result to be found.

If the supported keywords are not sufficient to declare and express your OQL query, or the method name becomes too verbose, then you can annotate the query methods with @Query as shown on the third and fourth methods.

The following table gives brief samples of the supported keywords that you can use in query methods:

Table 1. Supported keywords for query methods
Keyword Sample Logical result

GreaterThan

findByAgeGreaterThan(int age)

x.age > $1

GreaterThanEqual

findByAgeGreaterThanEqual(int age)

x.age >= $1

LessThan

findByAgeLessThan(int age)

x.age < $1

LessThanEqual

findByAgeLessThanEqual(int age)

x.age ⇐ $1

IsNotNull,NotNull

findByFirstnameNotNull()

x.firstname =! NULL

IsNull,Null

findByFirstnameNull()

x.firstname = NULL

In

findByFirstnameIn(Collection x)

x.firstname IN SET $1

NotIn

findByFirstnameNotIn(Collection x)

x.firstname NOT IN SET $1

IgnoreCase

findByFirstnameIgnoreCase(String firstName)

x.firstname.equalsIgnoreCase($1)

(No keyword)

findByFirstname(String name)

x.firstname = $1

Like

findByFirstnameLike(String name)

x.firstname LIKE $1

Not

findByFirstnameNot(String name)

x.firstname != $1

IsTrue,True

findByActiveIsTrue()

x.active = true

IsFalse,False

findByActiveIsFalse()

x.active = false

OQL Query Extensions Using Annotations

Many query languages, such as GemFire’s OQL (Object Query Language), have extensions that are not directly supported by Spring Data Commons’ Repository infrastructure.

One of Spring Data Commons’ Repository infrastructure goals is to function as the lowest common denominator to maintain support for and portability across the widest array of data stores available and in use for application development today. Technically, this means developers can access multiple different data stores supported by Spring Data Commons within their applications by reusing their existing application-specific Repository interfaces — a convenient and powerful abstraction.

To support GemFire’s OQL Query language extensions and preserve portability across different data stores, Spring Data for Tanzu GemFire adds support for OQL Query extensions by using Java annotations. These annotations are ignored by other Spring Data Repository implementations that do not have similar query language features.

For example, many data stores most likely do not implement GemFire’s OQL IMPORT keyword. Implementing IMPORT as an annotation (that is, @Import) rather than as part of the query method signature (specifically, the method ‘name’) does not interfere with the parsing infrastructure when evaluating the query method name to construct another data store language appropriate query.

The set of GemFire OQL Query language extensions that are supported by Spring Data for Tanzu GemFire include the following:

Table 2. Supported GemFire OQL extensions for Repository query methods
Keyword Annotation Description Arguments
HINT @Hint OQL query index hints String[] (Example: @Hint({ "IdIdx", "TxDateIdx" }))
IMPORT @Import Qualify application-specific types. String (Example: @Import("org.example.app.domain.Type"))
LIMIT @Limit Limit the returned query result set. Integer (Example: @Limit(10); default is Integer.MAX_VALUE)
TRACE @Trace Enable OQL query-specific debugging. N/A

As an example, suppose you have a Customers application domain class and corresponding GemFire Region along with a CustomerRepository and a query method to lookup Customers by last name, as follows:

Example 4. Sample Customers Repository

package ...;

import org.springframework.data.annotation.Id;
import org.springframework.data.gemfire.mapping.annotation.Region;
...

@Region("Customers")
public class Customer ... {

  @Id
  private Long id;

  ...
}
package ...;

import org.springframework.data.gemfire.repository.GemfireRepository;
...

public interface CustomerRepository extends GemfireRepository<Customer, Long> {

  @Trace
  @Limit(10)
  @Hint("LastNameIdx")
  @Import("org.example.app.domain.Customer")
  List<Customer> findByLastName(String lastName);

  ...
}

The preceding example results in the following OQL Query:

<TRACE> <HINT 'LastNameIdx'> IMPORT org.example.app.domain.Customer; SELECT * FROM /Customers x WHERE x.lastName = $1 LIMIT 10

The Spring Data for Tanzu GemFire Repository extension is careful not to create conflicting declarations when the OQL annotation extensions are used in combination with the @Query annotation.

As another example, suppose you have a raw @Query annotated query method defined in your CustomerRepository, as follows:

Example 5. CustomerRepository

public interface CustomerRepository extends GemfireRepository<Customer, Long> {

  @Trace
  @Limit(10)
  @Hint("CustomerIdx")
  @Import("org.example.app.domain.Customer")
  @Query("<TRACE> <HINT 'ReputationIdx'> SELECT DISTINCT * FROM /Customers c WHERE c.reputation > $1 ORDER BY c.reputation DESC LIMIT 5")
  List<Customer> findDistinctCustomersByReputationGreaterThanOrderByReputationDesc(Integer reputation);

}

The preceding query method results in the following OQL query:

IMPORT org.example.app.domain.Customer; 
<TRACE> <HINT 'ReputationIdx'> SELECT DISTINCT * FROM /Customers x WHERE x.reputation > $1 ORDER BY c.reputation DESC LIMIT 5

The @Limit(10) annotation does not override the LIMIT explicitly defined in the raw query. Also, the @Hint("CustomerIdx") annotation does not override the HINT explicitly defined in the raw query. Finally, the @Trace annotation is redundant and has no additional effect.

Note: The ReputationIdx index is probably not the most sensible index, given the number of customers who may possibly have the same value for their reputation, which reduces the effectiveness of the index. Please choose indexes and other optimizations wisely, as an improper or poorly chosen index can have the opposite effect on your performance because of the overhead in maintaining the index. The ReputationIdx was used only to serve the purpose of the example.

Query Post Processing

Thanks to using the Spring Data Repository abstraction, the query method convention for defining data store-specific queries (e.g. OQL) is easy and convenient. However, it is sometimes desirable to inspect or even possibly modify the query generated from the Repository query method.

Since 2.0.x, Spring Data for Tanzu GemFire includes the o.s.d.gemfire.repository.query.QueryPostProcessor functional interface. The interface is loosely defined as follows:

Example 6. QueryPostProcessor

package org.springframework.data.gemfire.repository.query;

import org.springframework.core.Ordered;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.QueryMethod;
import ...;

@FunctionalInterface
interface QueryPostProcessor<T extends Repository, QUERY> extends Ordered {

  default QUERY postProcess(QueryMethod queryMethod, QUERY query, Object... arguments) {...}

}

There are additional default methods provided that let you compose instances of QueryPostProcessor similar to the way that java.util.function.Function.andThen(:Function) and java.util.function.Function.compose(:Function) work.

Additionally, the QueryPostProcessor interface implements the org.springframework.core.Ordered interface, which is useful when multiple QueryPostProcessors are declared and registered in the Spring container and used to create a pipeline of processing for a group of generated query method queries.

Finally, the QueryPostProcessor accepts type arguments corresponding to the type parameters, T and QUERY, respectively. Type T extends the Spring Data Commons marker interface, org.springframework.data.repository.Repository. We discuss this further later in this section. All QUERY type parameter arguments in The Spring Data for Tanzu GemFire case are of type java.lang.String.

Note: Note It is useful to define the query as type QUERY, since this QueryPostProcessor interface may be ported to Spring Data Commons and therefore must handle all forms of queries by different data stores.

You can implement this interface to receive a callback with the query that was generated from the application Repository interface method when the method is called.

For example, you can log all queries from all application Repository interface definitions. Do this by using the following QueryPostProcessor implementation:

Example 7. LoggingQueryPostProcessor

package example;

import ...;

class LoggingQueryPostProcessor implements QueryPostProcessor<Repository, String> {

  private Logger logger = Logger.getLogger("someLoggerName");

  @Override
  public String postProcess(QueryMethod queryMethod, String query, Object... arguments) {

      String message = String.format("Executing query [%s] with arguments [%s]", query, Arrays.toString(arguments));

      this.logger.info(message);
  }
}

The LoggingQueryPostProcessor was typed to the Spring Data org.springframework.data.repository.Repository marker interface, and, therefore, logs all application Repository interface query method generated queries.

You could limit the scope of this logging to queries only from certain types of application Repository interfaces, such as a CustomerRepository, as the following example shows:

Example 8. CustomerRepository

interface CustomerRepository extends CrudRepository<Customer, Long> {

  Customer findByAccountNumber(String accountNumber);

  List<Customer> findByLastNameLike(String lastName);

}

Then you could have typed the LoggingQueryPostProcessor specifically to the CustomerRepository, as follows:

Example 9. CustomerLoggingQueryPostProcessor

class LoggingQueryPostProcessor implements QueryPostProcessor<CustomerRepository, String> { ... }

As a result, only queries defined in the CustomerRepository interface, such as findByAccountNumber, are logged.

You might want to create a QueryPostProcessor for a specific query defined by a Repository query method. For example, suppose you want to limit the OQL query generated from the CustomerRepository.findByLastNameLike(:String) query method to only return five results along with ordering the Customers by firstName, in ascending order . To do so, you can define a custom QueryPostProcessor, as the following example shows:

Example 10. OrderedLimitedCustomerByLastNameQueryPostProcessor

class OrderedLimitedCustomerByLastNameQueryPostProcessor implements QueryPostProcessor<CustomerRepository, String> {

  private final int limit;

  public OrderedLimitedCustomerByLastNameQueryPostProcessor(int limit) {
    this.limit = limit;
  }

  @Override
  public String postProcess(QueryMethod queryMethod, String query, Object... arguments) {

    return "findByLastNameLike".equals(queryMethod.getName())
      ? query.trim()
          .replace("SELECT", "SELECT DISTINCT")
          .concat(" ORDER BY firstName ASC")
          .concat(String.format(" LIMIT %d", this.limit))
      : query;
  }
}

While the preceding example works, you can achieve the same effect by using the Spring Data Repository convention provided by Spring Data for Tanzu GemFire. For example, the same query could be defined as follows:

Example 11. CustomerRepository using the convention

interface CustomerRepository extends CrudRepository<Customer, Long> {

  @Limit(5)
  List<Customer> findDistinctByLastNameLikeOrderByFirstNameDesc(String lastName);

}

However, if you do not have control over the application CustomerRepository interface definition, then the QueryPostProcessor (that is, OrderedLimitedCustomerByLastNameQueryPostProcessor) is convenient.

If you want to ensure that the LoggingQueryPostProcessor always comes after the other application-defined QueryPostProcessors that may have been declared and registered in the Spring ApplicationContext, you can set the order property by overriding the o.s.core.Ordered.getOrder() method, as the following example shows:

Example 12. Defining the order property

class LoggingQueryPostProcessor implements QueryPostProcessor<Repository, String> {

  @Override
  public int getOrder() {
    return 1;
  }
}
class CustomerQueryPostProcessor implements QueryPostProcessor<CustomerRepository, String> {

  @Override
  public int getOrder() {
    return 0;
  }
}

This ensures that you always see the effects of the post-processing applied by other QueryPostProcessors before the LoggingQueryPostProcessor logs the query.

You can define as many QueryPostProcessors in the Spring ApplicationContext as you like and apply them in any order, to all or specific application Repository interfaces, and be as granular as you like by using the provided arguments to the postProcess(...) method callback.