Provides Spring Data approach to work with Reindexer database.
<dependency>
<groupId>io.github.evgeniycheban</groupId>
<artifactId>spring-data-reindexer</artifactId>
<version>${spring-data-reindexer.version}</version>
</dependency>
Here is an example of basic spring-data-reindexer
usage:
import ru.rt.restream.reindexer.Reindexer;
import ru.rt.restream.reindexer.ReindexerConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.reindexer.repository.config.EnableReindexerRepositories;
import org.springframework.data.reindexer.repository.config.ReindexerConfigurationSupport;
@Configuration
@EnableReindexerRepositories
public class ReindexerConfig extends ReindexerConfigurationSupport {
@Bean
public Reindexer reindexer() {
return ReindexerConfiguration.builder()
.url("cproto://localhost:6534/items")
.getReindexer();
}
}
import ru.rt.restream.reindexer.annotations.Reindex;
import org.springframework.data.reindexer.core.mapping.Namespace;
@Namespace(name = "items")
public class Item {
@Reindex(name = "id", isPrimaryKey = true)
private Long id;
@Reindex(name = "name")
private String name;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
import java.util.Optional;
import org.springframework.data.reindexer.repository.ReindexerRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ItemRepository extends ReindexerRepository<Item, Long> {
Optional<Item> findByName(String name);
}
The ItemRepository
can be injected into spring-managed beans using @Autowired
or
another Spring Framework approach.
@Autowired
private ItemRepository repository;
More examples can be found here.
Transactions are implemented using @Transactional
annotation approach, to enable
transaction management support @EnableTransactionManagement
annotation should be placed
on the @Configuration
class and ReindexerTransactionManager
bean should be defined in
the context.
Note that ReindexerTransactionManager
manages transactions for a single namespace,
therefore it should be defined with a domain class that is mapped to a Reindexer
namespace.
Here is an example of basic transaction management usage:
import ru.rt.restream.reindexer.Reindexer;
import ru.rt.restream.reindexer.ReindexerConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.reindexer.ReindexerTransactionManager;
import org.springframework.data.reindexer.repository.config.EnableReindexerRepositories;
import org.springframework.data.reindexer.repository.config.ReindexerConfigurationSupport;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableReindexerRepositories
@EnableTransactionManagement
public class TransactionalReindexerConfig extends ReindexerConfigurationSupport {
@Bean
public Reindexer reindexer() {
return ReindexerConfiguration.builder()
.url("cproto://localhost:6534/items")
.getReindexer();
}
@Bean
public ReindexerTransactionManager<Item> itemTxManager(Reindexer reindexer) {
return new ReindexerTransactionManager<>(reindexer, Item.class);
}
}
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ItemTransactionalService {
private final ItemRepository repository;
public ItemTransactionalService(ItemRepository repository) {
this.repository = repository;
}
@Transactional(transactionManager = "itemTxManager")
public Item save(Item item) {
return this.repository.save(item);
}
}
The @Query
annotation is used to declare SQL-based Reindexer queries
directly on repository methods.
Parameter binding supports named parameters using @Param
annotation as well
as parameter number links.
@Query("SELECT * FROM items WHERE name = :name")
Optional<Item> findOneByName(@Param("name") String name);
@Query("SELECT * FROM items WHERE name = ?1")
Optional<Item> findOneByName(String name);
Query by Example (QBE) is a user-friendly querying technique with a simple interface.
It allows dynamic query creation and does not require you to write queries that contain
field names. In fact, Query by Example does not require you to write queries by using
query-methods or @Query
annotation.
Item item = new Item();
item.setName("Test");
NestedItem nestedItem = new NestedItem();
nestedItem.setValue("Value");
item.setNestedItem(nestedItem);
List<Item> items = this.repository.findAll(Example.of(item, ExampleMatcher.matching()
.withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING))
.withMatcher("nestedItem.value", ExampleMatcher.GenericPropertyMatcher.of(StringMatcher.ENDING))));
More information can be read in Spring Data reference guide.
The child-objects can be stored not only within the namespace,
they can also be stored separately using @NamespaceReference
annotation to refer a child-namespace.
The @NamespaceReference
annotation has the following attributes to specify how child-namespace is mapped and loaded from Reindexer:
namespace (String, optional)
Represents a namespace name to refer, defaults to@Namespace
entity definition.indexName (String, required)
The index name to be used from parent-namespace to refer to the child-namespace, typically this index stores a child-namespaceid
value.joinType (JoinType, optional)
The join type to be used to match values in parent and child namespaces, possible values:JoinType.LEFT (default)
Returns all records from the left (parent) namespace, and the matched records from the right (child) namespace.JoinType.RIGHT
Returns records that have matching values in both parent and child namespaces.
lazy (boolean, optional)
Controls whether the referenced entity should be loaded lazily. This defaults tofalse
.fetch (boolean, optional)
Controls whether the referenced entity should be fetched if it is a nested relationship within the child-object of the top level entity. This defaults tofalse
.
Long joinedItemId;
Long nestedJoinedItemId;
List<Long> joinedItemIds;
@Transient
@NamespaceReference(indexName = "joinedItemId", joinType = JoinType.INNER)
JoinedItem joinedItem;
@Transient
@NamespaceReference(indexName = "nestedJoinedItemId", fetch = true)
JoinedItem nestedJoinedItem;
@Transient
@NamespaceReference(indexName = "joinedItemIds", lazy = true)
List<JoinedItem> joinedItems;
- The
@Transient
annotation is required to use with@NamespaceReference
to indicate that Reindexer should not store child objects in the parent-namespace and therefore those objects should be loaded through referredindexName
. - When the
lazy
attribute is set totrue
the referenced entity is loaded through proxy object. Depending on a mapped type the framework would create either interface-based (JDK dynamic proxies) or class-based proxies (CGLIB),final
classes cannot be proxied since CGLIB relies on creating a subclass for the type being proxied. Whenlazy
attribute is set totrue
thejoinType
attribute is ignored since the object would be retrieved from Reindexer usingselect
query, for single result the query condition would beCondition.EQ
and for collection-like result type the query condition would beCondition.SET
with the value stored inindexName
specified in@NamespaceReference
annotation. The proxy object is thread-safe meaning that it is safe to access proxy object from multiple threads, and the initialization of proxy object would be triggered only once. - In the current implementation there is no option to provide a custom query for lazy loading namespace reference, however it might be provided it future releases.
- A
JoinType
is only applied to fetch non-lazy child-objects of the top level entity if you need to fetch deeply nested child-objects likeA - B - C
usefetch = true
to fetch objectC
, it will be fetched lazily. Reindexer does not support joins for deeply nested child-objects therefore they can only be loaded usingfetch = true
attribute.
Projections allow creating dedicated return types based on certain attributes of domain types. You can create partial views using interface-based or class-based projections.
The easiest way to limit the result of the queries to only name and value attributes is by declaring an interface that exposes accessor methods for the properties to be read, as shown in the following example:
A projection interface to retrieve a subset of attributes
interface ItemNameValue {
String getName();
String getValue();
}
The important bit here is that the properties defined here exactly match properties in the aggregate root. Doing so lets a query method be added as follows:
A repository using an interface based projection with a query method
interface ItemRepository extends ReindexerRepository<Item, Long> {
Collection<ItemNameValue> findByName(String name);
}
The query execution engine creates proxy instances of that interface at runtime for each element returned and forwards calls to the exposed methods to the target object.
Projections can be used recursively. If you want to include some of the JoinedItem information as well, create a projection interface for that and return that interface from the declaration of getJoinedItem(), as shown in the following example:
A projection interface to retrieve a subset of attributes
interface ItemNameValue {
String getName();
String getValue();
JoinedItemName getJoinedItem();
interface JoinedItemName {
String getName();
}
}
More information regarding Spring Data Projections and the difference between interface-based, class-based and dynamic projections can be read in Spring Data reference guide.
The following example of a Double Converter implementation converts from a Double to a custom Price value object:
public record Price (double price) {
}
@ReadingConverter
public class PriceReadingConverter implements Converter<Double, Price> {
public Price convert(Double source) {
return new Price(source);
}
}
If you write a Converter
whose source and target types are native types, we cannot determine
whether we should consider it as a reading or a writing converter. Registering the converter
instance as both might lead to unwanted results. For example, a Converter<String, Long>
is ambiguous,
although it probably does not make sense to try to convert all String
instances into Long
instances when writing.
To let you force the infrastructure to register a converter for only one way, we provide @ReadingConverter
and @WritingConverter
annotations to be used in the converter implementation.
Note:
In the current version @WritingConverter
annotation is not supported, it will be provided in future releases.
Converters are subject to explicit registration as instances are not picked up from a
classpath or container scan to avoid unwanted registration with a conversion service and
the side effects resulting from such a registration. Converters are registered with
CustomConversions
as the central facility that allows registration and querying for
registered converters based on source and target types.
The following example shows how converters are registered with CustomConverters
:
@Configuration
@EnableReindexerRepositories
public class ReindexerConfig extends ReindexerConfigurationSupport {
@Override
public ReindexerCustomConversions customConversions() {
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new PriceReadingConverter());
return new ReindexerCustomConversions(StoreConversions.NONE, converters);
}
}
More information can be read in Spring Data reference guide.
- Support more return types for
ReindexerRepository
.