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:
Keyword | Sample | Logical result |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(No keyword) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
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.
Content feedback and comments