Spring實(shí)戰(zhàn)(十二)-使用NoSQL數(shù)據(jù)庫

本文基于《Spring實(shí)戰(zhàn)(第4版)》所寫。

使用MongoDB持久化文檔數(shù)據(jù)

將數(shù)據(jù)收集到一個(gè)非規(guī)范化(也就是文檔)的結(jié)構(gòu)中,保持了獨(dú)立的實(shí)體,能夠按照這種方式優(yōu)化并處理文檔的數(shù)據(jù)庫,我們稱之為文檔數(shù)據(jù)庫。

有些數(shù)據(jù)具有明顯的關(guān)聯(lián)關(guān)系,文檔型數(shù)據(jù)庫并沒有針對存儲這樣的數(shù)據(jù)進(jìn)行優(yōu)化。

Spittr應(yīng)用的域?qū)ο蟛⒉贿m合文檔數(shù)據(jù)庫。在本章中,我們將會在一個(gè)購物訂單系統(tǒng)中學(xué)習(xí)MongoDB。

MongoDB是最為流行的開源文檔數(shù)據(jù)庫之一。Spring Data MongoDB提供了三種方式在Spring應(yīng)用中使用MongoDB:

  • 通過注解實(shí)現(xiàn)對象-文檔映射;
  • 使用MongoTemplate實(shí)現(xiàn)基于模板的數(shù)據(jù)庫訪問;
  • 自動化的運(yùn)行時(shí)Repository生成功能。

啟用MongoDB

在使用MongoDB之前,我們首先要配置Spring Data MongoBD ,在Spring配置中添加幾個(gè)必要的bean。

  • 配置MongoClient,以便于訪問MongoDB數(shù)據(jù)庫;
  • 配置MongoTemplate bean,實(shí)現(xiàn)基于模板的數(shù)據(jù)庫訪問;
  • 啟用Spring Data MongoDB的自動化Repository生成功能(不是必須,但強(qiáng)烈推薦)。

如下的程序清單展現(xiàn)了如何編寫簡單的Spring Data MongoDB配置類,它包含了上述的幾個(gè)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生成功能。

以上程序中還包含了兩個(gè)帶有@Bean注解的方法。第一個(gè)@Bean方法使用MongoClientFactoryBean聲明了一個(gè)Mongo實(shí)例。這個(gè)bean將Spring Data MongoDB與數(shù)據(jù)庫本身連接了起來。盡管我們可以使用MongoClient直接創(chuàng)建Mongo實(shí)例,但如果這樣做的話,就必須要處理MongoClient構(gòu)造器所拋出的UnknownHostException異常。在這里,使用Spring Data MongoDB的MongoClientFactoryBean更加簡單。因?yàn)樗且粋€(gè)工廠bean,因此MongoClientFactoryBean會負(fù)責(zé)構(gòu)建Mongo實(shí)例,我們不必再擔(dān)心UnknownHostException異常。

另外一個(gè)@Bean方法聲明了MongoTemplate bean,在它構(gòu)造時(shí),使用了其他@Bean方法所創(chuàng)建的Mongo實(shí)例的引用以及數(shù)據(jù)庫的名稱。Repository的自動化生成功能在底層使用了它。

除了直接聲明這些bean,我們還可以讓配置類擴(kuò)展AbstractMongoConfiguration并重載getDatabaseName()和mongo()方法。如下的程序展現(xiàn)了如何使用這種配置方式。

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() {  // 指定數(shù)據(jù)庫名稱
        return "OrdersDB";
    }
    public Mongo mongo() throws Exception {
        return new MongoClient();             // 創(chuàng)建Mongo客戶端
    }
}

這個(gè)新的配置類與上一個(gè)的配置類功能是相同的。最為顯著的區(qū)別在于這個(gè)配置中沒有直接聲明MongoTemplate bean,當(dāng)然它還是會被隱式低創(chuàng)建。我們在這里重載了getDatabaseName() 方法來提供數(shù)據(jù)庫的名稱。mongo()方法依然會創(chuàng)建一個(gè)MongoClient的實(shí)例,因?yàn)樗鼤伋鯡xception,所以我們可以直接使用MongoClient,而不必再使用MongoClientFactoryBean了。

如果MongoDB服務(wù)器運(yùn)行在其他的機(jī)器上,那么可以在創(chuàng)建MongoClient的時(shí)候進(jìn)行指定:

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

也可指定端口(有時(shí)并不是默認(rèn)的27017)

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

還可啟用認(rèn)證功能:為了訪問數(shù)據(jù)庫,需要提供應(yīng)用的憑證

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(  // 創(chuàng)建 MongoDB 憑證
                env.getProperty("mongo.username"),
                "OrdersDB",
                env.getProperty("mongo.password").toCharArray());

        return new MongoClient(  //  創(chuàng)建 MongoClient
                new ServerAddress("localhost" , 37017),
                Arrays.asList(credential));
    }
}

為了訪問需要認(rèn)證的MongoDB服務(wù)器,MongoClient在實(shí)例化的時(shí)候必須要有一個(gè)MongoCredential的列表。

除了Java配置的方案,還可以使用XML進(jìn)行配置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>

為模型添加注解,實(shí)現(xiàn)MongoDB持久化

MongoDB沒有提供對象-文檔映射的注解。Spring Data MongoDB填補(bǔ)了這一空白,提供了一些將Java類型映射為MongoDB文檔的注解。下表描述了這些注解

注解 描述
@Document 標(biāo)示映射到MongoDB文檔上的領(lǐng)域?qū)ο?/td>
@Id 標(biāo)示某個(gè)域?yàn)镮D域
@DbRef 標(biāo)示某個(gè)域要引用其他的文檔,這個(gè)文檔有可能位于另一個(gè)數(shù)據(jù)庫中
@Field 為文檔域指定自定義的元數(shù)據(jù)
@Version 標(biāo)示某個(gè)屬性用作版本域

@Document和@Id注解類似于JPA的@Entity和@Id注解。對于要以文檔形式保存到MongoDB數(shù)據(jù)庫的每個(gè)Java類型都會使用這兩個(gè)注解。例如,如下的程序展現(xiàn)了如何為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;      // 覆蓋默認(rèn)的域名

    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進(jìn)行持久化。其id屬性上使用了@Id注解,用來指定它作為文檔的ID。除此之外, customer屬性上使用了@Field注解,這樣的話,當(dāng)文檔持久化的時(shí)候customer屬性將會映射為名為client的域。

注意,其他的屬性并沒有添加注解。除非將屬性設(shè)置為瞬時(shí)態(tài)(transient)的,否則Java對象中所有的域都會持久化為文檔中的域。并且如果我們不使用@Field注解進(jìn)行設(shè)置的話,那么文檔域中的名字將會與對應(yīng)的Java屬性相同。

同時(shí),需要注意的是items屬性,它指的是訂單中具體條目的集合。在傳統(tǒng)的關(guān)系型數(shù)據(jù)庫中,這些條目將會保存在另外的一個(gè)數(shù)據(jù)庫表中,通過外鍵進(jìn)行應(yīng)用,items域上很可能還會使用JPA的@OneToMany注解。但在這里,情形完全不同。

文檔展現(xiàn)了關(guān)聯(lián)但非規(guī)范化的數(shù)據(jù)。相關(guān)的概念(如訂單中的條目)被嵌入到頂層的文檔數(shù)據(jù)中。

文檔可以與其他的文檔產(chǎn)生關(guān)聯(lián),但這并不是文檔數(shù)據(jù)庫擅長的功能。在本例購買訂單與行條目之間的關(guān)聯(lián)關(guān)系中,行條目只是同一個(gè)訂單文檔里面內(nèi)嵌的一部分。因此,沒有必要為這種關(guān)聯(lián)關(guān)系添加任何注解。實(shí)際上,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。這是因?yàn)槲覀儾粫为?dú)將Item持久化為文檔。它始終會是Order文檔中Item列表的一個(gè)成員,并且會作為文檔中的嵌入元素。

使用MongoTemplate訪問MongoDB

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

@Autowired
MongoOperations mongo;

注意,MongoOperations是MongoTemplate所實(shí)現(xiàn)的接口。MongoOperations暴露了多個(gè)使用MongoDB文檔數(shù)據(jù)庫的方法,比如計(jì)算文檔集合中有多少條文檔。使用注入MongoOperations,我們可以得到Order集合并調(diào)用count()來得到數(shù)量:

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

現(xiàn)在,假設(shè)要保存一個(gè)新的Order。為了完成這個(gè)任務(wù),我們可以調(diào)用save()方法:

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

save()方法的第一個(gè)參數(shù)是新創(chuàng)建的Order,第二個(gè)參數(shù)是要保存文檔存儲的名稱。

另外,我們還可以調(diào)用findById()方法來根據(jù)ID查找訂單:

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

對于更高級的查詢,我們需要構(gòu)造Query對象并將其傳遞給find()方法。例如,要查找所有client域等于“Chuck Wagon”的訂單,可以使用如下的代碼:

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

再比如,我們想查詢Chuck所有通過Web創(chuàng)建的訂單:

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

如果想移除某一個(gè)文檔的話,那么就應(yīng)該使用remove() 方法:

mongo.remove(order);

編寫MongoDB Repository

如果不愿意編寫Repository的(通常,我們將MongoOperations注入到自己設(shè)計(jì)的Repository類中,并使用它),那么Spring Data MongoDB能夠自動在運(yùn)行時(shí)生成Repository實(shí)現(xiàn)。

我們已經(jīng)通過@EnableMongoRepositories注解啟用了Spring Data MongoDB的Repository功能,接下來需要做的就是創(chuàng)建一個(gè)接口,Repository實(shí)現(xiàn)要基于這個(gè)接口來生成,這個(gè)接口要擴(kuò)展MongoRepository。如下的程序中OrderRepository擴(kuò)展了MongoRepository,為Order文檔提供了基本的CRUD操作。

package orders.db;

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

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

因?yàn)镺rderRepository擴(kuò)展了MongoRepository,因此它就會傳遞性的擴(kuò)展Repository標(biāo)記接口。任何擴(kuò)展Repository的接口將會在運(yùn)行時(shí)自動生成實(shí)現(xiàn)。

MongoRepository接口有兩個(gè)參數(shù),第一個(gè)是帶有@Document注解的對象類型,也就是該Repository要處理的類型。第二個(gè)參數(shù)是帶有@Id注解的屬性類型。

盡管OrderRepository本身并沒有定義任何方法,但是它會繼承多個(gè)方法,包括對Order文檔進(jìn)行CRUD操作的方法。下表描述了OrderRepository繼承的所有方法。

方法 描述
long count(); 返回指定Repository類型的文檔數(shù)量
void delete(Iterable<? extends T>); 刪除與指定對象關(guān)聯(lián)的所有文檔
void delete(T); 刪除與指定對象關(guān)聯(lián)的文檔
void delete(ID); 根據(jù)ID刪除某一個(gè)文檔
void deleteAll(); 刪除指定Repository類型的所有文檔
boolean exists(Object); 如果存在與指定對象相關(guān)聯(lián)的文檔,則返回true
boolean exists(ID); 如果存在指定ID的文檔,則返回true
List<T> findAll(); 返回指定Repository類型的所有文檔
List<T> findAll(Iterable<ID>); 返回指定文檔ID對應(yīng)的所有文檔
Page<?> findAll(Pageable pageable); 為指定的Repository類型,返回分頁且排序的文檔列表
List<T> findAll(Sort); 為指定的Repository類型,返回排序后的文檔列表
T findOne(ID); 為指定的ID返回單個(gè)文檔
save(Iterable<s>); 保存指定Iterable中所有文檔
save(<s>); 為給定的對象保存一條文檔

OrderRepository擴(kuò)展了MongoRepository<Order, String>,那么T就映射為Order,ID映射為String,而s映射為所有擴(kuò)展Order的類型。

添加自定義的查詢方法

與Spring Data JPA支持方法命名約定類似,都能夠幫助Spring Data為遵循約定的方法自動生成實(shí)現(xiàn)。這意味這我們可以為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這個(gè)查詢動詞并不是固定的,如果喜歡的話,我們還可以使用get或read作為查詢動詞;

除此之外,還有一個(gè)特殊的動詞用來為匹配的對象計(jì)數(shù):

int countByCustomer(String c);

與Spring Data JPA類似,在查詢動詞與By之前,我們有很大的靈活性。比如,我們可以標(biāo)示Order或者一些其他的詞語,都不會影響獲取的內(nèi)容。

如果只想要一個(gè)Order對象的話,我們可以只需要簡單的返回Order:

Order findASingleOrderByCustomer(String c);

這里,所返回的就是原本List中的第一個(gè)Order對象。如果沒有匹配元素的話,方法將會返回null。

指定查詢

@Query能夠想在JPA中那樣在MongoDB上為Repository方法指定自定義的查詢。唯一的區(qū)別在于針對MongoDB時(shí),@Query會接受一個(gè)JSON查詢,而不是JPA查詢。

例如,假設(shè)我們想要查詢給定類型的訂單,并要求customer的名稱為“Chuck Wagon”。OrderRepository中如下的方法聲明能夠完成所需的任務(wù):

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

@Query中給定的JSON將會與所有的Order文檔進(jìn)行匹配,并返回匹配的文檔。需要注意的是,type屬性映射成了“?0”,這表明type屬性應(yīng)該與查詢方法的第零個(gè)參數(shù)相等。如果有多個(gè)參數(shù)的話,他們可以通過“?1”、“?2”等方式進(jìn)行引用。

混合自定義的功能

對于JPA來說,混合自定義的功能涉及到創(chuàng)建一個(gè)中間接口來聲明自定義的方法,為這些自定義方法創(chuàng)建實(shí)現(xiàn)類并修改自動化的Repository接口,使其擴(kuò)展中間接口。對于Spring Data MongoDB來說,這些步驟都是相同的。

假設(shè)我們想要查詢文檔中type屬性匹配給定值的Order對象。而且,如果給定的類型是“NET”,那我們就查找type值為“WEB”的Order對象。

首先,定義中間接口:

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

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

接下來,我們要編寫混合實(shí)現(xiàn)

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);  // 創(chuàng)建查詢
        Query query = Query.query(where);
        return mongo.find(query,Order.class);  // 執(zhí)行查詢
    }
}

剩下的工作就是修改OrderRepository,讓其擴(kuò)展中間接口OrderOperations:

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

將這些關(guān)聯(lián)起來的關(guān)鍵點(diǎn)在于實(shí)現(xiàn)類的名稱為OrderRepositoryImpl。這個(gè)名字前半部分與OrderRepository相同,只是添加了“Impl”后綴。當(dāng)Spring Data MongoDB生成Repository實(shí)現(xiàn)時(shí),它會查找這個(gè)類并將其混合到自動生成的實(shí)現(xiàn)中。

如果不喜歡“Impl”后綴的話,可以配置。

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

如果使用XML配置的話,我們可以設(shè)置<mongo:repositories>的repository-impl-postfix屬性:

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

不管采用哪種方式,都讓Spring Data MongoDB 查找名為OrderRepositoryStuff的類,而不再查找OrderRepositoryImpl。

使用Redis操作key-value數(shù)據(jù)

Redis是一種key-value存儲的數(shù)據(jù)庫,也可以說是持久化的哈希Map。

Spring Data的關(guān)鍵特性,也就是面向模板的數(shù)據(jù)訪問,能夠在使用Redis的時(shí)候,為我們提供幫助。

Spring Data Redis包含了多個(gè)模板實(shí)現(xiàn),用來完成Redis數(shù)據(jù)庫的數(shù)據(jù)存取功能。為了創(chuàng)建Spring Data Redis的模板,我們首先需要有一個(gè)Redis連接工廠。Spring Data Redis提供了四個(gè)連接工廠供我們選擇。

連接到Redis

Redis連接工廠會生成到Redis數(shù)據(jù)庫服務(wù)器的連接。Spring Data Redis為四種Redis客戶端實(shí)現(xiàn)提供了連接工廠:

  • JedisConnectionFactory
  • JredisConnectionFactory
  • LettuceConnectionFactory
  • SrpConnectionFactory

從Spring Data Redis的角度來看,這些連接工廠在適用性上都是相同的。

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

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

通過默認(rèn)構(gòu)造器創(chuàng)建的連接工廠會向localhost上的6379端口創(chuàng)建連接,并且沒有密碼。如果你的Redis服務(wù)器運(yùn)行在其他的主機(jī)或端口上,在創(chuàng)建連接工廠的時(shí)候,可以設(shè)置這些屬性:

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

類似地,如果你的Redis服務(wù)器配置為需要客戶端認(rèn)證的話,那么可以通過調(diào)用setPassword()方法來設(shè)置密碼:

@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);
}

假設(shè)使用LettuceConnectionFactory的話,可以按照如下的方式進(jìn)行配置:

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

所有的Redis連接工廠都具有setHostName()、setPort()和setPassword()方法。這樣,它們在配置方面實(shí)際上是相同的。

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

顧名思義,Redis連接工廠會生成到Redis key-value存儲的連接(以RedisConnection的形式)。借助RedisConnection,可以存儲和讀取數(shù)據(jù)。例如,我們可以獲取連接并使用它來保存一個(gè)問候信息,如下所示:

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以模板的形式提供了較高等級的數(shù)據(jù)訪問方案。實(shí)際上,Spring Data Redis提供了兩個(gè)模板:

  • RedisTemplate
  • StringRedisTemplate

RedisTemplate可以極大地簡化Redis數(shù)據(jù)訪問,能夠讓我們持久化各種類型的key和value,并不局限于字節(jié)數(shù)組。在認(rèn)識到key和value通常是String類型之后,StringRedisTemplate擴(kuò)展了RedisTemplate,只關(guān)注String類型。

假設(shè)我們已經(jīng)有了RedisConnectionFactory,那么可以按照如下的方式構(gòu)建RedisTemplate:

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

注意,RedisTemplate使用了兩個(gè)類型進(jìn)行參數(shù)化。第一個(gè)是key的類型,第二個(gè)是value的類型。在這里所構(gòu)建的RedisTemplate中,將會保存Product對象作為value,并將其賦予一個(gè)String類型的key。

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

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

注意,與RedisTemplate不同,StringRedisTemplate有一個(gè)接受RedisConnectionFactory的構(gòu)造器,因此沒有必要在構(gòu)建后調(diào)用setConnectionFactory();

如果你經(jīng)常使用RedisTemplate或StringRedisTemplate的話,可以考慮將其配置為bean,然后注入到需要的地方。如下就是一個(gè)聲明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的大多數(shù)操作都是下表中的子API提供的。

方法 子API接口 描述
opsForValue() ValueOperations<K, V> 操作具有簡單值的條目
opsForList() ListOperations<K, V> 操作具有l(wèi)ist值的條目
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的方式,操作具有l(wèi)ist值的條目
boundSetOps(K) BoundSetOperations<K, V> 以綁定指定key的方式,操作具有set值的條目
boundZSetOps(K) BoundZSetOperations<K, V> 以綁定指定key的方式,操作具有ZSet(排序的set)值的條目
boundHashOps(K) BoundHashOperations<K, V> 以綁定指定key的方式,操作具有hash值的條目

使用簡單的值

假設(shè)我們通過RedisTemplate<String, Product>保存Product,其中key是sku屬性的值。如下的代碼片段展示了如何借助opsForValue()方法完成該功能:

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

類似地,如果希望獲取sku屬性為123456的產(chǎn)品,那么可以使用如下的代碼片段:

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

如果按照給定的key,無法獲得條目的話,將會返回null。

使用List類型的值

使用List類型,只需使用opsForList()方法即可。例如,我們可以在一個(gè)List類型的條目尾部添加一個(gè)值:

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

通過這種方式,我們向列表的尾部添加了一個(gè)Prodcut,所使用的這個(gè)列表在存儲時(shí)key為cart。如果這個(gè)key尚未存在列表的話,將會創(chuàng)建一個(gè)。

rightPush()會在列表的尾部添加一個(gè)元素,而leftPush() 則會在列表的頭部添加一個(gè)值:

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

我們有很多方式從列表中獲取元素,可以通過leftPop()或者rightPop()方法從列表中彈出一個(gè)元素:

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

除了從列表中獲取值以外,這兩個(gè)方法還有一個(gè)副作用就是從列表中移除所彈出的元素。如果你只是想獲取值的話(甚至可能要在列表中間獲?。?,那么可以使用range()方法:

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

range()方法不會從列表中移除任何元素。但是它會根據(jù)指定的key和索引防偽,獲取范圍內(nèi)的一個(gè)或多個(gè)值。上面的樣例中,會獲取11個(gè)元素,從索引為2的元素到索引為12的元素(不包含)。如果范圍超出了列表的邊界,那么只會返回索引在范圍內(nèi)的元素。如果該索引范圍內(nèi)沒有元素的話,將會返回一個(gè)空的列表。

在Set上執(zhí)行操作

除了操作列表以外,我們還可以使用opsForSet()操作Set。最為常見的是

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

在我們有多個(gè)Set并填充值之后,就可以對這些Set進(jìn)行一些有意思的操作,如獲取其差異,求交集和求并集:

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

當(dāng)然,我們還可以移除它的元素:

redis.opsForSet().remove(product);

我們設(shè)置可一顆隨機(jī)獲取Set中的一個(gè)元素:

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

因?yàn)镾et沒有索引和內(nèi)部的排序,因此我們無法精準(zhǔn)定位某個(gè)點(diǎn),然后從Set中獲取元素。

綁定到某個(gè)key上

為了記錄闡述這些子API的用法,我們假設(shè)將Product對象保存到一個(gè)list中,并且ket為cart。這種場景下,假設(shè)我們想從list的右側(cè)彈出一個(gè)元素,然后在list的尾部新增三個(gè)元素。我們此時(shí)可以使用boundListOps() 方法所返回的BoundListOperations:

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

注意,我們只在一個(gè)地方使用了條目的key,也就是調(diào)用boundListOps() 的時(shí)候。對返回的BoundListOperations執(zhí)行的所有操作都會應(yīng)用到這個(gè)key上。

使用key和value的序列化器

當(dāng)某個(gè)條目保存到Redis key-value存儲的時(shí)候,key和value都會使用Redis的序列化器(serializer)進(jìn)行序列化。Spring Data Redis提供了多個(gè)這樣的序列化器:

  • GenericToStringSerializer:使用String轉(zhuǎn)化服務(wù)進(jìn)行序列化;
  • JacksonJsonRedisSerializer:使用Jackson 1,將對象序列化為JSON;
  • Jackson2JsonRedisSerializer:使用Jackson 2,將對象序列化為JSON;
  • JdkSerializationRedisSerializer:使用Java序列化;
  • OxmSerializer:使用Spring O/X映射的編排器和解排器(marshaler和unmarshaler)實(shí)現(xiàn)序列化,用戶XML序列化;
  • StringRedisSerializer:序列化String類型的key和value。

這些序列化器都實(shí)現(xiàn)了RedisSerializer接口,如果其中沒有符合需求的序列化器,那么還可以自行創(chuàng)建。

RedisTemplate會使用JdkSerializationRedisSerializer,這意味著key和value都會通過Java進(jìn)行序列化。StringRedisTemplate默認(rèn)會使用StringRedisSerializer,它實(shí)際上就是實(shí)現(xiàn)String與byte數(shù)組之間的相互轉(zhuǎn)換。這些默認(rèn)的設(shè)置適用于很多的場景,但有時(shí)候可能會發(fā)現(xiàn)使用一個(gè)不同的序列化器也是很有用處的。

例如,假設(shè)當(dāng)使用RedisTemplate的時(shí)候,我們希望將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;
}

在這里,我們設(shè)置RedisTemplate在序列化key的時(shí)候,使用StringRedisSerializer,并且也設(shè)置了在序列化Product的Jackson2JsonRedisSerializer。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,261評論 6 342
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,534評論 19 139
  • 1.1 spring IoC容器和beans的簡介 Spring 框架的最核心基礎(chǔ)的功能是IoC(控制反轉(zhuǎn))容器,...
    simoscode閱讀 6,846評論 2 22
  • “財(cái)富自由”這個(gè)詞,三年前聽得最多的地方是在安利直銷,三年后聽的最多的是在創(chuàng)業(yè)圈里。很多人覺得努力的目的是為了有一...
    漠子閱讀 391評論 0 6
  • 我們每天或多或少都處于被抱怨的環(huán)境之中。東西用好沒有歸位要被父母批評幾句。出門等車車不來,人們會指責(zé)公交公司調(diào)度不...
    瑩瑩微光閱讀 241評論 0 0

友情鏈接更多精彩內(nèi)容