Spring实战(十二)-使用NoSQL数据库

本文基于《Spring实战(第4版)》所写。

使用MongoDB持久化文档数据

将数据收集到一个非规范化(也就是文档)的结构中,保持了独立的实体,能够按照这种方式优化并处理文档的数据库,我们称之为文档数据库。

有些数据具有明显的关联关系,文档型数据库并没有针对存储这样的数据进行优化。

Spittr应用的域对象并不适合文档数据库。在本章中,我们将会在一个购物订单系统中学习MongoDB。

MongoDB是最为流行的开源文档数据库之一。Spring Data MongoDB提供了三种方式在Spring应用中使用MongoDB:

  • 通过注解实现对象-文档映射;
  • 使用MongoTemplate实现基于模板的数据库访问;
  • 自动化的运行时Repository生成功能。

启用MongoDB

在使用MongoDB之前,我们首先要配置Spring Data MongoBD ,在Spring配置中添加几个必要的bean。

  • 配置MongoClient,以便于访问MongoDB数据库;
  • 配置MongoTemplate bean,实现基于模板的数据库访问;
  • 启用Spring Data MongoDB的自动化Repository生成功能(不是必须,但强烈推荐)。

如下的程序清单展现了如何编写简单的Spring Data MongoDB配置类,它包含了上述的几个bean:

package orders.config;


import com.mongodb.Mongo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoClientFactoryBean;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@Configuration
@EnableMongoRepositories(basePackages = "orders.db")  // 启动MongoDB的Repository功能
public class MongoConfig {
    @Bean
    public MongoClientFactoryBean mongo(){    // MongoClient Bean (MongoFactoryBean已废弃)
        MongoClientFactoryBean mongo = new MongoClientFactoryBean();
        mongo.setHost("localhost");
        return mongo;
    }
    @Bean
    public MongoOperations mongoTemplate(Mongo mongo){  // MongoTemplate bean
        return new MongoTemplate(mongo, "OrdersDB");
    }
}

@EnableMongoRepositories启动了MongoDB的自动化Repository生成功能。

以上程序中还包含了两个带有@Bean注解的方法。第一个@Bean方法使用MongoClientFactoryBean声明了一个Mongo实例。这个bean将Spring Data MongoDB与数据库本身连接了起来。尽管我们可以使用MongoClient直接创建Mongo实例,但如果这样做的话,就必须要处理MongoClient构造器所抛出的UnknownHostException异常。在这里,使用Spring Data MongoDB的MongoClientFactoryBean更加简单。因为它是一个工厂bean,因此MongoClientFactoryBean会负责构建Mongo实例,我们不必再担心UnknownHostException异常。

另外一个@Bean方法声明了MongoTemplate bean,在它构造时,使用了其他@Bean方法所创建的Mongo实例的引用以及数据库的名称。Repository的自动化生成功能在底层使用了它。

除了直接声明这些bean,我们还可以让配置类扩展AbstractMongoConfiguration并重载getDatabaseName()和mongo()方法。如下的程序展现了如何使用这种配置方式。

package orders.config;

import com.mongodb.Mongo;
import com.mongodb.MongoClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@Configuration
@EnableMongoRepositories("orders.db")
public class MongoConfig2  extends AbstractMongoConfiguration{
    protected String getDatabaseName() {  // 指定数据库名称
        return "OrdersDB";
    }
    public Mongo mongo() throws Exception {
        return new MongoClient();             // 创建Mongo客户端
    }
}

这个新的配置类与上一个的配置类功能是相同的。最为显著的区别在于这个配置中没有直接声明MongoTemplate bean,当然它还是会被隐式低创建。我们在这里重载了getDatabaseName() 方法来提供数据库的名称。mongo()方法依然会创建一个MongoClient的实例,因为它会抛出Exception,所以我们可以直接使用MongoClient,而不必再使用MongoClientFactoryBean了。

如果MongoDB服务器运行在其他的机器上,那么可以在创建MongoClient的时候进行指定:

public Mongo mongo() throws Exception {
    return new MongoClient("mongodbserver");
}

也可指定端口(有时并不是默认的27017)

public Mongo mongo() throws Exception {
    return new MongoClient("mongodbserver", 37017);
}

还可启用认证功能:为了访问数据库,需要提供应用的凭证

package orders.config;

import com.mongodb.Mongo;
import com.mongodb.MongoClient;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import java.util.Arrays;

@Configuration
@EnableMongoRepositories(basePackages = "orders.db")
public class MongoConfig3 extends AbstractMongoConfiguration{
    @Autowired
    private Environment env;

    protected String getDatabaseName() {
        return "OrdersDB";
    }

    public Mongo mongo() throws Exception {

        MongoCredential credential = MongoCredential.createMongoCRCredential(  // 创建 MongoDB 凭证
                env.getProperty("mongo.username"),
                "OrdersDB",
                env.getProperty("mongo.password").toCharArray());

        return new MongoClient(  //  创建 MongoClient
                new ServerAddress("localhost" , 37017),
                Arrays.asList(credential));
    }
}

为了访问需要认证的MongoDB服务器,MongoClient在实例化的时候必须要有一个MongoCredential的列表。

除了Java配置的方案,还可以使用XML进行配置Spring Data MongoDB。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mongo="http://www.springframework.org/schema/data/mongo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <mongo:repositories base-package="orders.db" />
    <mongo:mongo />
    <bean id="mongoTemplate"
          class="org.springframework.data.mongodb.core.MongoTemplate">
        <constructor-arg ref="mongo" />
        <constructor-arg value="OrdersDB" />
    </bean>
</beans>

为模型添加注解,实现MongoDB持久化

MongoDB没有提供对象-文档映射的注解。Spring Data MongoDB填补了这一空白,提供了一些将Java类型映射为MongoDB文档的注解。下表描述了这些注解

注解 描述
@Document 标示映射到MongoDB文档上的领域对象
@Id 标示某个域为ID域
@DbRef 标示某个域要引用其他的文档,这个文档有可能位于另一个数据库中
@Field 为文档域指定自定义的元数据
@Version 标示某个属性用作版本域

@Document和@Id注解类似于JPA的@Entity和@Id注解。对于要以文档形式保存到MongoDB数据库的每个Java类型都会使用这两个注解。例如,如下的程序展现了如何为Order类添加注解,它会被持久化到MongoDB中。

package orders;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import java.util.Collection;
import java.util.LinkedHashSet;

@Document
public class Order {

    @Id
    private String id;   // 指定ID

    @Field("client")
    private String customer;      // 覆盖默认的域名

    private String type;

    private Collection<Item> items = new LinkedHashSet<Item>();


    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getCustomer() {
        return customer;
    }

    public void setCustomer(String customer) {
        this.customer = customer;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public Collection<Item> getItems() {
        return items;
    }

    public void setItems(Collection<Item> items) {
        this.items = items;
    }
}

Order类添加了@Document注解,这样它就能够借助MongoTemplate或自动生成的Repository进行持久化。其id属性上使用了@Id注解,用来指定它作为文档的ID。除此之外, customer属性上使用了@Field注解,这样的话,当文档持久化的时候customer属性将会映射为名为client的域。

注意,其他的属性并没有添加注解。除非将属性设置为瞬时态(transient)的,否则Java对象中所有的域都会持久化为文档中的域。并且如果我们不使用@Field注解进行设置的话,那么文档域中的名字将会与对应的Java属性相同。

同时,需要注意的是items属性,它指的是订单中具体条目的集合。在传统的关系型数据库中,这些条目将会保存在另外的一个数据库表中,通过外键进行应用,items域上很可能还会使用JPA的@OneToMany注解。但在这里,情形完全不同。

文档展现了关联但非规范化的数据。相关的概念(如订单中的条目)被嵌入到顶层的文档数据中。

文档可以与其他的文档产生关联,但这并不是文档数据库擅长的功能。在本例购买订单与行条目之间的关联关系中,行条目只是同一个订单文档里面内嵌的一部分。因此,没有必要为这种关联关系添加任何注解。实际上,Item类本身并没有任何注解:

package orders;

public class Item {

    private Long id;
    private Order order;
    private String product;
    private double price;
    private int quantity;


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Order getOrder() {
        return order;
    }

    public void setOrder(Order order) {
        this.order = order;
    }

    public String getProduct() {
        return product;
    }

    public void setProduct(String product) {
        this.product = product;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

没有必要为Item添加@Document注解,也没有必要为它的域指定@Id。这是因为我们不会单独将Item持久化为文档。它始终会是Order文档中Item列表的一个成员,并且会作为文档中的嵌入元素。

使用MongoTemplate访问MongoDB

配置了MongoTemplate bean后,就可以将其注入到使用它的地方:

@Autowired
MongoOperations mongo;

注意,MongoOperations是MongoTemplate所实现的接口。MongoOperations暴露了多个使用MongoDB文档数据库的方法,比如计算文档集合中有多少条文档。使用注入MongoOperations,我们可以得到Order集合并调用count()来得到数量:

long orderCount = mongo.getCollection("order").count();

现在,假设要保存一个新的Order。为了完成这个任务,我们可以调用save()方法:

Order order = new Order();
... // set properties and add line items
mongo.save(order, "order");

save()方法的第一个参数是新创建的Order,第二个参数是要保存文档存储的名称。

另外,我们还可以调用findById()方法来根据ID查找订单:

String orderId = ...;
Order order = mongo.findById(orderId, Order.class);

对于更高级的查询,我们需要构造Query对象并将其传递给find()方法。例如,要查找所有client域等于“Chuck Wagon”的订单,可以使用如下的代码:

List<Order> chucksOrders = mongo.find(Query.query(
        Criteria.where("client").is("Chuck Wagon")), Order.class);

再比如,我们想查询Chuck所有通过Web创建的订单:

List<Order> chucksWebOrders = mongo.find(Query.query(
        Criteria.where("customer").is("Chuck Wagon")
        .and("type").is("WEB")), Order.class);

如果想移除某一个文档的话,那么就应该使用remove() 方法:

mongo.remove(order);

编写MongoDB Repository

如果不愿意编写Repository的(通常,我们将MongoOperations注入到自己设计的Repository类中,并使用它),那么Spring Data MongoDB能够自动在运行时生成Repository实现。

我们已经通过@EnableMongoRepositories注解启用了Spring Data MongoDB的Repository功能,接下来需要做的就是创建一个接口,Repository实现要基于这个接口来生成,这个接口要扩展MongoRepository。如下的程序中OrderRepository扩展了MongoRepository,为Order文档提供了基本的CRUD操作。

package orders.db;

import orders.Order;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface OrderRepository extends MongoRepository<Order, String> {}

因为OrderRepository扩展了MongoRepository,因此它就会传递性的扩展Repository标记接口。任何扩展Repository的接口将会在运行时自动生成实现。

MongoRepository接口有两个参数,第一个是带有@Document注解的对象类型,也就是该Repository要处理的类型。第二个参数是带有@Id注解的属性类型。

尽管OrderRepository本身并没有定义任何方法,但是它会继承多个方法,包括对Order文档进行CRUD操作的方法。下表描述了OrderRepository继承的所有方法。

方法 描述
long count(); 返回指定Repository类型的文档数量
void delete(Iterable<? extends T>); 删除与指定对象关联的所有文档
void delete(T); 删除与指定对象关联的文档
void delete(ID); 根据ID删除某一个文档
void deleteAll(); 删除指定Repository类型的所有文档
boolean exists(Object); 如果存在与指定对象相关联的文档,则返回true
boolean exists(ID); 如果存在指定ID的文档,则返回true
List<T> findAll(); 返回指定Repository类型的所有文档
List<T> findAll(Iterable<ID>); 返回指定文档ID对应的所有文档
Page<?> findAll(Pageable pageable); 为指定的Repository类型,返回分页且排序的文档列表
List<T> findAll(Sort); 为指定的Repository类型,返回排序后的文档列表
T findOne(ID); 为指定的ID返回单个文档
save(Iterable<s>); 保存指定Iterable中所有文档
save(<s>); 为给定的对象保存一条文档

OrderRepository扩展了MongoRepository<Order, String>,那么T就映射为Order,ID映射为String,而s映射为所有扩展Order的类型。

添加自定义的查询方法

与Spring Data JPA支持方法命名约定类似,都能够帮助Spring Data为遵循约定的方法自动生成实现。这意味这我们可以为OrderRepository添加自定义的方法:

public interface OrderRepository extends MongoRepository<Order, String> {
    List<Order> findByCustomer(String c);
    List<Order> findByCustomerLike(String c);
    List<Order> findByCustomerAndType(String c, String t);
    List<Order> findByCustomerLikeAndType(String c, String t);
}

其中,find这个查询动词并不是固定的,如果喜欢的话,我们还可以使用get或read作为查询动词;

除此之外,还有一个特殊的动词用来为匹配的对象计数:

int countByCustomer(String c);

与Spring Data JPA类似,在查询动词与By之前,我们有很大的灵活性。比如,我们可以标示Order或者一些其他的词语,都不会影响获取的内容。

如果只想要一个Order对象的话,我们可以只需要简单的返回Order:

Order findASingleOrderByCustomer(String c);

这里,所返回的就是原本List中的第一个Order对象。如果没有匹配元素的话,方法将会返回null。

指定查询

@Query能够想在JPA中那样在MongoDB上为Repository方法指定自定义的查询。唯一的区别在于针对MongoDB时,@Query会接受一个JSON查询,而不是JPA查询。

例如,假设我们想要查询给定类型的订单,并要求customer的名称为“Chuck Wagon”。OrderRepository中如下的方法声明能够完成所需的任务:

@Query("{'customer' : 'Chuck Wagon', 'type' : ?0 }")
List<Order> findChucksOrders(String t);

@Query中给定的JSON将会与所有的Order文档进行匹配,并返回匹配的文档。需要注意的是,type属性映射成了“?0”,这表明type属性应该与查询方法的第零个参数相等。如果有多个参数的话,他们可以通过“?1”、“?2”等方式进行引用。

混合自定义的功能

对于JPA来说,混合自定义的功能涉及到创建一个中间接口来声明自定义的方法,为这些自定义方法创建实现类并修改自动化的Repository接口,使其扩展中间接口。对于Spring Data MongoDB来说,这些步骤都是相同的。

假设我们想要查询文档中type属性匹配给定值的Order对象。而且,如果给定的类型是“NET”,那我们就查找type值为“WEB”的Order对象。

首先,定义中间接口:

package orders.db;
import orders.Order;
import java.util.List;

public interface OrderOperations {
    List<Order> findOrdersByType(String t);
}

接下来,我们要编写混合实现

package orders.db;

import orders.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;

import java.util.List;

public class OrderRepositoryImpl implements OrderOperations {
    
    @Autowired
    private MongoOperations mongo; // 注入MongoOperations
    
    public List<Order> findOrdersByType(String t) {
        String type = t.equals("NET") ? "WEB" : t;
        Criteria where = Criteria.where("type").is(type);  // 创建查询
        Query query = Query.query(where);
        return mongo.find(query,Order.class);  // 执行查询
    }
}

剩下的工作就是修改OrderRepository,让其扩展中间接口OrderOperations:

public interface OrderRepository extends MongoRepository<Order, String>,OrderOperations {
     ...
}

将这些关联起来的关键点在于实现类的名称为OrderRepositoryImpl。这个名字前半部分与OrderRepository相同,只是添加了“Impl”后缀。当Spring Data MongoDB生成Repository实现时,它会查找这个类并将其混合到自动生成的实现中。

如果不喜欢“Impl”后缀的话,可以配置。

@Configuration
@EnableMongoRepositories(value = "orders.db", repositoryImplementationPostfix = "Stuff")
public class MongoConfig extends AbstractMongoConfiguration{
    ...
}

如果使用XML配置的话,我们可以设置<mongo:repositories>的repository-impl-postfix属性:

<mongo:repositories base-package="orders.db"
                  repository-impl-postfix="Stuff" />

不管采用哪种方式,都让Spring Data MongoDB 查找名为OrderRepositoryStuff的类,而不再查找OrderRepositoryImpl。

使用Redis操作key-value数据

Redis是一种key-value存储的数据库,也可以说是持久化的哈希Map。

Spring Data的关键特性,也就是面向模板的数据访问,能够在使用Redis的时候,为我们提供帮助。

Spring Data Redis包含了多个模板实现,用来完成Redis数据库的数据存取功能。为了创建Spring Data Redis的模板,我们首先需要有一个Redis连接工厂。Spring Data Redis提供了四个连接工厂供我们选择。

连接到Redis

Redis连接工厂会生成到Redis数据库服务器的连接。Spring Data Redis为四种Redis客户端实现提供了连接工厂:

  • JedisConnectionFactory
  • JredisConnectionFactory
  • LettuceConnectionFactory
  • SrpConnectionFactory

从Spring Data Redis的角度来看,这些连接工厂在适用性上都是相同的。

例如,如下展示了如何配置JedisConnectionFactory bean:

@Bean
public RedisConnectionFactory redisCF() {
    return new JedisConnectionFactory();
}

通过默认构造器创建的连接工厂会向localhost上的6379端口创建连接,并且没有密码。如果你的Redis服务器运行在其他的主机或端口上,在创建连接工厂的时候,可以设置这些属性:

@Bean
public RedisConnectionFactory redisCF(){
    JedisConnectionFactory cf = new JedisConnectionFactory();
    cf.setHostName("redis-server");
    cf.setPort(7379);
    return cf;
}

类似地,如果你的Redis服务器配置为需要客户端认证的话,那么可以通过调用setPassword()方法来设置密码:

@Bean
public RedisConnectionFactory redisCF(){
    JedisConnectionFactory cf = new JedisConnectionFactory();
    cf.setHostName("redis-server");
    cf.setPort(7379);
    cf.setPassword("foobared");
    return cf;
}

如果使用Spring Data Redis 2.0以上的话,setHostName等方法会被废弃,可以使用以下方式配置

@Bean
public JedisConnectionFactory jedisConnectionFactory() {
    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6379);
    redisStandaloneConfiguration.setPassword(RedisPassword.of("123456"));
    redisStandaloneConfiguration.setDatabase(6);
    return new JedisConnectionFactory(redisStandaloneConfiguration);
}

假设使用LettuceConnectionFactory的话,可以按照如下的方式进行配置:

@Bean
public RedisConnectionFactory redisCF(){
    LettuceConnectionFactory cf = new LettuceConnectionFactory();
    cf.setHostName("redis-server");
    cf.setPort(7379);
    cf.setPassword("foobared");
    return cf;
}

所有的Redis连接工厂都具有setHostName()、setPort()和setPassword()方法。这样,它们在配置方面实际上是相同的。

接下来,就可以使用Spring Data Redis模板了。

顾名思义,Redis连接工厂会生成到Redis key-value存储的连接(以RedisConnection的形式)。借助RedisConnection,可以存储和读取数据。例如,我们可以获取连接并使用它来保存一个问候信息,如下所示:

RedisConnectionFactory cf = ...;
RedisConnection conn = cf.getConnection();
conn.set("greeting".getBytes, "Hello World".getBytes);

与之类似,我们还可以使用RedisConnection来获取之前存储的问候信息:

byte[] greetingBytes = conn.get("greeting".getBytes());
String greeting = new String(greetingBytes);

除了这种方式,Spring Data Redis以模板的形式提供了较高等级的数据访问方案。实际上,Spring Data Redis提供了两个模板:

  • RedisTemplate
  • StringRedisTemplate

RedisTemplate可以极大地简化Redis数据访问,能够让我们持久化各种类型的key和value,并不局限于字节数组。在认识到key和value通常是String类型之后,StringRedisTemplate扩展了RedisTemplate,只关注String类型。

假设我们已经有了RedisConnectionFactory,那么可以按照如下的方式构建RedisTemplate:

RedisConnectionFactory cf = ...;
RedisTemplate<String, Product> redis = 
        new RedisTemplate<String, Product>();
redis.setConnectionFactory(cf);

注意,RedisTemplate使用了两个类型进行参数化。第一个是key的类型,第二个是value的类型。在这里所构建的RedisTemplate中,将会保存Product对象作为value,并将其赋予一个String类型的key。

但如果所使用的value和key都是String类型,那么可以考虑使用StringRedisTemplate来代替RedisTemplate:

RedisConnectionFactory cf = ...;
StringRedisTemplate redis = new StringRedisTemplate(cf);

注意,与RedisTemplate不同,StringRedisTemplate有一个接受RedisConnectionFactory的构造器,因此没有必要在构建后调用setConnectionFactory();

如果你经常使用RedisTemplate或StringRedisTemplate的话,可以考虑将其配置为bean,然后注入到需要的地方。如下就是一个声明RedisTemplate的简单@Bean方法:

@Bean
public RedisTemplate<String, Product>
                  redisTemplate(RedisConnectionFactory cf) {
    RedisTemplate<String, Product> redis = 
            new RedisTemplate();
    redis.setConnectionFactory(cf);
    return redis;
}

如下是声明StringRedisTemplate bean的@Bean方法:

@Bean
public StringRedisTemplate
              stringRedisTemplate(RedisConnectionFactory cf) {
    return new StringRedisTemplate(cf);
}

RedisTemplate的大多数操作都是下表中的子API提供的。

方法 子API接口 描述
opsForValue() ValueOperations<K, V> 操作具有简单值的条目
opsForList() ListOperations<K, V> 操作具有list值的条目
opsForSet() SetOperations<K, V> 操作具有set值的条目
opsForZSet() ZSetOperations<K, V> 操作具有ZSet值(排序的set)的条目
opsForHash() HashOperations<K, HK, HV> 操作具有hash值的条目
boundValueOps(K) BoundValueOperations<K, V> 以绑定指定key的方式,操作具有简单值的条目
boundListOps(K) BoundListOperations<K, V> 以绑定指定key的方式,操作具有list值的条目
boundSetOps(K) BoundSetOperations<K, V> 以绑定指定key的方式,操作具有set值的条目
boundZSetOps(K) BoundZSetOperations<K, V> 以绑定指定key的方式,操作具有ZSet(排序的set)值的条目
boundHashOps(K) BoundHashOperations<K, V> 以绑定指定key的方式,操作具有hash值的条目

使用简单的值

假设我们通过RedisTemplate<String, Product>保存Product,其中key是sku属性的值。如下的代码片段展示了如何借助opsForValue()方法完成该功能:

redis.opsForValue().set(product.getSku(), product);

类似地,如果希望获取sku属性为123456的产品,那么可以使用如下的代码片段:

Product product = redis.opsForValue().get("123456");

如果按照给定的key,无法获得条目的话,将会返回null。

使用List类型的值

使用List类型,只需使用opsForList()方法即可。例如,我们可以在一个List类型的条目尾部添加一个值:

redis.opsForList().rightPush("cart", product);

通过这种方式,我们向列表的尾部添加了一个Prodcut,所使用的这个列表在存储时key为cart。如果这个key尚未存在列表的话,将会创建一个。

rightPush()会在列表的尾部添加一个元素,而leftPush() 则会在列表的头部添加一个值:

redis.opsForList().leftPush("cart", product);

我们有很多方式从列表中获取元素,可以通过leftPop()或者rightPop()方法从列表中弹出一个元素:

Product first = redis.opsForList().leftPop("cart");
Product last = redis.opsForList().rightPop("cart");

除了从列表中获取值以外,这两个方法还有一个副作用就是从列表中移除所弹出的元素。如果你只是想获取值的话(甚至可能要在列表中间获取),那么可以使用range()方法:

List<Product> products = redis.opsForList().range("cart", 2, 12);

range()方法不会从列表中移除任何元素。但是它会根据指定的key和索引防伪,获取范围内的一个或多个值。上面的样例中,会获取11个元素,从索引为2的元素到索引为12的元素(不包含)。如果范围超出了列表的边界,那么只会返回索引在范围内的元素。如果该索引范围内没有元素的话,将会返回一个空的列表。

在Set上执行操作

除了操作列表以外,我们还可以使用opsForSet()操作Set。最为常见的是

redis.opsForSet().add("cart", product);

在我们有多个Set并填充值之后,就可以对这些Set进行一些有意思的操作,如获取其差异,求交集和求并集:

List<Product> diff = redis.opsForSet().difference("cart1", "cart2");
List<Product> union = redis.opsForSet().union("cart1", "cart2");
List<Product> isect = redis.opsForSet().isect("cart1","cart2");

当然,我们还可以移除它的元素:

redis.opsForSet().remove(product);

我们设置可一颗随机获取Set中的一个元素:

Product random = redis.opsForSet().randomMember("cart");

因为Set没有索引和内部的排序,因此我们无法精准定位某个点,然后从Set中获取元素。

绑定到某个key上

为了记录阐述这些子API的用法,我们假设将Product对象保存到一个list中,并且ket为cart。这种场景下,假设我们想从list的右侧弹出一个元素,然后在list的尾部新增三个元素。我们此时可以使用boundListOps() 方法所返回的BoundListOperations:

BoundListOperations <String, Product> cart = 
            redis.boundListOps("cart");
Product popped = cart.rightPop();
cart.rightPush(product1);
cart.rightPush(product2);
cart.rightPush(product3);

注意,我们只在一个地方使用了条目的key,也就是调用boundListOps() 的时候。对返回的BoundListOperations执行的所有操作都会应用到这个key上。

使用key和value的序列化器

当某个条目保存到Redis key-value存储的时候,key和value都会使用Redis的序列化器(serializer)进行序列化。Spring Data Redis提供了多个这样的序列化器:

  • GenericToStringSerializer:使用String转化服务进行序列化;
  • JacksonJsonRedisSerializer:使用Jackson 1,将对象序列化为JSON;
  • Jackson2JsonRedisSerializer:使用Jackson 2,将对象序列化为JSON;
  • JdkSerializationRedisSerializer:使用Java序列化;
  • OxmSerializer:使用Spring O/X映射的编排器和解排器(marshaler和unmarshaler)实现序列化,用户XML序列化;
  • StringRedisSerializer:序列化String类型的key和value。

这些序列化器都实现了RedisSerializer接口,如果其中没有符合需求的序列化器,那么还可以自行创建。

RedisTemplate会使用JdkSerializationRedisSerializer,这意味着key和value都会通过Java进行序列化。StringRedisTemplate默认会使用StringRedisSerializer,它实际上就是实现String与byte数组之间的相互转换。这些默认的设置适用于很多的场景,但有时候可能会发现使用一个不同的序列化器也是很有用处的。

例如,假设当使用RedisTemplate的时候,我们希望将Product类型的value序列化为JSON,而key是String类型。RedisTemplate的setKeySerializer()和setValueSerializer()方法就需要如下所示:

@Bean
public RedisTemplate<String, Product>
        redisTemplate(RedisConnectionFactory cf) {
    RedisTemplate<String, Product> redis = 
        new RedisTemplate<String, Product>();
    redis.setConnectionFactory(cf);
    redis.setKeySerializer(new StringRedisSerializer());
    redis.setValueSerializer(
        new Jackson2JsonRedisSerializer<Product>(Product.class)
    );
    return redis;
}

在这里,我们设置RedisTemplate在序列化key的时候,使用StringRedisSerializer,并且也设置了在序列化Product的Jackson2JsonRedisSerializer。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355

推荐阅读更多精彩内容