使用NoSQL数据库
使用MongoDB持久化文档数据
文档数据库不适用于什么场景
有些数据具有明显的关联关系,文档型数据库并没有针对存储这样的数据进行优化。例如,社交网络表现了应用中不同的用户之间是如何建立关联的,这种情况就不适合放到文档型数据库中。在文档数据库中存储具有丰富关联关系的数据也并非完全不可能,但这样做的话,通常会发现遇到的挑战要多于所带来的收益。
MongoDB是最为流行的开源文档数据库之一
- 通过注解实现对象-文档映射
- 使用MongoTemplate实现基于模板的数据库访问
- 自动化的运行时Repository生成功能
启用MongoDB
package orders.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
@Configuration
@EnableMongoRepositories(basePackages = "orders.db")
public class MongoConfig {
@Bean
public MongoFactoryBean() {
MongoFactoryBean mongo = new MongoFactoryBean();
mongo.setHost("localhost");
return mongo;
}
@Bean
public MongoOperations MongoTemplate(Mongo mongo) {
return new MongoTemplate(mongo, "OrdersDB");
}
}
之前通过@EnableJpaRepositories注解,启用了Spring Data的自动化JPA Repository生成功能。与之类似,@EnableMongoRepositories为MongoDB实现了相同的功能。
除了@EnableMongoRepositories之外,上述程序中还包含了两个带有@Bean注解的方法。第一个@Bean方法使用MongoFactoryBean声明了一个Mongo实例。这个bean将Spring Data MongoDB与数据库本身连接了起来(与使用关系型数据时DataSource所做的事情并没有什么区别)。尽管可以使用MongoClient直接创建Mongo实例,但如果这样做的话,就必须要处理MongoClient构造器所抛出的UnknownHostException异常。在这里,使用Spring Data MongoDB的MongoFactoryBean更加简单。因为它是一个工厂bean,因此MongoFactoryBean会负责构建Mongo实例,不必再担心UnknownHostException异常。
另外一个@Bean方法声明了MongoTemplate bean,在它构造时,使用了其他@Bean方法所创建的Mongo实例的引用以及数据库的名称。稍后,将会看到如何使用MongoTemplate来查询数据库。即便不直接使用MongoTemplate,也会需要这个bean,因为Repository的自动化生成功能在底层使用了它。
除了直接声明这些bean,还可以让配置类扩展AbstractMongo-Configuration并重载getDatabaseName()和mongo()方法:
package orders.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
@Configuration
@EnableMongoRepositories(basePackages = "orders.db")
public class MongoConfig extends AbstractMongoConfiguration {
@Override
protected String getDatabaseName() {
return "OrdersDB";
}
@Override
public Mongo mongo() throws Exception {
return new MongoClient();
}
}
这个新的配置类与之前程序的功能是相同的,只不过在篇幅上更加简洁。最为显著的区别在于这个配置中没有直接声明MongoTemplate bean,当然它还是会被隐式地创建。在这里重载了getDatabaseName()方法来提供数据库的名称。mongo()方法依然会创建一个MongoClient的实例,因为它会抛出Exception,所以可以直接使用MongoClient,而不必再使用MongoFactoryBean了。
到目前为止,不管是使用上面两个哪个程序,都为Spring Data MongoDB提供了一个运行配置,也就是说,只要MongoDB服务器运行在本地即可。如果MongoDB服务器运行在其他的机器上,那么可以在创建MongoClient的时候进行指定:
public Mongo mongo() throws Exception {
return new MongoClient("mongodbserver");
}
MongoDB服务器有可能监听的端口并不是默认的27017。如果是这样的话,在创建MongoClient的时候,还需要指定端口:
public Mongo mongo() throws Exception {
return new MongoClient("mongodbserver", 37017);
}
如果MongoDB服务器运行在生产配置上,认为你可能还启用了认证功能。在这种情况下,为了访问数据库,我们还需要提供应用的凭证。访问需要认证的MongoDB服务器稍微有些复杂,如下面的程序清单所示。
@Autowired
private Environment env;
@Override
public Mongo mongo() throws Exception {
MongoCredential credential = MongoCredential.createMongoCRCredential(env.getProperty("mongo.username"), "OrdersDB", env.getProperty("mongo.password").toCharArray());
return new MongoClient(new ServerAddress("localhost", 37017), Arrays.asList(credential));
}
为了访问需要认证的MongoDB服务器,MongoClient在实例化的时候必须要有一个MongoCredential的列表。在上述程序中,为此创建了一个MongoCredential。为了将凭证信息的细节放在配置类外边,它们是通过注入的Environment对象解析得到的。
如下的程序展现了如何使用mongo配置命名空间来配置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/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo.xsd 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持久化
用于对象-文档映射的Spring Data MongoDB注解
| 注 解 | 描 述 |
|---|---|
| @Document | 标示映射到MongoDB文档上的领域对象 |
| @Id | 标示某个域为ID域 |
| @DbRef | 标示某个域要引用其他的文档,这个文档有可能位于另外一个数据库中 |
| @Field | 为文档域指定自定义的元数据 |
| @Version | 标示某个属性用作版本域 |
@Document和@Id注解类似于JPA的@Entity和@Id注解。将会经常使用这两个注解,对于要以文档形式保存到MongoDB数据库的每个Java类型都会使用这两个注解。例如,如下的程序清单展现了如何为Order类添加注解,它会被持久化到MongoDB中。
package orders;
import java.util.Collection;
import java.util.LinkedHashSet;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
@Document
public class Order {
@Id
private String id;
@Field("customer")
private String customer;
private String type;
private Collection<Item> items = new LinkedHashSet<Item>();
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;
}
public String getId() {
return id;
}
}
可以看到,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 Order getOrder() {
return 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;
}
public Long getId() {
return id;
}
}
没有必要为Item添加@Document注解,也没有必要为它的域指定@Id。这是因为不会单独将Item持久化为文档。它始终会是Order文档中Item列表的一个成员,并且会作为文档中的嵌入元素。
当然,如果想指定Item中的某个域如何持久化到文档中,那么可以为对应的Item属性添加@Field注解。
使用MongoTemplate访问MongoDB
在配置类中配置了MongoTemplate bean,不管是显式声明还是扩展AbstractMongoConfiguration都能实现相同的效果。接下来,需要做的就是将其注入到使用它的地方:
@Autowired
MongoOperations mongo;
注意,在这里将MongoTemplate注入到一个类型为MongoOperations的属性中。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);
在本例中,用来构造Query对象的Criteria只检查了一个域,但是它也可以用来构造更加有意思的查询。比如,我们想要查询Chuck所有通过Web创建的订单:
List<Order> chucksOrders = mongo.find(Query.query(Criteria.where("client").is("Chuck Wagon").and("type").is("WEB")), Order.class);
如果想移除某一个文档的话,那么就应该使用remove()方法:
mongo.remove(order);
编写MongoDB Repository
已经通过@EnableMongoRepositories注解启用了Spring Data MongoDB的Repository功能,接下来需要做的就是创建一个接口,Repository实现要基于这个接口来生成。不过,在这里,不再扩展JpaRepository,而是要扩展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标记接口。为OrderRepository生成读取和写入数据到MongoDB文档数据库的实现。
MongoRepository接口有两个参数,第一个是带有@Document注解的对象类型,也就是该Repository要处理的类型。第二个参数是带有@Id注解的属性类型。
尽管OrderRepository本身并没有定义任何方法,但是它会继承多个方法,包括对Order文档进行CRUD操作的方法:
| 方 法 | 描 述 |
|---|---|
| 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 findAll(); | 返回指定Repository类型的所有文档 |
| List findAll(Iterable); | 返回指定文档ID对应的所有文档 |
| List findAll(Pageable); | 为指定的Repository类型,返回分页且排序的文档列表 |
| List findAll(Sort); | 为指定的Repository类型,返回排序后的所有文档列表 |
| T findOne(ID); | 为指定的ID返回单个文档 |
| Save( terable | 保存指定Iterable中的所有文档 |
| save ( < S > ); | 为给定的对象保存一条文档 |
上表中的方法使用了传递进来和方法返回的泛型。OrderRepository扩展了MongoRepository<Order, String>,那么T就映射 为Order,ID映射为String,而S映射为所有扩展Order的类型。
添加自定义的查询方法
Spring Data JPA支持方法命名约定,它能够帮助Spring Data为遵循约定的方法自动生成实现。实际上,相同的约定也适用于Spring Data MongoDB。这意味着可以为OrderRepository添加自定义的方法:
public interface OrderRepository extends MongoRepository<Order, String> {
List<Order> findByCustomer(String customer);
List<Order> findByCustomerLike(String customer);
List<Order> findByCustomerAndType(String customer, String type);
List<Order> findByCustomerLikeAndType(String customer, String type);
}
这里有四个新的方法,每一个都是查找满足特定条件的Order对象。其中第一个用来获取customer属性等于传入值的Order列表;第二个方法获取customer属性like传入值的Order列表;接下来方法会返回customer和type属性等于传入值的Order对象;最后一个方法与前一个类似,只不过customer在对比的时候使用的是like而不是equals。
其中,find这个查询动词并不是固定的。如果喜欢的话,还可以使用get作为查询动词:
List<Order> getByCustomer(String c);
如果read更适合的话,还可以使用这个动词:
List<Order> readByCustomer(String c);
除此之外,还有一个特殊的动词用来为匹配的对象计数:
int countByCustomer(String c);
与Spring Data JPA类似,在查询动词与By之前,有很大的灵活性。例如,可以标示要查找什么内容:
List<Order> findOrderByCustomer(String c);
其中,Orders这个词没并没有什么特殊之处,它不会影响要获取的内容。也可以将方法按照如下的方式命名:
List<Order> findSomerStuffWeNeedByCustomer(String c);
其实,并不是必须要返回List,如果只想要一个Order对象的话,可以只需简单地返回Order:
Order findASingleOrderByCustomer(String c);
这里,所返回的就是原本List中的第一个Order对象。如果没有匹配元素的话,方法将会返回null。
指定查询
@Query注解可以为Repository方法指定自定义的查询。@Query能够像在JPA中那样用在MongoDB上。唯一的区别在于针对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”等方式进行引用。
混合自定义的功能
假设想要查询文档中type属性匹配给定值的Order对象。可以通过创建签名为List findByType(String t)的方法,很容易实现这个功能。但是,如果给定的类型是“NET”,那就查找type值为“WEB”的Order对象。要实现这个功能的话,这就有些困难了,即便使用@Query注解也不容易实现。不过,混合实现的做法能够完成这项任务。
首先,定义中间接口:
package orders.db;
import java.util.List;
import orders.Order;
public interface OrderOperations {
List<Order> findOrdersByType(String t);
}
这非常简单。接下来,要编写混合实现,具体实现如下面的程序清单所示。
package orders.db;
import java.util.List;
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;
public class OrderRepositoryImpl implements OrderOperations {
@Autowired
private MongoOperations mongo;
public List<Order> findOrderByType(String t) {
String type = t.equals("NET") ? "WEB" : t;
Criteria where = Criteria.where("type").is(t);
Query query = Query.query(where);
return mongo.find(query, Order.class);
}
}
可以看到,混合实现中注入了MongoOperations(也就是MongoTemplate所实现的接口)。findOrdersByType()方法使用MongoOperations对数据库进行了查询,查找匹配条件的文档。
剩下的工作就是修改OrderRepository,让其扩展中间接口OrderOperations:
public interface OrderRepository extends MongoRepository<Order, String>, OrderOperations {
...
}
将这些关联起来的关键点在于实现类的名称为OrderRepositoryImpl。这个名字前半部分与OrderRepository相同,只是添加了“Impl”后缀。当Spring Data MongoDB生成Repository实现时,它会查找这个类并将其混合到自动生成的实现中。
如果不喜欢“Impl”后缀的话,那么可以配置Spring Data MongoDB,让其按照名字查找具备不同后缀的类。需要做的就是设置@EnableMongoRepositories的属性(在Spring配置类中):
@Configuration
@EnableMongoRepositories(basePackages="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。
使用Neo4j操作图数据
配置Spring Data Neo4j
配置Spring Data Neo4j的关键在于声明GraphDatabaseService bean和启用Neo4j Repository自动生成功能。如下的程序展现了Spring Data Neo4j所需的基本配置。
package orders.config;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.factory.GraphDatabaseFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.config.Neo4jConfiguration;
@Configuration
@EnableNeo4jRepositories(basePackages = "orders.db")
public class Neo4jConfig extends Neo4jConfiguration {
public Neo4jConfig() {
setBasePackage("orders");
}
@Bean(destroyMethod="shutdown")
public GraphDatabaseService graphDatabaseService() {
return new GraphDatabaseFactory()
.newEmbeddedDatabase("/tmp/graphdb");
}
}
@EnableNeo4jRepositories注解能够让Spring Data Neo4j自动生成Neo4j Repository实现。它的basePackages属性设置为orders.db包,这样它就会扫描这个包来查找(直接或间接)扩展Repository标记接口的其他接口。
Neo4jConfig扩展自Neo4jConfiguration,后者提供了多个便利的方法来配置Spring Data Neo4j。在这些方法中,就包括setBasePackage(),它会在Neo4jConfig的构造器中调用,用来告诉Spring Data Neo4j要在orders包中查找模型类。
最后一部分是定义GraphDatabaseServicebean。在本例中,graphDatabaseService()方法使用GraphDatabaseFactory来创建嵌入式的Neo4j数据库。在Neo4j中,嵌入式数据库不要与内存数据库相混淆。在这里,“嵌入式”指的是数据库引擎与应用运行在同一个JVM中,作为应用的一部分,而不是独立的服务器。数据依然会持久化到文件系统中(在本例中,也就 是“/tmp/graphdb”中)。
作为另外的一种方式,可能会希望配置GraphDatabaseService连接远程的Neo4j服务器。如果spring-data-neo4j-rest库在应用的类路径下,那么就可以配置SpringRestGraphDatabase,它会通过RESTful API来访问远程的Neo4j数据库:
@Bean(destroyMethod="shutdown")
public GraphDatabaseService graphDatabaseService() {
return new SpringRestGraphDatabase("http://graphdbserver:7474/db/data/");
}
如上所示,SpringRestGraphDatabase在配置时,假设远程的数据库并不需要认证。但是,在生产环境的配置中,当创建SpringRestGraphDatabase的时候,可能希望提供应用的凭证:
@Bean(destroyMethod="shutdown")
public GraphDatabaseService graphDatabaseService(Environment env) {
return new SpringRestGraphDatabase("http://graphdbserver:7474/db/data/", env.getProperty("db.username"), env.getProperty("db.password"));
}
在这里,凭证是通过注入的Environment获取到的,避免了在配置类中的硬编码。
Spring Data Neo4j同时还提供了XML命名空间。如果更愿意在XML中配置Spring Data Neo4j的话,那可以使用该命名空间中的<neo4j:config >和<neo4j:repositories >元素:
<?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:neo4j="http://www.springframework.org/schema/data/neo4j" xsi:schemaLocation="http://www.springframework.org/schema/data/neo4j http://www.springframework.org/schema/data/neo4j/spring-neo4j.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<neo4j:config storeDirectory="/tmp/graphdb" base-package="orders" />
<neo4j:repositories base-package="orders.db" />
</beans>
<neo4j:config >元素配置了如何访问数据库的细节。在本例中,它配置Spring Data Neo4j使用嵌入式的数据库。具体来讲,storeDirectory属性指定了数据要持久化到哪个文件系统路径中。base-package属性设置了模型类定义在哪个包中。
至于<neo4j:repositories >元素,它启用Spring Data Neo4j自动生成Repository实现的功能,它会扫描orders.db包,查找所有扩展Repository的接口
如果要配置Spring Data Neo4j访问远程的Neo4j服务器,所需要做的就是声明SpringRestGraphDatabasebean,并设置<neo4j:config >的graphDatabaseService属性:
<neo4j:config base-package="orders" graphDatabaseService="graphDatabaseService" />
<bean id="graphDatabaseService" class="org.springframework.data.neo4j.rest.SpringRestGraphDatabase">
<constructor-arg value="http://graphdbserver:7474/db/data/" />
<constructor-arg value="db.username" />
<constructor-arg value="db.password" />
</bean>
不管是通过Java还是通过XML来配置Spring Data Neo4j,都需要确保模型类位于基础包所指定的包中(通过@EnableNeo4jRepositories的basePackages属性或<neo4j:config >的base-package属性来进行设置)。它们都需要使用注解将其标注为节点实体或关联关系实体。
使用注解标注图实体
Neo4j定义了两种类型的实体:节点(node)和关联关系(relationship)。一般来讲,节点反映了应用中的事物,而关联关系定义了这些事物是如何联系在一起的。
Spring Data Neo4j提供了多个注解,它们可以应用在模型类型及其域上,实现Neo4j中的持久化。
| 注 解 | 描 述 |
|---|---|
| @NodeEntity | 将Java类型声明为节点实体 |
| @RelationshipEntity | 将Java类型声明为关联关系实体 |
| @StartNode | 将某个属性声明为关联关系实体的开始节点 |
| @EndNode | 将某个属性声明为关联关系实体的结束节点 |
| @Fetch | 将实体的属性声明为立即加载 |
| @GraphId | 将某个属性设置为实体的ID域(这个域的类型必须是Long) |
| @GraphProperty | 明确声明某个属性 |
| @GraphTraversal | 声明某个属性会自动提供一个iterable元素,这个元素是图遍历所构建的 |
| @Indexed | 声明某个属性应该被索引 |
| @Labels | 为@NodeEntity声明标签 |
| @Query | 声明某个属性会自动提供一个iterable元素,这个元素是执行给定的Cypher查询所构建的 |
| @QueryResult | 声明某个Java或接口能够持有查询的结果 |
| @RelatedTo | 通过某个属性,声明当前的@NodeEntity与另外一个@NodeEntity之间的关联关系 |
| @RelatedToVia | 在@NodeEntity上声明某个属性,指定其引用该节点所属的某一个@RelationshipEntity |
| @RelationshipType | 将某个域声明为关联实体类型 |
| @ResultColumn | 在带有@QueryResult注解的类型上,将某个属性声明为获取查询结果集中的某个特定列 |
为了了解如何使用其中的某些注解,会将其应用到订单/条目样例中。
在该样例中,数据建模的一种方式就是将订单设定为一个节点,它会与一个或多个条目关联。连接两个节点的简单关联关系,关系本身不包含任何属性
为了将订单指定为节点,我们需要为Order类添加@NodeEntity注解。如下的程序展现了带有@NodeEntity注解的Order类,它还包含了表中的几个其他注解。
package orders;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.NodeEntity;
import org.springframework.data.neo4j.annotation.RelatedTo;
@NodeEntity
public class Order {
@GraphId
private Long id;
private String customer;
private String type;
@RelatedTo(type="HAS_ITEMS")
private Set<Item> items = new LinkedHashSet<Item>();
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(Set<Item> items) {
this.items = items;
}
public Long getId() {
return id;
}
}
除了类级别上的@NodeEntity,还要注意id属性上使用了@GraphId注解。Neo4j上的所有实体必要要有一个图ID。这大致类似于JPA@Entity以及MongoDB @Document类中使用@Id注解的属性。在这里,@GraphId注解标注的属性必须是Long类型。
customer和type属性上没有任何注解。只要这些属性不是瞬态的,它们都会成为数据库中节点的属性。
items属性上使用了@RelatedTo注解,这表明Order与一个Item的Set存在关联关系。type属性实际上就是为关联关系建立了一个文本标记。它可以设置成任意的值,但通常会给定一个易于人类阅读的文本,用来简单描述这个关联关系的特征。稍后,将会看到如何将这个标记用在查询中,实现跨关联关系的查询。
就Item本身来说,下面展现了如何为其添加注解实现图的持久化。
package orders;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.NodeEntity;
@NodeEntity
public class Item {
@GraphId
private Long id;
private Order order;
private String product;
private double price;
private int quantity;
public Order getOrder() {
return 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;
}
public Long getId() {
return id;
}
}
类似于Order,Item也使用了@NodeEntity注解,将其标记为一个节点。它同时也有一个Long类型的属性,借助@GraphId注解将其标注为节点的图ID,而product、price以及quantity属性均会作为图数据库中节点的属性。
Order和Item之间的关联关系很简单,关系本身并不包含任何的数据。因此,@RelatedTo注解就足以定义关联关系。但是,并不是所有的关联关系都这么简单。
重新考虑该如何为数据建模,从而学习如何使用更为复杂的关联关系。在当前的数据模型中,将条目和产品的信息组合到了Item类中。但是,重新考虑的时候,会发现订单会与一个或多个产品相关联。订单与产品之间的关系构成了订单的一个条目。关联关系实体自身具有属性
在这个新的模型中,订单中产品的数量是条目中的一个属性,而产品本身是另外一个概念。与前面一样,订单和产品都是节点,而条目是关联关系。因为现在的条目必须要包含一个数量值,关联关系不像前面那么简单。需要定义一个类来代表条目:
package orders;
import org.springframework.data.neo4j.annotation.EndNode;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.RelationshipEntity;
import org.springframework.data.neo4j.annotation.StartNode;
@RelationshipEntity(type="HAS_LINE_ITEM_FOR")
public class LineItem {
@GraphId
private Long id;
@StartNode
private Order order;
@EndNode
private Product product;
private int quantity;
...
}
Order类通过@NodeEntity注解将其标示为一个节点,而LineItem类则使用了@RelationshipEntity注解。LineItem同样也有一个id属性标注了@GraphId注解,不管是节点实体还是关联关系实体,都必须要有一个图ID,而且其类型必须为Long。
关联关系实体的特殊之处在于它们连接了两个节点。@StartNode和@EndNode注解用在定义关联关系两端的属性上。在本例中,Order是开始节点,Product是结束节点。
最后,LineItem类有一个quantity属性,当关联关系创建的时候,它会持久化到数据库中。
使用Neo4jTemplate
Spring Data MongoDB提供了MongoTemplate实现基于模板的MongoDB持久化,与之类似,Spring Data Neo4j提供了Neo4jTemplate来操作Neo4j图数据库中的节点和关联关系。如果你已经按照前面的方式配置了Spring Data Neo4j,在Spring应用上下文中就已经具备了一个Neo4jTemplatebean。接下来需要做的就是将其注入到任意想使用它的地方。
例如,可以直接将其自动装配到某个bean的属性上:
@Autowired
private Neo4jOperations neo4j;
Neo4jTemplate定义了很多的方法,包括保存节点、删除节点以及创建节点间的关联关系。
借助Neo4jTemplate完成的最基本的一件事情可能就是将某个对象保存为节点。假设这个对象已经使用了@NodeEntity注解,那么可以按照如下的方式来使用save()方法:
Order order = ...;
Order saveOrder = neo4j.save(order);
如果能知道对象的图ID,那么可以通过findOne()方法来获取它:
Order order = neo4j.findOne(42, Order.class);
如果按照给定的ID找不到节点的话,那么findOne()方法将会抛出NotFound(Exception)。
如果想获取给定类型的所有对象,那么可以使用findAll()方法:
EndResult<Order> allOrders = neo4j.findAll(Order.class);
这里返回的EndResult是一个Iterable,它能够用在for-each循环以及任何可以使用Iterable的地方。如果不存在这样的节点的话,findAll()方法将会返回空的Iterable。
如果只是想知道Neo4j数据库中指定类型的对象数量,那么就可以调用count()方法:
long orderCount = count(Order.class);
delete()方法可以用来删除对象:
neo4j.delete(order);
createRelationshipBetween()是Neo4jTemplate所提供的最有意思的方法之一。可以猜到,它会为两个节点创建关联关系。例如,可以在Order节点和Product节点之间建立LineItem关联关系:
Order order = ...;
Product prod = ...;
LineItem lineItem = neo4j.createRelationshipBetween(order, prod, LineItem.class, "HAS_LINE_ITEM_FOR", false);
lineItem.setQuantity(5);
neo4j.save(lineItem);
createRelationshipBetween()方法的前两个参数是关联关系两端的节点对象。接下来的参数指定了使用@RelationshipEntity注解的类型,它会代表这种关系。接下来的String值描述了关联关系的特征。最后的参数是一个boolean值,它表明这两个节点实体之间是否允许存在重复的关联关系。
createRelationshipBetween()会返回关联关系类的一个实例。通过它,我们可以设置任意的属性。上面的示例中设置了quantity属性。当这一切完成后,调用save()方法将关联关系保存到数据库中。
创建自动化的Neo4j Repository
已经将@EnableNeo4jRepositories添加到了配置中,所以Spring Data Neo4j已经配置为支持自动化生成Repository的功能。我们所需要做的就是编写接口,如下的OrderRepository就是很好的起点:
package orders.db;
import orders.Order;
import org.springframework.data.neo4j.repository.GraphRepository;
public interface OrderRepository extends GraphRepository<Order> {}
与其他的Spring Data项目一样,Spring Data Neo4j会为扩展Repository接口的其他接口生成Repository方法实现。在本例中,OrderRepository扩展了GraphRepository,而后者又间接扩展了Repository接口。因此,Spring Data Neo4j将会在运行时创建OrderRepository的实现。
注意,GraphRepository使用Order进行了参数化,也就是这个Repository所要使用的实体类型。因为Neo4j要求图ID的类型为Long,因此在扩展GraphRepository的时候,没有必要再去指定ID类型。
现在,就能够使用很多通用的CRUD操作,这与JpaRepository和MongoRepository所提供的功能类似。下表描述了扩 展GraphRepository所能够得到的方法:
| 方 法 | 描 述 |
|---|---|
| long count(); | 返回在数据库中,目标类型有多少实体 |
| void delete(Iterable<? extends T>); | 删除多个实体 |
| void delete(Long id); | 根据ID,删除一个实体 |
| void delete(T); | 删除一个实体 |
| void deleteAll(); | 删除目标类型的所有实体 |
| boolean exists(Long id); | 根据指定的ID,检查实体是否存在 |
| EndResult findAll(); | 获取目标类型的所有实体 |
| Iterable findAll(Iterable); | 根据给定的ID,获取目标类型的实体 |
| Page findAll(Pageable); | 返回目标类型分页和排序后的实体列表 |
| EndResult findAll(Sort); | 返回目标类型排序后的实体列表 |
| EndResult findAllBySchemaPropertyValue(String,Object); | 返回指定属性匹配给定值的所有实体 |
| Iterable findAllByTraversal(N, TraversalDescription); | 返回从某个节点开始,图遍历到达的节点 |
| T findBySchemaPropertyValue (String,Object); | 返回指定属性匹配给定值的一个实体 |
| T findOne(Long); | 根据ID,获得某一个实体 |
| EndResult query(String, Map<String, Object>); | 返回匹配给定Cypher查询的所有实体 |
| Iterable save(Iterable); | 保存多个实体 |
| S save(S); | 保存一个实体 |
例如,如下的代码能够保存一个Order实体:
Order saveOrder = orderRepository.save(order);
当实体保存之后,save()方法将会返回被保存的实体,如果之前它使用@GraphId注解的属性值为null的话,此时这个属性将会填充上值。
还可以使用findOne()方法查询某一个实体。例如,下面的这行代码将会查询图ID为4的Order:
Order order = orderRepository.findOne(4L);
可以查询所有的Order:
EndResult<Order> allOrders = orderRepository.findAll();
还希望删除某一个实体。这种情况下,可以使用delete()方法:
delete(order);
这将会从数据库中删除给定的Order节点。如果你只有图ID的话,那可以将其传递到delete()方法中,而不是再使用节点类型本身:
delete(orderId);
添加查询方法
package orders.db;
import java.util.List;
import orders.Order;
import org.springframework.data.neo4j.repository.GraphRepository;
public interface OrderRepository extends GraphRepository<Order> {
List<Order> findByCustomer(String customer);
List<Order> findByCustomerAndType(String customer, String type);
}
这里,添加了两个方法。其中一个会查询customer属性等于给定String值的Order节点。另外一个方法与之类似,但是除了匹配customer属性以外,Order节点的type属性必须还要等于给定的类型值。
指定自定义查询
当命名约定无法满足需求时,还可以为方法添加@Query注解,为其指定自定义的查询。之前已经见过@Query注解。在Spring Data JPA中,使用它来为Repository方法指定JPA查询。在Spring Data MongoDB中,使用它来指定匹配JSON的查询。但是,在使用Spring Data Neo4j的时候,必须指定Cypher查询:
@Query("match (o:Order)-[:HAS_ITEMS]->(i:Item) " + "where i.product='Spring in Action' return o")
List<Order> findSiAOrders();
在这里,findSiAOrders()方法上使用了@Query注解,并设置了一个Cypher查询,它会查找与Item关联并且product属性等于“Spring in Action”的所有Order节点。
混合自定义的Repository行为
例如,假设想自己编写findSiAOrders()方法的实现,而不是依赖于@Query注解。那么可以首先定义一个中间接口,该接口包含findSiAOrders()方法的定义:
package orders.db;
import java.util.List;
import orders.Order;
public interface OrderOperations {
List<Order> findSiAOrders();
}
然后,修改OrderRepository,让它扩展OrderOperations和GraphRepository:
public interface OrderRepository extends GraphRepository<Order>, OrderOperations {
...
}
最后,需要自己编写实现。与Spring Data JPA和Spring Data MongoDB类似,Spring Data Neo4j将会查找名字与Repository接口相同且添加“Impl”后缀的实现类。因此,需要创建OrderRepositoryImpl类。如下的程序展示了OrderRepositoryImpl类,它实现了findSiAOrders()方法。
package orders.db;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import orders.Order;
import org.neo4j.helpers.collection.IteratorUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.conversion.EndResult;
import org.springframework.data.neo4j.conversion.Result;
import org.springframework.data.neo4j.template.Neo4jOperations;
public class OrderRepositoryImpl implements OrderOperations {
private final Neo4jOperations neo4j;
@Autowired
public OrderRepositoryImpl(Neo4jOperations neo4j) {
this.neo4j = neo4j;
}
public List<Order> findSiAOrders() {
Result<Map<String, Object>> result = neo4j.query("match (o:Order)-[:HAS_ITEMS]->(i:Item) " + "where i.product='Spring in Action' return o");
EndResult<Order> endResult = result.to(Order.class);
return IteratorUtil.asList(endResult);
}
}
OrderRepositoryImpl中注入了一个Neo4jOperations(具体来讲,就是Neo4jTemplate的实例),它会用来查询数据库。因为query()方法返回的是Result<Map<String, Object>>,需要将其转换为List。第一步是调用Result的to()方法,产生一个EndResult。然后,使用Neo4j的IteratorUtil.asList()方法将EndResult转换为List,然后将其返回。
使用Redis操作key-value数据
连接到Redis
Redis连接工厂会生成到Redis数据库服务器的连接。Spring Data Redis为四种Redis客户端实现提供了连接工厂:
- JedisConnectionFactory
- JredisConnectionFactory
- LettuceConnectionFactory
- SrpConnectionFactory
可以将连接工厂配置为Spring中的bean。例如,如下展示了如何配置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;
}
在上面的这些例子中,假设使用的是JedisConnectionFactory。如果选择使用其他连接工厂的话,只需进行简单地替换就可以了。例如,假设要使用LettuceConnectionFactory的话,可以按照如下的方式进行配置:
@Bean
public RedisConnectionFactory redisCF() {
JedisConnectionFactory cf = new LettuceConnectionFactory();
cf.setHostName("redis-server");
cf.setPort(7379);
cf.setPassword("foobared");
return cf;
}
所有的Redis连接工厂都具有setHostName()、setPort()和setPassword()方法。这样,它们在配置方面实际上是相同的。
使用RedisTemplate
顾名思义,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项目类似,Spring Data Redis以模板的形式提供了较高等级的数据访问方案。实际上,Spring Data Redis提供了两个模板:
- RedisTemplate
- StringRedisTemplate
RedisTemplate可以极大地简化Redis数据访问,能够持久化各种类型的key和value,并不局限于字节数组。在认识到key和value通常是String类型之后,StringRedisTemplate扩展了RedisTemplate,只关注String类型。
假设已经有了RedisConnectionFactory,那么可以按照如下的方式构建RedisTemplate:
RedisConnectionFactory cd = ...;
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 cd = ...;
StringRedisTemplate redis = new StringRedisTemplate();
注意,与RedisTemplate不同,StringRedisTemplate有一个接受RedisConnectionFactory的构造器,因此没有必要在构建后再调用setConnectionFactory()。
尽管这并非必须的,但是如果经常使用RedisTemplate或StringRedisTemplate的话,你可以考虑将其配置为bean,然后注入到需要的地方。如下就是一个声明RedisTemplate的简单@Bean方法:
@Bean
public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
redis.setConnectionFactory(cf);
return redis;
}
如下是声明StringRedisTemplate bean的@Bean方法:
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory cf) {
return new StringRedisTemplate(cf);
}
有了RedisTemplate(或StringRedisTemplate)之后,就可以开始保存、获取以及删除key-value条目了。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值的条目 |
| boundZSet(K) | BoundZSetOperations<K, V> | 以绑定指定key的方式,操作具有ZSet值(排序的set)的条目 |
| boundHashOps(K) | BoundHashOperations<K, V> | 以绑定指定key的方式,操作具有hash值的条目 |
可以看到,上表中的子API能够通过RedisTemplate(和StringRedis-Template)进行调用。其中每个子API都提供了使用数据条目的操作,基于value中所包含的是单个值还是一个值的集合它们会有所差别。
使用简单的值
假设想通过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类型的value与之类似,只需使用opsForList()方法即可。例如,可以在一个List类型的条目尾部添加一个值:
redis.opsForList().rightPush("cart", product);
通过这种方式,向列表的尾部添加了一个Product,所使用的这个列表在存储时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。最为常用的操作就是向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");
绑定到某个key上
假设将Product对象保存到一个list中,并且key为cart。在这种场景下,假设我们想从list的右侧弹出一个元素,然后在list的尾部新增三个元素。此时可以使用boundListOps()方法所返回的BoundListOperations:
BoundListOpertions<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:使用Spring转换服务进行序列化
- 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默认会使用StringRedis-Serializer,这在预料之中,它实际上就是实现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。