一、使用MongoDB持久化文档数据
有一些数据的最佳表现形式是文档,也就是说,不要把这些数据分散到多个表、节点或实体中,将这些信息收集到一个非规范化(也就是文档)的结构中更有意义。如果数据之间有明显的关联关系,文档数据库就不太适合了。
我们在一个购物订单系统中学习MongoDB。接下来我们要配置Spring Data MongoDB
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(basePackages = "orders.db")
public class MongoConfig extends AbstractMongoConfiguration {
@Override
public Mongo mongo() throws Exception {
return new MongoClient();
}
@Override
protected String getDatabaseName() {
return "OrdersDB";
}
}
通过@EnableMongoRepository注解启用Spring Data 的自动化MongoDB repository生成功能,我们让配置类扩展AbstractMongoConfiguration并重载mongo()方法,getDatabaseName()方法,mongo()方法会直接返回一个MongoClient实例而不是声明MongoFactory bean 和MongoTemplate bean。使用MongoClient更加简单。这里需要一个运行在本地的MongoDB 服务器,MongoClient监听的默认的端口27017,如果需要更改端口在MongoClient构造方法中指定端口即可。
下面是Order类
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;
@Field("customer")
private String customer;
private String type;
private Collection<Item> items = new LinkedHashSet<>();
public String getId() {
return id;
}
public String getCustomer() {
return customer;
}
public void setCustomer(String customer) {
this.customer = customer;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Collection<Item> getItems() {
return items;
}
public void setItems(Collection<Item> items) {
this.items = items;
}
}
我们可以看到Order类使用了@Document注解,这样它就能借助MongoTemplate或自动生成的Repository进行持久化,其id属性上使用了@Id注解用来指定它作为文档的ID,除此之外,customer属性上使用了@Field注解,这样在文档持久化的时候customer属性会映射为customer的域。同时items属性它指的是订单中具体条目的集合,在传统的关系型数据库中,这些条目会在另一个数据表中,通过外键进行引用,items域上可能还会有使用JPA的@OneToMany注解。
下面是Item类,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 Order getOrder() {
return order;
}
public String getProduct() {
return product;
}
public double getPrice() {
return price;
}
public int getQuantity() {
return quantity;
}
public void setProduct(String product) {
this.product = product;
}
public void setPrice(double price) {
this.price = price;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}
我们定义OrderRepository扩展MongoRepository,它和Spring Data JPA一样,扩展了Repository的接口将会在运行时自动生成实现。
package orders.db;
import orders.Order;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.util.List;
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> getByType(String type);
@Query("{customer:‘Chuck Wagon‘}")
List<Order> findChucksOrders();
}
下面是测试类
package orders;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import orders.config.MongoConfig;
import orders.db.OrderRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@ContextConfiguration(classes=MongoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class MongoDbTest {
@Autowired
private OrderRepository orderRepository;
@Autowired MongoOperations mongoOps;
@Before
public void cleanup() {
// Deleting all orders (just in case something is left over from a previous failed run)
orderRepository.deleteAll();
}
@Test
public void testMongoRepository() {
assertEquals(0, orderRepository.count());
Order order = createAnOrder();
// Saving an order
Order savedOrder = orderRepository.save(order);
assertEquals(1, orderRepository.count());
// Finding an order by ID
Order foundOrder = orderRepository.findOne(savedOrder.getId());
assertEquals("Chuck Wagon", foundOrder.getCustomer());
assertEquals(2, foundOrder.getItems().size());
// Finding an order by a single field value
List<Order> chucksOrders = orderRepository.findByCustomer("Chuck Wagon");
assertEquals(1, chucksOrders.size());
assertEquals("Chuck Wagon", chucksOrders.get(0).getCustomer());
assertEquals(2, chucksOrders.get(0).getItems().size());
// Finding an order by a single field value like
List<Order> chuckLikeOrders = orderRepository.findByCustomerLike("Chuck");
assertEquals(1, chuckLikeOrders.size());
assertEquals("Chuck Wagon", chuckLikeOrders.get(0).getCustomer());
assertEquals(2, chuckLikeOrders.get(0).getItems().size());
// Finding an order by multiple field values
List<Order> chucksWebOrders = orderRepository.findByCustomerAndType("Chuck Wagon", "WEB");
assertEquals(1, chucksWebOrders.size());
assertEquals("Chuck Wagon", chucksWebOrders.get(0).getCustomer());
assertEquals(2, chucksWebOrders.get(0).getItems().size());
List<Order> chucksPhoneOrders = orderRepository.findByCustomerAndType("Chuck Wagon", "PHONE");
assertEquals(0, chucksPhoneOrders.size());
// Finding an order by a custom query method
List<Order> chucksOrders2 = orderRepository.findChucksOrders();
assertEquals(1, chucksOrders2.size());
assertEquals("Chuck Wagon", chucksOrders2.get(0).getCustomer());
assertEquals(2, chucksOrders2.get(0).getItems().size());
// Deleting an order
orderRepository.delete(savedOrder.getId());
assertEquals(0, orderRepository.count());
}
private Order createAnOrder() {
Order order = new Order();
order.setCustomer("Chuck Wagon");
order.setType("WEB");
Item item1 = new Item();
item1.setProduct("Spring in Action");
item1.setQuantity(2);
item1.setPrice(29.99);
Item item2 = new Item();
item2.setProduct("Module Java");
item2.setQuantity(31);
item2.setPrice(29.95);
order.setItems(Arrays.asList(item1, item2));
return order;
}
}
二、使用Neo4j操作位图数据
文档型数据库会将数据存储到粗粒度的文档中,图数据库会将数据存储到多个细粒度的节点中,这些节点之间通过关系建立关联。Spring Data Neo4j提供了将Java对象映射到节点和关联关系的注解、面向模板的Neo4j访问方式以及Repository‘实现的自动化生成功能。
配置Spring Data Neo4j的关键在于声明GraphDatabaseService bean 和启用Neo4j Repository自动生成功能。
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实现。定义GraphDatabaseService bean使用GraphDatabaseFactory 来创建嵌入式的Neo4j数据库。
Neo4j定义了两种类型的实体:节点和关联关系。节点反映了应用中的事物,而关联关系定义了这些事物是如何联系在一起的。
Order、Item是两个节点他们之间的关系是Order has items
下面我们为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注解代表这是一个节点,@GraphId注解代表这是一个图Id,@RelatedTo(type="HAS_ITEMS")注解代表Order与一个Item的Set存在关系。
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;
}
}
同样我们需要创建自动化的Neo4j Repository
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> findByCustomerLike(String customer);
List<Order> findByCustomerAndType(String customer, String type);
List<Order> getByType(String type);
// @Query("{customer:‘Chuck Wagon‘}")
// List<Order> findChucksOrders();
}
下面是测试类
package orders;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import orders.config.Neo4jConfig;
import orders.db.OrderRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@ContextConfiguration(classes=Neo4jConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Neo4jTest {
@Autowired
private OrderRepository orderRepository;
@Before
public void cleanup() {
// Deleting all orders (just in case something is left over from a previous failed run)
orderRepository.deleteAll();
}
@Test
public void testNeo4jRepository() {
assertEquals(0, orderRepository.count());
Order order = createAnOrder();
// Saving an order
Order savedOrder = orderRepository.save(order);
assertEquals(1, orderRepository.count());
// Finding an order by ID
Order foundOrder = orderRepository.findOne(savedOrder.getId());
assertEquals("Chuck Wagon", foundOrder.getCustomer());
assertEquals(2, foundOrder.getItems().size());
// Finding an order by a single field value
List<Order> chucksOrders = orderRepository.findByCustomer("Chuck Wagon");
assertEquals(1, chucksOrders.size());
assertEquals("Chuck Wagon", chucksOrders.get(0).getCustomer());
assertEquals(2, chucksOrders.get(0).getItems().size());
// Finding an order by a single field value like
List<Order> chuckLikeOrders = orderRepository.findByCustomerLike("Chuck.*");
assertEquals(1, chuckLikeOrders.size());
assertEquals("Chuck Wagon", chuckLikeOrders.get(0).getCustomer());
assertEquals(2, chuckLikeOrders.get(0).getItems().size());
// Finding an order by multiple field values
List<Order> chucksWebOrders = orderRepository.findByCustomerAndType("Chuck Wagon", "WEB");
assertEquals(1, chucksWebOrders.size());
assertEquals("Chuck Wagon", chucksWebOrders.get(0).getCustomer());
assertEquals(2, chucksWebOrders.get(0).getItems().size());
List<Order> chucksPhoneOrders = orderRepository.findByCustomerAndType("Chuck Wagon", "PHONE");
assertEquals(0, chucksPhoneOrders.size());
// Finding an order by a custom query method
// List<Order> chucksOrders2 = orderRepository.findChucksOrders();
// assertEquals(1, chucksOrders2.size());
// assertEquals("Chuck Wagon", chucksOrders2.get(0).getCustomer());
// assertEquals(2, chucksOrders2.get(0).getItems().size());
// Deleting an order
orderRepository.delete(savedOrder.getId());
assertEquals(0, orderRepository.count());
}
private Order createAnOrder() {
Order order = new Order();
order.setCustomer("Chuck Wagon");
order.setType("WEB");
Item item1 = new Item();
item1.setProduct("Spring in Action");
item1.setQuantity(2);
item1.setPrice(29.99);
Item item2 = new Item();
item2.setProduct("Module Java");
item2.setQuantity(31);
item2.setPrice(29.95);
order.setItems(new HashSet<Item>(Arrays.asList(item1, item2)));
return order;
}
}
三、使用Redis操作键值对数据
Redis是一种特殊类型的数据库,它被称之为key-value存储,key-value存储与哈希Map有很大的相似性,可以把它理解为持久化的哈希Map。
连接Redis
package cart;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
public class RedisConfig {
@Bean
public RedisConnectionFactory redisCF() {
return new JedisConnectionFactory();
}
@Bean
public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
redis.setConnectionFactory(cf);
return redis;
}
}
我们配置RedisConnectionFactory bean 通过默认构造器创建的连接工厂会向localhost上的6379端口创建连接。顾名思义,Redis连接工厂会生成到Redis key-value存储的连接,我们使用RedisTemplate简化数据访问,能够让我们持久化各种类型的key和value。
将RedisConnectionFactory bean 注入到 RedisTemlate中,RedisTemplate使用两个类型进行参数化,第一个是key的类型,第二个是value的类型。
下面是Product类,实现了Serializable接口,因为Redis存储对象时需要将其序列化,取出时需要反序列化。
package cart;
import java.io.Serializable;
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
private String sku;
private String name;
private float price;
public String getSku() {
return sku;
}
public void setSku(String sku) {
this.sku = sku;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
}
下面是测试类的一部分
package cart;
import static org.junit.Assert.*;
import java.util.List;
import java.util.Set;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=RedisConfig.class)
public class CartTest {
/*
* IMPORTANT: This test class requires that a Redis server be running on
* localhost and listening on port 6379 (the default port).
*/
@Autowired
private RedisConnectionFactory cf;
@Autowired
private RedisTemplate<String, Product> redis;
@After
public void cleanUp() {
redis.delete("9781617291203");
redis.delete("cart");
redis.delete("cart1");
redis.delete("cart2");
}
@Test
public void workingWithSimpleValues() {
Product product = new Product();
product.setSku("9781617291203");
product.setName("Spring in Action");
product.setPrice(39.99f);
redis.opsForValue().set(product.getSku(), product);
Product found = redis.opsForValue().get(product.getSku());
assertEquals(product.getSku(), found.getSku());
assertEquals(product.getName(), found.getName());
assertEquals(product.getPrice(), found.getPrice(), 0.005);
}
}