【Spring连载】使用Spring Data访问Redis(十四)----Redis Repositories
- 一、核心概念
- 二、定义Repository接口
- 三、创建Repository实例
- 四、用法
- 五、对象映射基础Object Mapping Fundamentals
- 六、对象到哈希映射Object-to-Hash Mapping
- 七、Keyspaces
- 八、二级索引Secondary Indexes
- 九、存活时间Time To Live
- 十、Redis特定的查询方法
- 十一、Query by Example
- 十二、在Redis集群上运行Redis Repositories
- 十三、Redis Repositories 剖析
- 十四、投影Projections
- 十五、自定义存储库实现
- 十六、从聚合根(Aggregate Roots)发布事件
- 十七、存储库方法的null处理
- 十八、CDI 集成
- 十九、存储库查询关键字
- 二十、存储库查询返回类型
- 二十一、定义查询方法
本文解释了Spring Data repository的基本基础和Redis的特性。在继续学习Redis特性之前,请确保你对基本概念有充分的理解。
Spring Data repository抽象(abstraction)的目标是显著减少为各种持久性存储实现数据访问层所需的样板代码量。
使用Redis Repositories可以无缝地转换和存储域对象到Redis Hashes中,应用自定义映射策略,并使用辅助索引。
Redis Repositories至少需要Redis Server 2.8.0版本,并且不适用于事务。请确保使用已禁用事务支持的RedisTemplate。
一、核心概念
二、定义Repository接口
三、创建Repository实例
本节介绍如何为已定义的存储库接口创建实例和bean定义。
3.1 Java配置
在Java配置类上使用特定于存储的@EnableRedisRepositories注解来定义存储库激活的配置。有关Spring容器基于Java的配置的介绍,请参阅Spring参考文档中的JavaConfig。
启用Spring数据存储库的示例配置如下所示:
基于注解的存储库配置示例
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
前面的示例使用特定于JPA的注解,你可以根据实际使用的存储模块对其进行更改。这同样适用于EntityManagerFactory bean的定义。请参阅介绍特定于存储的配置的部分。
3.2 XML配置
每个Spring Data模块都包括一个存储库元素,该元素允许您定义Spring为您扫描的基本包,如以下示例所示:
通过XML启用Spring Data存储库
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="com.acme.repositories" />
</beans:beans>
在前面的示例中,Spring被指示扫描com.acme.repositories及其所有子包,以查找继承Repository 或其子接口之一的接口。对于找到的每个接口,基础结构(infrastructure)都注册特定于持久化技术的FactoryBean,以创建适当的代理来处理查询方法的调用。每个bean都注册在从接口名称派生的bean名称下,因此UserRepository的接口将注册在userRepository下。嵌套存储库接口的Bean名称以其封闭类型名称为前缀。基本包属性允许使用通配符,以便你可以定义扫描包的模式(pattern )。
3.3 使用过滤器
默认情况下,基础结构(infrastructure)会拾取位于配置的基本包下的每个继承持久化技术特定的Repository子接口的接口,并为其创建一个bean实例。但是,你可能需要更细粒度地控制为哪些接口创建bean实例。为此,请在存储库声明中使用过滤器元素。语义与Spring的组件过滤器中的元素完全等效。有关详细信息,请参阅这些元素的Spring参考文档。
例如,要将某些接口从实例化为存储库bean中排除,可以使用以下配置:
使用过滤器Java配置
@Configuration
@EnableRedisRepositories(basePackages = "com.acme.repositories",
includeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeRepository") },
excludeFilters = { @Filter(type = FilterType.REGEX, pattern = ".*SomeOtherRepository") })
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
使用过滤器xml配置
<repositories base-package="com.acme.repositories">
<context:include-filter type="regex" expression=".*SomeRepository" />
<context:exclude-filter type="regex" expression=".*SomeOtherRepository" />
</repositories>
前面的例子包括所有以SomeRepository结尾的接口,而不包括那些以SomeOtherRepository结束的接口。
3.4 独立使用
你还可以在Spring容器之外使用存储库基础结构(infrastructure) — 例如在CDI环境中。类路径中仍然需要一些Spring库,但通常也可以通过编程设置存储库。提供存储库支持的Spring Data模块附带了一个特定于持久化技术的RepositoryFactory,你可以使用它,如下所示:
repository工厂的独立使用
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
四、用法
Spring Data Redis让你可以轻松实现域(domain)实体,如下例所示:
例1:Person实体
@RedisHash("people")
public class Person {
@Id String id;
String firstname;
String lastname;
Address address;
}
这里有一个非常简单的域对象。请注意,它的类型上有一个@RedisHash注释和一个名为id的属性,该属性用org.springframework.data.annotation.Id进行注解。这两项负责创建用于持久化哈希的实际key。
用@Id注解的属性以及那些命名为id的属性被视为标识符属性。那些有注解的比其他的更受青睐。
现在要真正拥有一个负责存储和检索的组件,我们需要定义一个存储库接口,如以下示例所示:
例2。持久化Person实体的基本Repository接口
public interface PersonRepository extends CrudRepository<Person, String> {
}
由于我们的repository继承了CrudRepository,它提供了基本的CRUD和finder操作。我们需要相应的Spring配置将两者粘合在一起,如以下示例所示:
例3。用于Redis存储库的JavaConfig
@Configuration
@EnableRedisRepositories
public class ApplicationConfig {
@Bean
public RedisConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}
@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
给定前面的设置,我们可以将PersonRepository注入到我们的组件中,如以下示例所示:
例4: 访问Person实体
@Autowired PersonRepository repo;
public void basicCrudOperations() {
Person rand = new Person("rand", "al'thor");
rand.setAddress(new Address("emond's field", "andor"));
repo.save(rand); --------1
repo.findOne(rand.getId()); --------2
repo.count(); --------3
repo.delete(rand); --------4
}
1. 如果当前值为null或重复使用已设置的id值,则生成一个新的id,并将Person类型的属性存储在Redis Hash中,该属性具有keyspace:id模式(pattern)的key — 在这种情况下,可能是people:5d67b7e1-8640-2024-beeb-c666fab4c0e5。
2. 使用提供的id检索存储在keyspace:id中的对象。
3. 统计keyspace中可用的实体总数,people,由@RedisHash对Person定义。
4. 从Redis中删除给定对象的key。
4.1 持久化引用
使用@Reference标记属性可以存储一个简单的键引用,而不是将值复制到哈希本身中。从Redis加载时,引用会自动解析并映射回对象,如下例所示:
例5: Property 引用
_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
mother = people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56
Reference存储被引用对象的整个键(keyspace:id)。
保存引用对象时,被引用的对象不会持久化。由于只存储引用,因此必须单独保存对被引用对象的更改。被引用类型的属性上设置的索引未解析。
4.2 持久化部分更新
在某些情况下,你不需要加载和重写整个实体,只需在其中设置一个新值。上一个活动时间的会话时间戳可能是你想要更改一个属性的情况。PartialUpdate允许你定义对现有对象的set和delete操作,同时负责更新实体本身和索引结构的潜在过期时间。以下示例展示了部分更新:
例6:部分更新示例
PartialUpdate<Person> update = new PartialUpdate<Person>("e2c7dcee", Person.class)
.set("firstname", "mat") --------1
.set("address.city", "emond's field") --------2
.del("age"); --------3
template.update(update);
update = new PartialUpdate<Person>("e2c7dcee", Person.class)
.set("address", new Address("caemlyn", "andor")) --------4
.set("attributes", singletonMap("eye-color", "grey")); --------5
template.update(update);
update = new PartialUpdate<Person>("e2c7dcee", Person.class)
.refreshTtl(true); --------6
.set("expiration", 1000);
template.update(update);
1. 将firstname属性设置为mat。
2. 将简单的“address.city”属性设置为“emond’s field”,而不必传入整个对象。注册了自定义转换时,此操作不起作用。
3. 删除age属性。
4. 设置复杂address属性。
5. 设置一个值map,该map将删除先前存在的映射,并用给定的值替换这些值。
6. 更改存活时间(章节九Time To Live)时自动更新服务器过期时间。
更新复杂对象以及map(或其他集合)结构需要与Redis进一步交互以确定现有值,这意味着重写整个实体可能会更快。
五、对象映射基础Object Mapping Fundamentals
六、对象到哈希映射Object-to-Hash Mapping
Redis存储库支持持久化对象到哈希。这需要一个由RedisConverter完成的对象到哈希的转换。默认实现使用Converter将属性值映射到Redis native byte[]。
给定前面部分中的Person类型,默认映射如下所示:
_class = org.example.Person --------1
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand --------2
lastname = al’thor
address.city = emond's field --------3
address.country = andor
1. _class属性包含在根级别以及任何嵌套接口或抽象类型上。
2. 简单属性值按路径映射。
3. 复杂类型的属性通过其点路径(dot path)进行映射。
6.1 数据映射和类型转换
本节解释如何将各种类型在Hash和对象之间来回映射:
表1:默认映射规则
Type | Sample | Mapped Value |
---|---|---|
Simple Type(例如, String) | String firstname = “rand”; | firstname = “rand” |
Byte array (byte[]) | byte[] image = “rand”.getBytes(); | image = “rand” |
Complex Type(例如, Address) | Address address = new Address(“emond’s field”); | address.city = “emond’s field” |
List of Simple Type | List nicknames = asList(“dragon reborn”, “lews therin”); | nicknames.[0] = “dragon reborn”,nicknames.[1] = “lews therin” |
Map of Simple Type | Map<String, String> atts = asMap({“eye-color”, “grey”}, {"… | atts.[eye-color] = “grey”, atts.[hair-color] = "… |
List of Complex Type | List addresses = asList(new Address("em… | addresses.[0].city = “emond’s field”, addresses.[1].city = "… |
Map of Complex Type | Map<String, Address> addresses = asMap({“home”, new Address("em… | addresses.[home].city = “emond’s field”, addresses.[work].city = "… |
由于展平的(flat)表示结构,Map keys需要是简单的类型,例如String或Number。
可以通过在RedisCustomConversions中注册相应的Converter来自定义映射行为。这些转换器可以处理从单个byte[]到单个byte[]的转换,以及Map<String, byte[]>。第一个适用于将复杂类型转换为仍然使用默认映射哈希结构的二进制JSON表示。第二个选项提供对生成的哈希的完全控制。
将对象写入Redis哈希会删除哈希中的内容并重新创建整个哈希,因此未映射的数据会丢失。
以下示例展示了两个示例字节数组转换器:
例1: byte[] 转换器
@WritingConverter
public class AddressToBytesConverter implements Converter<Address, byte[]> {
private final Jackson2JsonRedisSerializer<Address> serializer;
public AddressToBytesConverter() {
serializer = new Jackson2JsonRedisSerializer<Address>(Address.class);
serializer.setObjectMapper(new ObjectMapper());
}
@Override
public byte[] convert(Address value) {
return serializer.serialize(value);
}
}
@ReadingConverter
public class BytesToAddressConverter implements Converter<byte[], Address> {
private final Jackson2JsonRedisSerializer<Address> serializer;
public BytesToAddressConverter() {
serializer = new Jackson2JsonRedisSerializer<Address>(Address.class);
serializer.setObjectMapper(new ObjectMapper());
}
@Override
public Address convert(byte[] value) {
return serializer.deserialize(value);
}
}
使用前面的字节数组Converter产生的输出类似如下:
_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
address = { city : "emond's field", country : "andor" }
以下示例显示了Map转换器的两个示例:
例2:Map<String, byte[]> Converters
@WritingConverter
public class AddressToMapConverter implements Converter<Address, Map<String, byte[]>> {
@Override
public Map<String, byte[]> convert(Address source) {
return singletonMap("ciudad", source.getCity().getBytes());
}
}
@ReadingConverter
public class MapToAddressConverter implements Converter<Map<String, byte[]>, Address> {
@Override
public Address convert(Map<String, byte[]> source) {
return new Address(new String(source.get("ciudad")));
}
}
使用前面的Map Converter产生的输出类似如下:
_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
ciudad = "emond's field"
自定义转换对索引解析没有影响。二级索引仍然被创建,即使对于自定义转换类型也是如此。
6.2 自定义类型映射
如果你希望避免将整个Java类名作为类型信息写入,并且希望使用键,则可以在要持久化的实体类上使用@TypeAlias注释。如果你需要更多地自定义映射,请查看TypeInformationMapper接口。该接口的实例可以在DefaultRedisTypeMapper上配置,该映射器可以在MappingRedisConverter上配置。
以下示例展示如何为实体定义类型别名:
例3:为实体定义 @TypeAlias
@TypeAlias("pers")
class Person {
}
生成的document包含pers作为_class字段中的值。
6.2.1 配置自定义类型映射
下面的例子演示了如何在MappingRedisConverter中配置自定义RedisTypeMapper:
例4:通过Spring Java Config配置自定义RedisTypeMapper
class CustomRedisTypeMapper extends DefaultRedisTypeMapper {
//implement custom type mapping here
}
@Configuration
class SampleRedisConfiguration {
@Bean
public MappingRedisConverter redisConverter(RedisMappingContext mappingContext,
RedisCustomConversions customConversions, ReferenceResolver referenceResolver) {
MappingRedisConverter mappingRedisConverter = new MappingRedisConverter(mappingContext, null, referenceResolver,
customTypeMapper());
mappingRedisConverter.setCustomConversions(customConversions);
return mappingRedisConverter;
}
@Bean
public RedisTypeMapper customTypeMapper() {
return new CustomRedisTypeMapper();
}
}
七、Keyspaces
Keyspaces定义用于为Redis哈希创建实际key的前缀。默认情况下,前缀设置为getClass().getName()。你可以通过在聚合根级别上设置@RedisHash或通过设置编程配置来更改此默认值。但是,带注解的keyspace将取代任何其他配置。
以下示例显示如何使用@EnableRedisRepositories注解设置keyspace配置:
例1:通过@EnableRedisRepositories设置keyspace
@Configuration
@EnableRedisRepositories(keyspaceConfiguration = MyKeyspaceConfiguration.class)
public class ApplicationConfig {
//... RedisConnectionFactory and RedisTemplate Bean definitions omitted
public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
@Override
protected Iterable<KeyspaceSettings> initialConfiguration() {
return Collections.singleton(new KeyspaceSettings(Person.class, "people"));
}
}
}
以下示例展示如何以编程方式设置keyspace:
例2:编程Keyspace设置
@Configuration
@EnableRedisRepositories
public class ApplicationConfig {
//... RedisConnectionFactory and RedisTemplate Bean definitions omitted
@Bean
public RedisMappingContext keyValueMappingContext() {
return new RedisMappingContext(
new MappingConfiguration(new IndexConfiguration(), new MyKeyspaceConfiguration()));
}
public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
@Override
protected Iterable<KeyspaceSettings> initialConfiguration() {
return Collections.singleton(new KeyspaceSettings(Person.class, "people"));
}
}
}
八、二级索引Secondary Indexes
二级索引用于启用基于native Redis结构的查找操作。值在每次保存时被写入相应的索引,并在对象被删除或过期时被删除。
8.1 简单属性索引
给定前面例子中的Person实体示例,我们可以通过用@Indexed注解属性来为firstname创建索引,如下面的示例所示:
例1:注解驱动索引
@RedisHash("people")
public class Person {
@Id String id;
@Indexed String firstname;
String lastname;
Address address;
}
索引是为实际的属性值建立的。保存两个Persons(例如,“rand”和“aviendha”)将建立如下类似的索引:
SADD people:firstname:rand e2c7dcee-b8cd-4424-883e-736ce564363e
SADD people:firstname:aviendha a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56
也可以在嵌套元素上建立索引。假设Address有一个带@Indexed注解的city属性。在这种情况下,一旦person.address.city不为空,我们就有了每个城市的set,如下面的例子所示:
SADD people:address.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e
此外,编程设置允许你在map键和list属性上定义索引,如下面的示例所示:
@RedisHash("people")
public class Person {
// ... other properties omitted
Map<String, String> attributes; --------1
Map<String, Person> relatives; --------2
List<Address> addresses; --------3
}
1. SADD people:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e
2. SADD people:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e
3. SADD people:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e
不能在引用(章节4.1 持久化引用)上解析索引。
与keyspaces一样,你可以配置索引,而不需要注解实际的域类型,如下例所示:
例2:索引设置与@EnableRedisRepositories
@Configuration
@EnableRedisRepositories(indexConfiguration = MyIndexConfiguration.class)
public class ApplicationConfig {
//... RedisConnectionFactory and RedisTemplate Bean definitions omitted
public static class MyIndexConfiguration extends IndexConfiguration {
@Override
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.singleton(new SimpleIndexDefinition("people", "firstname"));
}
}
}
同样,与keyspaces一样,你可以以编程方式配置索引,如以下示例所示:
例3:编程索引设置
@Configuration
@EnableRedisRepositories
public class ApplicationConfig {
//... RedisConnectionFactory and RedisTemplate Bean definitions omitted
@Bean
public RedisMappingContext keyValueMappingContext() {
return new RedisMappingContext(
new MappingConfiguration(
new KeyspaceConfiguration(), new MyIndexConfiguration()));
}
public static class MyIndexConfiguration extends IndexConfiguration {
@Override
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.singleton(new SimpleIndexDefinition("people", "firstname"));
}
}
}
8.2 地理空间索引
假设Address 类型包含Point 类型的location 属性,该属性保存特定地址的地理坐标。通过用@GeoIndexed注解属性,Spring Data Redis通过使用Redis GEO命令添加这些值,如下例所示:
@RedisHash("people")
public class Person {
Address address;
// ... other properties omitted
}
public class Address {
@GeoIndexed Point location;
// ... other properties omitted
}
public interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByAddressLocationNear(Point point, Distance distance); --------1
List<Person> findByAddressLocationWithin(Circle circle); --------2
}
Person rand = new Person("rand", "al'thor");
rand.setAddress(new Address(new Point(13.361389D, 38.115556D)));
repository.save(rand); --------3
repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200, Metrics.KILOMETERS)); --------4
1. 在嵌套属性上的查询方法声明,使用“Point”和“Distance”。
2. 在嵌套属性上的查询方法声明,使用Circle在其中进行搜索。
3. GEOADD people:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e
4. GEORADIUS people:address:location 15.0 37.0 200.0 km
在前面的示例中,经度和纬度值是通过使用对象id作为成员名的GEOADD来存储的。查找方法允许使用Circle或Point, Distance组合来查询这些值。
不能将“near”和“within”与其他条件结合起来做查询。
九、存活时间Time To Live
存储在Redis中的对象可能仅在一定时间内有效。这对于在Redis中持久化short-lived对象特别有用,而不必在它们到达生命尽头(end of life)时手动删除它们。以秒为单位的过期时间可以通过@RedisHash(timeToLive=…)以及使用KeyspaceSettings(参见章节七、Keyspaces)来设置。
通过在数字属性或方法上使用@TimeToLive注解,可以设置更灵活的过期时间。但是,不要将@TimeToLive同时应用于同一类中的方法和属性。以下示例展示了属性和方法上的@TimeToLive注解:
例1:到期时间
public class TimeToLiveOnProperty {
@Id
private String id;
@TimeToLive
private Long expiration;
}
public class TimeToLiveOnMethod {
@Id
private String id;
@TimeToLive
public long getTimeToLive() {
return new Random().nextLong();
}
}
用@TimeToLive显式注解属性会从Redis中读取实际的TTL或PTTL值。-1表示该对象没有关联的过期。
repository的实现确保通过RedisMessageListenerContainer订阅 Redis keyspace通知。
当过期设置为正值时,将运行相应的EXPIRE命令。除了保留原始副本外,Redis中还保留了一个幻影(phantom)副本,并将其设置为在原始副本后五分钟过期。这样做是为了使存储库支持发布RedisKeyExpiredEvent,每当key过期时,即使原始值已经被删除,也会在Spring的ApplicationEventPublisher 中保存过期值。所有使用Spring Data Redis存储库连接的应用程序都会收到过期事件。
默认情况下,在初始化应用程序时会禁用key过期侦听器。可以在@EnableRedisRepositories或RedisKeyValueAdapter中调整启动模式,以启动带有应用程序的监听器,或在首次插入具有TTL的entity时启动监听器。有关可能的值,请参阅EnableKeyspaceEvents。
RedisKeyExpiredEvent保存过期域对象的副本以及key。
延迟或禁用过期事件监听器启动会影响RedisKeyExpiredEvent发布。禁用的事件侦听器不发布过期事件。由于监听器初始化延迟,延迟启动可能会导致事件丢失。
如果Redis中的notify-keyspace-events尚未设置,则keyspace通知消息监听器会更改这些设置。现有设置不会被覆盖,因此必须正确设置这些设置(或将其留空)。
Redis Pub/Sub消息不持久。如果某个key在应用程序关闭时过期,则不会处理过期事件,这可能会导致二级索引包含对过期对象的引用。
@EnableKeyspaceEvents(shadowCopy = OFF)禁用幻影(phantom)副本的存储,并减少Redis中的数据大小。RedisKeyExpiredEvent将只包含过期key的id。
十、Redis特定的查询方法
查询方法允许从方法名称自动派生简单的查找器查询,如以下示例所示:
例1:Repository查找器方法
public interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByFirstname(String firstname);
}
请确保finder方法中使用的属性已设置为索引。
Redis存储库的查询方法仅支持对具有分页功能的实体和实体集合的查询。
使用派生查询方法可能并不总是足以对要运行的查询进行建模(model)。RedisCallback提供了对索引结构甚至自定义索引的实际匹配的更多控制。要做到这一点,请提供一个RedisCallback,它返回单个或Iterable的id值集,如以下示例所示:
例2:使用RedisCallback的示例查找器
String user = //...
List<RedisSession> sessionsByUser = template.find(new RedisCallback<Set<byte[]>>() {
public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
return connection
.sMembers("sessions:securityContext.authentication.principal.username:" + user);
}}, RedisSession.class);
下表概述了Redis支持的关键字,以及包含该关键字的方法的基本含义:
表1:方法名称中支持的关键字
Keyword | 示例 | Redis snippet |
---|---|---|
And | findByLastnameAndFirstname | SINTER …:firstname:rand …:lastname:al’thor |
Or | findByLastnameOrFirstname | SUNION …:firstname:rand …:lastname:al’thor |
Is, Equals | findByFirstname, findByFirstnameIs, findByFirstnameEquals | SINTER …:firstname:rand |
IsTrue | FindByAliveIsTrue | SINTER …:alive:1 |
IsFalse | findByAliveIsFalse | SINTER …:alive:0 |
Top,First | findFirst10ByFirstname,findTop5ByFirstname |
10.1 排序查询方法结果
Redis存储库允许使用各种方法来定义排序顺序。Redis本身在检索hashes或sets时不支持即时排序。因此,Redis存储库查询方法构造一个Comparator,在将结果返回为List之前将其应用于结果。让我们来看看下面的例子:
例3:对查询结果进行排序
interface PersonRepository extends RedisRepository<Person, String> {
List<Person> findByFirstnameOrderByAgeDesc(String firstname); --------1
List<Person> findByFirstname(String firstname, Sort sort); --------2
}
1. 从方法名派生的静态排序。
2. 使用方法参数进行动态排序。
十一、Query by Example
本章主要内容见这里。
Redis存储库及其辅助索引(Secondary indexes)支持Spring Data的Query by Example功能的子集。特别是,只有精确的、区分大小写的和非null的值才用于构造查询。
辅助索引使用基于set的操作(设置交集、设置并集)来确定匹配的关键字。将未编入索引的属性添加到查询中不会返回任何结果,因为不存在索引。Query by Example支持检查索引配置,以便在查询中仅包括索引所覆盖的属性。这是为了防止意外包含未编入索引的属性。
不区分大小写的查询和不受支持的StringMatcher实例在运行时被拒绝。
以下列表显示了支持的“Query by Example”选项:
- 区分大小写,精确匹配简单属性和嵌套属性
- 任意/所有匹配模式
- 条件(criteria)值的值转换
- 从条件中排除空值
以下列表显示了“Query by Example”不支持的属性:
- 不区分大小写的匹配
- Regex, prefix/contains/suffix字符串匹配
- 查询Associations, Collection和类Map属性
- 包含条件中的null值
- findAll带排序
十二、在Redis集群上运行Redis Repositories
你可以在Redis集群环境中使用Redis存储库支持。有关ConnectionFactory配置的详细信息,请参阅“Redis Cluster”部分。尽管如此,还是必须进行一些额外的配置,因为默认的key分发将实体和辅助索引分布在整个集群及其槽中。
下表展示了集群上数据的详细信息(基于前面的示例):
Key | Type | Slot | Node |
---|---|---|---|
people:e2c7dcee-b8cd-4424-883e-736ce564363e | id for hash | 15171 | 127.0.0.1:7381 |
people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 | id for hash | 7373 | 127.0.0.1:7380 |
people:firstname:rand | index | 1700 | 127.0.0.1:7379 |
某些命令(如SINTER和SUNION)只能在所有涉及的keys映射到同一槽时在服务器端进行处理。否则,必须在客户端进行计算。因此,将keyspaces固定到单个槽非常有用,这样就可以立即利用Redis服务器端的计算。下表展示了执行此操作时发生的情况(请注意槽列中的更改和节点列中的端口值):
Key | Type | Slot | Node |
---|---|---|---|
{people}:e2c7dcee-b8cd-4424-883e-736ce564363e | id for hash | 2399 | 127.0.0.1:7379 |
{people}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 | id for hash | 2399 | 127.0.0.1:7379 |
{people}:firstname:rand | index | 2399 | 127.0.0.1:7379 |
当你使用Redis集群时,通过使用@RedisHash(“{yourkeyspace}”)定义keyspaces并将其固定到特定的槽。
十三、Redis Repositories 剖析
Redis作为一个存储本身提供了一个narrow low-level API,将更高级的功能,如二级索引和查询操作,留给用户实现。
本节提供了存储库发出的命令的更详细视角,以便更好地理解潜在的性能影响。
将以下实体类视为所有操作的起点:
例1:示例实体
@RedisHash("people")
public class Person {
@Id String id;
@Indexed String firstname;
String lastname;
Address hometown;
}
public class Address {
@GeoIndexed Point location;
}
13.1 插入操作
repository.save(new Person("rand", "al'thor"));
HMSET "people:19315449-cda2-4f5c-b696-9cb8018fa1f9" "_class" "Person" "id" "19315449-cda2-4f5c-b696-9cb8018fa1f9" "firstname" "rand" "lastname" "al'thor" --------1
SADD "people" "19315449-cda2-4f5c-b696-9cb8018fa1f9" --------2
SADD "people:firstname:rand" "19315449-cda2-4f5c-b696-9cb8018fa1f9" --------3
SADD "people:19315449-cda2-4f5c-b696-9cb8018fa1f9:idx" "people:firstname:rand" --------4
1. 将展平(flattened)的条目另存为哈希。
2. 将<1>中写入的哈希的key添加到同一keyspace中实体的辅助索引中。
3. 将<2>中写入的哈希的key添加到具有属性值的firstnames的二级索引中。
4. 将<3>的索引添加到条目的辅助结构集,以跟踪删除/更新时要清除的索引。
13.2 替换现有的
repository.save(new Person("e82908cf-e7d3-47c2-9eec-b4e0967ad0c9", "Dragon Reborn", "al'thor"));
DEL "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" --------1
HMSET "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "_class" "Person" "id" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "firstname" "Dragon Reborn" "lastname" "al'thor" --------2
SADD "people" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" --------3
SMEMBERS "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" --------4
TYPE "people:firstname:rand" --------5
SREM "people:firstname:rand" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" --------6
DEL "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" --------7
SADD "people:firstname:Dragon Reborn" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" --------8
SADD "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" "people:firstname:Dragon Reborn" --------9
1. 删除现有的hash,以避免hash keys的剩余部分可能不再存在。
2. 将展平(flattened)的条目另存为哈希。
3. 将<1>中写入的哈希的key添加到同一keyspace中实体的辅助索引中。
4. 获取可能需要更新的现有索引结构。
5. 检查索引是否存在以及它的类型(文本、地理位置…)。
6. 从索引中删除可能存在的键。
7. 删除包含索引信息的辅助数据。
8. 将<2>中添加的哈希的key添加到具有属性值的名字的二级索引中。
9. 将<6>的索引添加到条目的辅助结构集,以跟踪删除/更新时要清除的索引。
13.3 保存地理数据
地理索引遵循与基于普通文本的索引相同的规则,但使用地理结构来存储值。保存使用地理索引特性的实体会产生以下命令:
GEOADD "people:hometown:location" "13.361389" "38.115556" "76900e94-b057-44bc-abcf-8126d51a621b" --------1
SADD "people:76900e94-b057-44bc-abcf-8126d51a621b:idx" "people:hometown:location" --------2
1. 将保存的条目的键添加到地理索引中。
2. 跟踪索引结构。
13.4 使用简单索引查找
repository.findByFirstname("egwene");
SINTER "people:firstname:egwene" --------1
HGETALL "people:d70091b5-0b9a-4c0a-9551-519e61bc9ef3" --------2
HGETALL ...
1. 获取二级索引中包含的键。
2. 单独获取<1>返回的每个键。
13.5 使用地理索引查找
repository.findByHometownLocationNear(new Point(15, 37), new Distance(200, KILOMETERS));
GEORADIUS "people:hometown:location" "15.0" "37.0" "200.0" "km" --------1
HGETALL "people:76900e94-b057-44bc-abcf-8126d51a621b" --------2
HGETALL ...
1. 获取二级索引中包含的键。
2. 单独获取<1>返回的每个键。
十四、投影Projections
十五、自定义存储库实现
自定义存储库实现见这里。
十六、从聚合根(Aggregate Roots)发布事件
十七、存储库方法的null处理
十八、CDI 集成
存储库接口的实例通常由容器创建,在使用Spring Data时,Spring是最自然的选择。Spring为创建bean实例提供了复杂的功能。Spring Data Redis附带了一个自定义的CDI扩展,允许您在CDI环境中使用存储库抽象。该扩展是JAR的一部分,因此,要激活它,请将Spring Data Redis JAR放入类路径中。
然后,你可以通过为RedisConnectionFactory和RedisOperations实现CDI Producer来设置基础结构,如以下示例所示:
class RedisOperationsProducer {
@Produces
RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration());
connectionFactory.afterPropertiesSet();
connectionFactory.start();
return connectionFactory;
}
void disposeRedisConnectionFactory(@Disposes RedisConnectionFactory redisConnectionFactory) throws Exception {
if (redisConnectionFactory instanceof DisposableBean) {
((DisposableBean) redisConnectionFactory).destroy();
}
}
@Produces
@ApplicationScoped
RedisOperations<byte[], byte[]> redisOperationsProducer(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
template.setConnectionFactory(redisConnectionFactory);
template.afterPropertiesSet();
return template;
}
}
必要的设置可能会有所不同,具体取决于您的JavaEE环境。
Spring Data Redis CDI扩展将所有可用的存储库作为CDI bean,并在容器请求存储库类型的bean时为Spring Data存储库创建一个代理。因此,获取Spring Data存储库的实例需要声明@Injected属性,如以下示例所示:
class RepositoryClient {
@Inject
PersonRepository repository;
public void businessMethod() {
List<Person> people = repository.findAll();
}
}
Redis存储库需要RedisKeyValueAdapter和RedisKeyValueTemplate实例。如果找不到提供的bean,这些bean将由Spring Data CDI extension创建和管理。但是,你可以提供自己的bean来配置RedisKeyValueAdapter和RedisKeyValueTemplate的特定属性。
十九、存储库查询关键字
见存储库查询关键字。