本文基于《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注解。但在這里,情形完全不同。

文檔可以與其他的文檔產(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。