spring实战 使用NoSQL数据库

636 阅读34分钟

使用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。