10.1 Spring的数据访问哲学
为了避免应用与特定的数据访问策略耦合在一起,编写良好的 Repository应该以接口的方式暴露功能。图10.1展现了设计数据访问层 的合理方式。
服务对象通过接口来访问Repository。这样做会有几个好处。第一,它使得服务对象易于测试,因为它们不再与特定的数据访问实现绑定在一起。实际上,你可以为这些数据访问接口创建mock实现,这样无需连接数据库就能测试服务对象,而且会显著提升单元测 试的效率并排除因数据不一致所造成的测试失败。
此外,数据访问层是以持久化技术无关的方式来进行访问的。持久化 方式的选择独立于Repository,同时只有数据访问相关的方法才通过接口进行暴露。这可以实现灵活的设计,并且切换持久化框架对应用 程序其他部分所带来的影响最小。如果将数据访问层的实现细节渗透到应用程序的其他部分中,那么整个应用程序将与数据访问层耦合在 一起,从而导致僵化的设计。
Spring将数据访问过程中固定的和可变的部分明确划分为两个不同的类:模板(template)和回调(callback)。模板管理过程中固定的部 分,而回调处理自定义的数据访问代码。图10.2展现了这两个类的职 责。
图10.2 Spring的数据访问模板类负责通用的数据访问功能。对于应用程序 特定的任务,则会调用自定义的回调对象
如图所示,Spring的模板类处理数据访问的固定部分——事务控制、 管理资源以及处理异常。同时,应用程序相关的数据访问——语句、 绑定参数以及整理结果集——在回调的实现中处理。事实证明,这是一个优雅的架构,因为你只需关心自己的数据访问逻辑即可。
针对不同的持久化平台,Spring提供了多个可选的模板。如果直接使 用JDBC,那你可以选择JdbcTemplate。如果你希望使用对象关系 映射框架,那HibernateTemplate或JpaTemplate可能会更适合 你。表10.2列出了Spring所提供的所有数据访问模板及其用途。
表10.2 Spring提供的数据访问模板,分别适用于不同的持久化机制
10.2 配置数据源
Spring提供了在Spring上下文中配置数据源bean的多种方式,包括:
(1)通过JDBC驱动程序定义的数据源;
(2)通过JNDI查找的数据源;
(3)连接池的数据源。
对于即将发布到生产环境中的应用程序,作者建议使用从连接池获取连接的数据源。如果可能的话,倾向于通过应用服务器的JNDI来获取数据源。
10.2.1 使用JNDI数据源
Spring应用程序经常部署在Java EE应用服务器中,如WebSphere、 JBoss或甚至像Tomcat这样的Web容器中。这些服务器允许你配置通过 JNDI获取数据源。这种配置的好处在于数据源完全可以在应用程序之 外进行管理,这样应用程序只需在访问数据库的时候查找数据源就可以了。另外,在应用服务器中管理的数据源通常以池的方式组织,从而具备更好的性能,并且还支持系统管理员对其进行热切换。
(1) 首先修改tomcat的server.xml文件,位置: tomcat目录\conf\server.xml,在<GlobalNamingResources>结点下添加如下:
<!--配置MySQL数据库的JNDI数据源-->
<!--
|- name:表示以后要查找的名称。通过此名称可以找到DataSource,此名称任意更换,但是程序中最终要查找的就是此名称,
为了不与其他的名称混淆,所以使用jdbc/oracle,现在配置的是一个jdbc的关于oracle的命名服务。
|- auth:由容器进行授权及管理,指的用户名和密码是否可以在容器上生效
|- type:此名称所代表的类型,现在为javax.sql.DataSource
|- maxActive:表示一个数据库在此服务器上所能打开的最大连接数
|- maxIdle:表示一个数据库在此服务器上维持的最小连接数
|- maxWait:最大等待时间。10000毫秒
|- username:数据库连接的用户名
|- password:数据库连接的密码
|- driverClassName:数据库连接的驱动程序
|- url:数据库连接的地址
-->
<Resource name="jndi/spittrDB"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/spittr?characterEncoding=UTF-8"
username="root"
password="root"
maxActive="20"
maxIdle="10"
maxWait="10000"/>
这是一个全局的配置,这时如果每个具体的context(webapp)中如果要引用这个resource,则需要在各个context对象中配置 resourcelink,然后在各个app的web.xml中配置<resource-ref>.
(2)在conf/context.xml里添加引用:
<!-- global\name属性对应server.xml Resource的name属性-->
<ResourceLink global="jndi/spittrDB"
name="jndi/spittrDB"
auth="Container"
type="javax.sql.DataSource" />
(3)使用Java配置的话,那可以借助 JndiObjectFactoryBean从JNDI中查找DataSource:
@Bean
@Profile("jndi")
public DataSourceInitializer dataSourceInitializer(DataSource dataSource) {
System.out.println("生产环境初始化数据表");
DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
dataSourceInitializer.setDataSource(dataSource);
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
//加载SQL文件
databasePopulator.addScript(new ClassPathResource("spittr/data/schema_mysql.sql"));
databasePopulator.addScript(new ClassPathResource("spittr/data/security_mysql.sql"));
dataSourceInitializer.setDatabasePopulator(databasePopulator);
dataSourceInitializer.setEnabled(true);
return dataSourceInitializer;
}
@Bean(destroyMethod="")
@Profile("jndi")
public JndiObjectFactoryBean jndiDataSource() throws IllegalArgumentException, NamingException {
JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
//jndiObjectFB.setJndiName("jndi/spittrDB");
jndiObjectFB.setJndiName("java:comp/env/jndi/spittrDB");
jndiObjectFB.setResourceRef(true);
jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
return jndiObjectFB;
}
JndiObjectFactoryBean是个工厂类,spring会自动调用JndiObjectFactoryBean的afterPropertiesSet()方法去造真正需要的bean,然后调用getObject()和getObjectType()方法返回已造好的datasource,再将其准确的注入依赖它的其他bean里面。这是最好的方式。
当然,也可以直接使用jndiObjectFB.getObject(),如下:
@Bean(destroyMethod="")
@Profile("jndi")
public DataSource jndiDataSource() throws IllegalArgumentException, NamingException {
JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
jndiObjectFB.setJndiName("java:comp/env/jndi/spittrDB");
jndiObjectFB.setResourceRef(true);
jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
jndiObjectFB.afterPropertiesSet();
return (DataSource) jndiObjectFB.getObject();
}
注意,在使用jndiObjectFB.getObject()之前,必须调用jndiObjectFB.afterPropertiesSet(),否则dataSource会为空。
10.2.2 使用数据源连接池
尽管Spring并没有提供数据源连接池实现,但是我们有多项可用的方案,包括如下开源的实现:
- Apache Commons DBCP (http://jakarta.apache.org/commons/dbcp);
- c3p0 (http://sourceforge.net/projects/c3p0/) ;
- BoneCP (http://jolbox.com/) 。
这些连接池中的大多数都能配置为Spring的数据源,在一定程度上与 Spring自带的DriverManagerDataSource 或SingleConnectionDataSource很类似。
以DBCP为例,使用MySQL数据库,配置DBCP BasicDataSource的XML方式:
<bean id="dataSource" class="org,apache.commons.dbcp.basicDataSource"
p:driverClassName="com.mysql.jdbc.Driver"
P:url="jdbc:mysql://localhost:3306/spittr?characterEncoding=UTF-8"
p:uasername="root"
p:initialSize="5"
p:password="root"
p:maxActive="10"/>
前四个属性是配置BasicDataSource所必需的。属性driverClassName指定了JDBC驱动类的全限定类名。在这里配置的是MySQL数据库的数据源。属性url用于设置数据库的JDBC URL。最后,username和password用于在连接数据库时进行认证。
如果你喜欢Java配置的话,连接池形式的DataSourcebean可以声明如下:
@Bean
public DataSource mysqlDataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spittr?characterEncoding=UTF-8");
ds.setUsername("root");
ds.setPassword("");
ds.setInitialSize(5);
ds.setMaxActive(10);
return ds;
}
表10.3列出了DBCP BasicDataSource最有用的一些池配置属性:
表10.3 BasicDataSource的池配置属性
在我们的示例中,连接池启动时会创建5个连接;当需要的时候,允许BasicDataSource创建新的连接,但最大活跃连接数为10。
10.2.3 在Spring中使用JDBC
在Spring中,通过JDBC驱动定义数据源是最简单的配置方式。Spring提供了三个这样的数据源类供选择:
- DriverManagerDataSource:在每个连接请求时都返回一个新建的连接。与DBCP的BasicDataSource不同,由DriverManagerDataSource提供的连接并没有进行池化管理;
- SimpleDriverDataSource:与DriverManagerDataSource的工作方式类似,但是它直接使用JDBC驱动,来解决在特定环境下的类加载问题,这样的环境包括OSGi容器;
- SingleConnectionDataSource:在每个连接请求时都会返回同一个的连接。尽管SingleConnectionDataSource不是严格意义上的连接池数据源,但是你可以将其视为只有一个连接的池。
以上这些数据源的配置与DBCP BasicDataSource的配置类似。例如,如下就是配置DriverManagerDataSource的方法 XML:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="org.h2.Driver"
p:url="jdbc:h2:tcp://localhost/~/spitter"
P:username="sa"
p:password=""/>
Java配置:
@Bean
public DriverManagerDataSource dataSource(){
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
ds.setUsername("sa");
ds.setPassword("");
return ds;
}
10.2.4 使用嵌入式的数据源
嵌入式数据库 (embedded database)。嵌入式数据库作为应用的一部分运行,而不是应用连接的独立数据库服务器。对于开发和测试来讲,嵌入式数据库是很好的可选方案。这是因为每次重启应用或运行测试的时候,都能够重新填充测试数据。
Spring的jdbc命名空间能够简化嵌入式数据库的配置。例如,如下的程序清单展现了如何使用jdbc命名空间来配置嵌入式的H2数据库, 它会预先加载两组测试数据。
@Bean(destroyMethod="shutdown")
public DataSource dataSource() {
System.out.println("初始化数据库");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
//初始化数据,chema.sql文件中包含用于创建数据表的关系
.addScript("classpath:spittr/data/schema.sql")
.addScript("classpath:spittr/data/h2_security.sql")
.setScriptEncoding("UTF-8")
.build();
}
10.2.5 使用profile选择数据源
实际上,我们很可能面临 这样一种需求,那就是在某种环境下需要其中一种数据源,而在另外 的环境中需要不同的数据源。 在3.1版本中,Spring引入了bean profile的功能。要使用profile,你首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署到每个环境时,要确保对应的profile处于激活(active)的状态。
在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile。在Spring 3.1中,只能在类级别上使用@Profile注解。不过,从Spring 3.2开始,你也可以在方法级别上使用@Profile注解,与@Bean注解一同使用。
例如,在测试环境中使用H2数据库,而在生产环境中使用MySQL数据库,DataConfig修改如下:
package spittr.config;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
@Configuration
public class DataConfig {
@Bean(destroyMethod="shutdown")
@Profile("dev")
public DataSource dataSource() {
System.out.println("初始化数据库");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
//初始化数据,chema.sql文件中包含用于创建数据表的关系
.addScript("classpath:spittr/data/schema.sql")
.addScript("classpath:spittr/data/h2_security.sql")
//.addScript("classpath:spittr/test/test-data.sql")
.setScriptEncoding("UTF-8")
.build();
}
@Bean
@Profile("prod")
public DataSource mysqlDataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spittr?characterEncoding=UTF-8");
ds.setUsername("root");
ds.setPassword("root");
ds.setInitialSize(5);
ds.setMaxActive(10);
return ds;
}
@Bean
@Profile("prod")
public DataSourceInitializer dataSourceInitializer(DataSource dataSource) {
System.out.println("生产环境初始化数据表");
DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
dataSourceInitializer.setDataSource(dataSource);
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
//加载SQL文件
databasePopulator.addScript(new ClassPathResource("spittr/data/schema_mysql.sql"));
databasePopulator.addScript(new ClassPathResource("spittr/data/security_mysql.sql"));
dataSourceInitializer.setDatabasePopulator(databasePopulator);
dataSourceInitializer.setEnabled(true);
return dataSourceInitializer;
}
@Bean
public JdbcOperations jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
DataSourceInitializer来执行数据表的初始化工作,ResourceDatabasePopulator使用SQL脚本中定义的外部资源进行初始化或清理数据库。需添加以下3个jar包:commons-dbcp-1.4.jarcommons-pool-1.6.jarmysql-connector-java-5.1.38-bin.jar 在跑程序之前需执行两个MySQL脚本:
清单 schema_mysql.sql
DROP TABLE IF EXISTS Spittle;
create table Spittle (
id int(10) NOT NULL auto_increment,
message varchar(140) not null,
created_at timestamp not null,
latitude double,
longitude double,
PRIMARY KEY (id)
);
INSERT INTO Spittle (id, message, created_at, latitude, longitude) VALUES (1111, 'Spittles go fourth', '2018-02-24', 116.579618, 39.647447);
DROP TABLE IF EXISTS Spitter;
create table Spitter (
id int(10) NOT NULL auto_increment,
username varchar(20) unique not null,
password varchar(20) not null,
first_name varchar(30) not null,
last_name varchar(30) not null,
email varchar(30) not null,
PRIMARY KEY (id)
);
清单 security_mysql.sql:
DROP TABLE IF EXISTS users;
create table users (
id int NOT NULL auto_increment,
username varchar(50) unique not null,
password varchar(20) not null,
enabled tinyint(4) default null,
PRIMARY KEY (id)
);
DROP TABLE IF EXISTS authorities;
create table authorities (
id int NOT NULL auto_increment,
username varchar(50) unique not null,
authority varchar(50) default null,
PRIMARY KEY (id)
);
DROP TABLE IF EXISTS groups;
create table groups (
id int,
group_name varchar(50) not null,
PRIMARY KEY (id)
);
DROP TABLE IF EXISTS group_members;
create table group_members (
id int NOT NULL auto_increment,
group_id int(11) not null,
username varchar(50) not null,
PRIMARY KEY (id)
);
DROP TABLE IF EXISTS group_authorities;
create table group_authorities (
id int NOT NULL auto_increment,
group_id int(11) not null,
authority varchar(50) default null,
PRIMARY KEY (id)
);
INSERT INTO users (username, password, enabled) VALUES ('username', '123456', 1);
INSERT INTO authorities (username, authority) VALUES ('username', 'ROLE_USER');
INSERT INTO groups (id, group_name) VALUES (1, 'group_name');
INSERT INTO group_members (group_id, username) VALUES (1, 'username');
INSERT INTO group_authorities (group_id, authority) VALUES (1, 'ROLE_USER');
通过使用profile功能,会在运行时选择数据源,这取决于哪一个 profile处于激活状态。Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default。如果设置了spring.profiles.active属性的话,那么它的值就会用来确定哪个profile是激活的。但如果没有设置spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值。如果spring.profiles.active和spring.profiles.default均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean。
有多种方式来设置这两个属性:
- 作为DispatcherServlet的初始化参数;
- 作为Web应用的上下文参数;
- 作为JNDI条目;
- 作为环境变量;
- 作为JVM的系统属性;在集成测试类上,使用@ActiveProfiles注解设置。
这里选择第一种方式,在AbstractAnnotationConfigDispatcherServletInitializer中设置:
package spittr.config;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 将一个或多个路径映射到DispatcherServlet上
*/
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
/**
* 设置spring上下文配置类
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
/**
* 设置springmvc上下文配置类
* 当DispatcherServlet启动的时候,它会创建Spring应用上下文,并加载配置类或配置文件中所声明的bean
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected void customizeRegistration(Dynamic registration) {
// TODO Auto-generated method stub
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152,4194304,0));
}
@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
// TODO Auto-generated method stub
super.onStartup(servletContext);
servletContext.setInitParameter("spring.profiles.active", "prod");
}
}
将数据库设置为生产环境,即使用MySQL数据库。当程序运行时,会自动选择MySQL数据库。
10.3 在Spring中使用JDBC
JDBC不要求我们掌握其他框架的查询语言。它是建立在SQL之上的,而SQL本身就是数据访问语言。此外,与其他的技术相比,使用JDBC能够更好地对数据访问的性能进行调优。JDBC允许你使用数据库的所有特性,而这是其他框架不鼓励甚至禁
止的。
10.3.2 使用JDBC模板
Spring的JDBC框架承担了资源管理和异常处理的工作,从而简化了JDBC代码,让我们只需编写从数据库读写数据的必需代码。
Spring为JDBC提供了三个模板类供选择:
- jdbcTemplate:最基本的Spring JDBC模板,这个模板支持简单的JDBC数据库访问功能以及基于索引参数的查询;
- NamedParameterJDBCTemplate:使用该模板类执行查询时可以将值以命名参数的形式绑定到SQL中,而不是使用简单的索引参数;
- SimpleJDBCTemplate:该模板类利用Java5的一些特性如自动装箱、泛型以及可变参数列表来简化JDBC模板的使用。
从Spring3.1开始,SimpleJDBCTemplate已经被废弃,器Java5的特性被转移到了jdbcTemplate中,并且只有在你需要使用命名参数的时候,才需要使用NamedParameterJDBCTemplate。这样的话,对于大多数的JDBC任务来说,jdbcTemplate就是最好的可选方案。
使用JdbcTemplate来插入数据
为了让JdbcTemplate正常工作,只需要为其设置DataSource就可以了,这使得在Spring中配置JdbcTemplate非常容易,如下面的@Bean方法所示:
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}
现在,我们可以将jdbcTemplate装配到Repository中并使用它来访问数据库。例如,SpitterRepository使用了JdbcTemplate:
@Repository
public class JdbcSpitterRepository implements SpitterRepository {
private JdbcOperations jdbc;
@Autowired
public JdbcSpitterRepository(JdbcOperations jdbc) {
this.jdbc = jdbc;
}
...
}
JdbcOperations是一个接口,定义了JdbcTemplate所实现的操作。通过注入JdbcOperations,而不是具体的JdbcTemplate,能够保证JdbcSpitterRepository通过JdbcOperations接口达到与JdbcTemplate保持松耦合。 在Repository中具备可用的JdbcTemplate后,我们可以极大地简化程序清单10.4中的addSpitter()方法。基于JdbcTemplate的addSpitter()方法如下:
public Spitter addSpitter(Spitter spitter) {
// TODO Auto-generated method stub
jdbc.update(
"insert into Spitter (username, password, first_name, last_name, email)" +
" values (?, ?, ?, ?, ?)",
spitter.getUsername(),
spitter.getPassword(),
spitter.getFirstName(),
spitter.getLastName(),
spitter.getEmail());
return spitter;
}
使用JdbcTemplate来读取数据
程序清单10.8展现了新版本的findOne()方法,它使用了JdbcTemplate的回调,实现根据ID查询Spitter,并将结果集映射为Spitter对象。
程序清单10.8 使用JdbcTemplate查询Spitter
public Spitter findByUsername(String username) {
// TODO Auto-generated method stub
return jdbc.queryForObject(
"select id, username, password, first_name,
last_name, email from Spitter where username=?",
new SpitterRowMapper(),
username);
}
private static class SpitterRowMapper implements RowMapper<Spitter> {
public Spitter mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Spitter(
rs.getLong("id"),
rs.getString("username"),
null,
rs.getString("first_name"),
rs.getString("last_name"),
rs.getString("email"));
}
}
queryForObject()方法有三个参数:
- String对象,包含了要从数据库中查找数据的SQL;
- RowMapper对象,用来从ResultSet中提取数据并构建域对象(本例中为Spitter);
- 可变参数列表,列出了要绑定到查询上的索引参数值。
SpitterRowMapper对象实现了RowMapper接口。对于查询返回的每一行数据,JdbcTemplate将会调用RowMapper的mapRow()方法,并传入一个ResultSet和包含行号的整数。在SpitterRowMapper的mapRow()方法中,我们创建了Spitter对象并将ResultSet中的值填充进去。