小时候,我们骑车就能获得快乐。后来随着慢慢长大,我们需要的自行车就能满足了,我们可能想要装载一些生活用品,或者接送孩子,或者要去很远的地方。我们的需求超出了自行车的功能范围。
在持久化的世界里,JDBC就像是自行车。对于分内的工作,它能很好的完成。然而,随着应用越来越复杂。我们可能需要将对象属性映射到数据库的列上,我们可能需要自动生成语句和查询,我们可能需要一些更复杂的特性,譬如:
- 延迟加载(Lazy loading):比如我们在查询一个对象A的时候,这个对象的属性包括了普通的String/int等等,也包括了其他对象B,我们这时候可能不太需要B,然而B的开销不容小觑,所以延迟加载允许我们在需要的时候才开销。
- 预先抓取(Eager fetching):这与延迟加载相对。假设我们需要对象A及其关联的对象B,预先抓去功能可以在一个操作中将他们全部从数据库里抓取出来,节省了多次查询的成本。
- 级联(Cascading):有时候更改数据库会同时修改其他关联的表,当我们删除对象A我们可能也希望删除关联的对象B。
一些可用的框架提供了这样的服务,这些服务的通用名称是ORM(object-relational mapping,对象-关系映射)。在持久层使用ORM工具,可以节省数千行的代码和大量的开发时间。
在Spring中集成Hibernate
我们只关注Spring和Hibernate的集成,而不会涉及太多Hibernate使用时的复杂细节。
声明Hibernate的Session工厂
Hibernate所需的主要接口是org.hibernate.Session。这个Session接口提供了基本的数据访问功能,如更新、保存、删除以及从数据库加载对象的功能。通过Session接口,应用程序的Repository能满足所有的持久化需求。
获取Session对象的标准方式是通过工厂方法:SessionFactory接口的实现类。除了一些其他的任务,SessionFactory主要负责Hibernate Session的打开、关闭以及管理。
从Hibernate3.1版本开始,Spring提供了三个SessionFactory bean:
这些SessionFactory的选择取决于使用哪个版本的Hibernate以及是使用XML还是使用注解来定义对象-数据库之间的映射关系,如果使用Hibernate3.2或者更高版本(直到Hibernate4.0,但不包括4.0)并且使用XML定义映射,那么,你需要定义Spring的org.springframework.orm.hibernate3包中的LocalSessionFacrotyBean:
package spittr.config;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.hibernate3.LocalSessionFactoryBean;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
public class DataSoueceConfiguration {
@Bean
public DataSource Data(){
BasicDataSource ds=new BasicDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost:8080/promusician");
ds.setUsername("root");
ds.setPassword("root");
ds.setInitialSize(5);
ds.setMaxActive(10);
return ds;
}
@Bean
public LocalSessionFactoryBean sessionFactory(DataSource dataSource){
LocalSessionFactoryBean sessionFactoryBean=new LocalSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
sessionFactoryBean.setMappingResources(new String[]{"Spitter.hbm.xml"});
Properties properties=new Properties();
properties.setProperty("dialect","org.hibernate.dialect.H2Dialect");
sessionFactoryBean.setHibernateProperties(properties);
return sessionFactoryBean;
}
}
这里使用dataSource属性装配了一个DataSource bean的引用。mappingResources属性列出了一个或多个Hibernate映射文件,最后hibernateProperties属性配置了Hibernate如何操作的细节。本例中我们配置Hibernate使用H2数据库并且按照H2Dialect来构建SQL。
如果你倾向于使用注解的方式来定义持久化,而且并不使用Hibernate4:
@Bean
public AnnotationSessionFactoryBean sessionFactory(DataSource dataSource){
AnnotationSessionFactoryBean sessionFactoryBean=new AnnotationSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
sessionFactoryBean.setPackagesToScan(new String[]{"spittr.domain"});
Properties properties=new Properties();
properties.setProperty("dialect","org.hibernate.dialect.H2Dialect");
sessionFactoryBean.setHibernateProperties(properties);
return sessionFactoryBean;
}
可以观察出,他只改变了一个方法。就是通过java配置而不是xml配置。packageToScan属性告诉Spring扫描一个或多个包来查找域类,这些类通过注解的方式来表明要使用Hibernate进行持久化,这些类可以使用的注解包括JPA的@Entity或@MappedSuperclasses以及Hibernate的@Entity
或者使用Hibernate4的LocalSessionFactoryBean
它支持两种方式的配置。也就是把上面的类名改改就行。
构建不依赖与Spring的Hibernate代码
使用上下文Session(Contextual Session)会直接将Hibernate SessionFactory装配到Repository中,并使用它来获取Session:
@Repository
public class HibernateSpitterRepository implements SpitterRepository{
private SessionFactory sessionFactory;
@Inject
public HibernateSpitterRepository(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
private Session currentSession() {
return sessionFactory.getCurrentSession();
}
public long count() {
return findAll().size();
}
public Spitter save(Spitter spitter) {
Serializable id = currentSession().save(spitter);
return new Spitter((Long) id,
spitter.getUsername(),
spitter.getPassword(),
spitter.getFullName(),
spitter.getEmail(),
spitter.isUpdateByEmail());
}
public Spitter findOne(long id) {
return (Spitter) currentSession().get(Spitter.class, id);
}
...
}
这个程序有几个地方需要注意。首先,我们通过@Inject
注解让Spring自动将一个SessionFactory注入到HibernateSpitterRepository的sessionFactory中。接下来的currentSession()
方法中,我们使用sessionFactory获得当前事务的Session。
@Repository
用于声明Component。
我们使用Session,那异常抓换怎么处理呢?为了添加异常转换功能,我们只需要在Spring的应用上下文中添加一个bean:
@Bean
public BeanPostProcessor persistenceTranslation(){
return new PersistenceExceptionTranslationPostProcessor();
}
这个bean会在所有拥有@Repository注解的类上添加一个通知其,这样就会捕捉任何平台相关的异常,并且以Spirng非检查型数据访问异常的形式抛出。
使用Redis
Redis是一种特殊类型的数据库,它被称之为key-value存储。顾名思义,key-value存储保存的是键值对。实际上key-value存储与哈希Map有很大的相似性。可以不太夸张的说,它就是持久化的Hash Map。
当然Redis是一种NoSQL,所以这里不需要使用并配置DataSource。
链接到Redis
Redis提供了四种连接工厂:
具体使用哪种,本书不做讲解。
我们可以将连接工厂配置为Spring中的bean。例如:
@Bean
public RedisConnectionFactory redisCF() {
return new JedisConnectionFactory();
}
这种默认构造器会想localhost的6379端口创建连接,并且没有密码,如果你有额外需求你可以使用:
@Bean
public RedisConnectionFactory redisCF() {
JedisConnectionFactory cf= new JedisConnectionFactory();
cf.setHostName("redis-server");
cf.setPort(7379);
cf.setPassword("foobared");
return cf;
}
当然,我们这里的显式配置都很简单,实际工程项目还复杂的多,这里只做最基本的介绍。
使用RedisTemplate
Redis Connection Factory会生成一个RedisConnection。借助这个RedisConnection,可以存储和读取数据。比如,我们可以使用这个RedisConnection存储一个信息:
//假设已经注入了一个工厂类ConnectionFactory
RedisConnection conn=rediusConnectitonFactory.getConnection();
conn.set("greeting".getBytes(),"Hello World".getBytes());//键值对的形式存储。
毫无疑问,这可以正常运行,但是你愿意使用字节数组来存储信息?
当然,Spring Data Redis以模版的形式提供了较高级的数据访问方案。实际上,它提供了两个模版:
- RedisTemplate
- StringRedisTemplate
RedisTemplate可以极大简化Redis的数据访问,持久化各种类型的key-value,而不仅仅是字节数组。但在认识到key-value通常是String类型的后,StringRedisTemplate又扩展了RedisTemplate,只关注String类型。
我们这样构建RedisTemplate:
@Bean
public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
redis.setConnectionFactory(cf);
return redis;
}
这样构建StringRedisTemplate:
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory cf){
return new StringRedisTemplate(cf);
}
有了RedisTemplate之后,我们就可以开始保存获取以及删除键值对了。
假设我们想通过RedisTemplate<String,Product>
保存Product,其中key是num属性的值,如下代码展示了如何借助opsForValue()
来完成该功能:
redis.opsForValue().set(product.getNum(),product);//存储num-对象的键值对
如果想获得该对象Product,且num=123456:
Product product=redis.opsForValue().get("123456");
那我们使用List类型的value与之类似,只需要使用opsForList()
即可:(假设我们希望在List类型的条目尾部添加一个值)
redis.opsForList().rightPush("cart",product);
使用rightPush()
在列的尾部添加了一个Product,所使用这个列表在存储时,key为cart。如果这个key尚未存在,将会创建一个。同理leftPush()
会在头部添加。
我们也有方法获取单个元素:
Product first=redis.opsForList().leftPop("cart");
Product last=redis.opsForList().rightPop("cart");
这两个方法有个副作用是获取的同时,也出队列了。我们如果只想获取值的话,甚至可能是在中间获取,那么我们可以使用range()
方法:
List<Product> products=redis.opsForList().range("cart",2,12);
除了操作列表外,我们还可以使用opsForSet()操作Set,最为常用的操作就是Set中添加一个元素:
redis.opsForSet().add("cart",product);
在我们有多个Set并填充值之后,我们就可以对这些Set进行一些操作,比如获取其差异,求交集,求并集:
List<Product> diff=redis.opsForSet().difference("cart","cart1");
List<Product> union=redis.opsForSet().union("cart","cart1");
List<Product> isect=redis.opsForSet().isect("cart","cart1");
当然还可以移除它的元素:
redis.opsForSet().remove(product);
使用key和value的序列化器
Redis会将key-value用序列化器进行序列化。Spring Data Redis提供了多种序列化器,我们这里暂时不做介绍。