JDBC
为了使 JDBC 更加易于使用,Spring 在 JDBCAPI 上定义了一个抽象层, 以此建立一个JDBC存取框架.
作为 SpringJDBC 框架的核心, JDBC 模板的设计目的是为不同类型的JDBC操作提供模板方法. 每个模板方法都能控制整个过程,并允许覆盖过程中的特定任务.通过这种方式,可以在尽可能保留灵活性的情况下,将数据库存取的工作量降到最低.
引入Spring框架相关的jar包以及c3p0和mysql连接jar包。
- c3p0-0.9.1.2.jar
- com.springsource.net.sf.cglib-2.2.0.jar
- com.springsource.org.aopalliance-1.0.0.jar
- com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
- commons-logging-1.1.3.jar
- mysql-connector-java-5.1.7-bin.jar
- spring-aop-4.0.0.RELEASE.jar
- spring-aspects-4.0.0.RELEASE.jar
- spring-beans-4.0.0.RELEASE.jar
- spring-context-4.0.0.RELEASE.jar
- spring-core-4.0.0.RELEASE.jar
- spring-expression-4.0.0.RELEASE.jar
- spring-jdbc-4.0.0.RELEASE.jar
- spring-orm-4.0.0.RELEASE.jar
- spring-tx-4.0.0.RELEASE.jar
- spring-web-4.0.0.RELEASE.jar
- spring-webmvc-4.0.0.RELEASE.jar
User.java
public class User {
private Integer id;
private String username;
private String password;
private Integer balance;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getBalance() {
return balance;
}
public void setBalance(Integer balance) {
this.balance = balance;
}
public User(Integer id, String username, String password, Integer balance) {
super();
this.id = id;
this.username = username;
this.password = password;
this.balance = balance;
}
public User() {
super();
}
@Override
public String toString() {
return "Users [id=" + id + ", username=" + username + ", password="
+ password + ", balance=" + balance + "]";
}
}
db.properties
jdbc.user=root
jdbc.password=1230
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql:///test
jdbc.initPoolSize=5
jdbc.maxPoolSize=10
applicationContext.xml
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 配置C3P0数据源 -->
<bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!-- 配置Spring的JdbcTemplate-->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
创建一个测试类对JDBCTemplate的方法进行测试
JDBCTest.java
public class JDBCTest {
private ApplicationContext ctx = null;
private JdbcTemplate jdbcTemplate = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
}
@Test
public void testDataSource() throws SQLException {
DataSource dataSource = ctx.getBean(DataSource.class);
System.out.println(dataSource.getConnection());
}
/**
* 执行INSERT,UPDATE,DELETE
*/
@Test
public void testUpdate(){
String sql = "insert into users values(3, 'kety', '123', 200)";
jdbcTemplate.update(sql);
}
/**
* 执行批量更新:批量的INSERT,UPDATE,DELETE
* 最后一个参数是Object[]的List类型:因为修改一条记录需要一个Object数组,那么多条就需要多个Object的数组
*/
@Test
public void testBatchUpdate(){
String sql = "insert into users(id,username,password,balance) values(?,?,?,?)";
List<Object[]> batchArgs = new ArrayList<Object[]>();
batchArgs.add(new Object[]{6, "AA", "123", 1000});
batchArgs.add(new Object[]{7, "BB", "123", 2000});
batchArgs.add(new Object[]{8, "CC", "123", 3000});
batchArgs.add(new Object[]{9, "DD", "123", 4000});
jdbcTemplate.batchUpdate(sql, batchArgs);
}
/**
* 从数据库中获取一条记录, 实际得到对应的一个对象
* 注意不是调用 queryForObject(String sql, Class<Employee> requiredType, Object... args) 方法!
* 而需要调用 queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args)
* 1. 其中的 RowMapper 指定如何去映射结果集的行, 常用的实现类为 BeanPropertyRowMapper
* 2. 使用 SQL 中列的别名完成列名和类的属性名的映射. 例如 last_name lastName
* 3. 不支持级联属性. JdbcTemplate 到底是一个 JDBC 的小工具, 而不是 ORM 框架
*/
@Test
public void testQueryForObject(){
String sql = "select id, username, password, balance from users where id=?";
RowMapper<User> rowMapper = new BeanPropertyRowMapper<User>(User.class);
User user = jdbcTemplate.queryForObject(sql, rowMapper, 6);
System.out.println("User:" + user);
}
/**
* 查到实体类的集合
* 注意调用的不是 queryForList 方法
*/
@Test
public void testQueryForList(){
String sql = "select id, username, password, balance from users where id<?";
RowMapper<User> rowMapper = new BeanPropertyRowMapper<User>(User.class);
List<User> users = jdbcTemplate.query(sql, rowMapper, 10);
System.out.println(users);
}
/**
* 获取单个列的值, 或做统计查询
* 使用 queryForObject(String sql, Class<Long> requiredType)
*/
@Test
public void testQueryForObject2(){
String sql = "select count(id) from users";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("count:" + count);
}
}
在实际的使用中,一般会创建一个dao类来封装对某个对象的所有增删改查操作.
UserDao.java
@Repository
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public User get(Integer id){
String sql = "select id, username, password, balance from users where id=?";
RowMapper<User> rowMapper = new BeanPropertyRowMapper<User>(User.class);
User user = jdbcTemplate.queryForObject(sql, rowMapper, 6);
return user;
}
}
在applicationContext.xml文件中增加标签
<context:component-scan base-package="com.atguigu.spring.jdbc"></context:component-scan>
JDBCTest.java
public class JDBCTest {
private ApplicationContext ctx = null;
private JdbcTemplate jdbcTemplate = null;
private UserDao userDao = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
userDao = ctx.getBean(UserDao.class);
}
@Test
public void testUserDao(){
System.out.println(userDao.get(1));
}
}
在JdbcTemplate中使用具名参数
在经典的 JDBC 用法中, SQL 参数是用占位符 ? 表示,并且受到位置的限制. 定位参数的问题在于, 一旦参数的顺序发生变化, 就必须改变参数绑定.
在 Spring JDBC 框架中, 绑定 SQL 参数的另一种选择是使用具名参数(named parameter).
具名参数: SQL 按名称(以冒号开头)而不是按位置进行指定. 具名参数更易于维护, 也提升了可读性. 具名参数由框架类在运行时用占位符取代
具名参数只在 NamedParameterJdbcTemplate 中得到支持
步骤:
在applicationContext.xml中添加代码
<!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数 -->
<bean class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate" id="namedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
JDBCTest.java
public class JDBCTest {
private ApplicationContext ctx = null;
private JdbcTemplate jdbcTemplate = null;
private UserDao userDao = null;
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
userDao = ctx.getBean(UserDao.class);
namedParameterJdbcTemplate = ctx.getBean(NamedParameterJdbcTemplate.class);
}
/**
* 可以为参数起名字.
* 1. 好处: 若有多个参数, 则不用再去对应位置, 直接对应参数名, 便于维护
* 2. 缺点: 较为麻烦.
*/
@Test
public void testNamedParameterJdbcTemplate(){
String sql = "insert into users(id,username, password, balance) values(:id, :username, :password, :balance)";
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("id", 10);
paramMap.put("username", "Herry");
paramMap.put("password", "1234");
paramMap.put("balance", 1000);
namedParameterJdbcTemplate.update(sql, paramMap);
}
/**
* 使用具名参数时, 可以使用 update(String sql, SqlParameterSource paramSource) 方法进行更新操作
* 1. SQL 语句中的参数名和类的属性一致!
* 2. 使用 SqlParameterSource 的 BeanPropertySqlParameterSource 实现类作为参数.
*/
@Test
public void testNamedParameterJdbcTemplate2(){
String sql = "insert into users(id,username, password, balance) values(:id, :username, :password, :balance)";
User user = new User();
user.setId(11);
user.setBalance(1000);
user.setUsername("Beme");
user.setPassword("123456");
SqlParameterSource paramSource = new BeanPropertySqlParameterSource(user);
namedParameterJdbcTemplate.update(sql, paramSource);
}
}
事务
声明式事务管理: 大多数情况下比编程式事务管理更好用. 它将事务管理代码从业务方法中分离出来, 以声明的方式来实现事务管理. 事务管理作为一种横切关注点, 可以通过 AOP 方法模块化. Spring 通过 Spring AOP 框架支持声明式事务管理.
声明式事务
BookShopDao.java
public interface BookShopDao {
//根据书号获取书的单价
public int findBookPriceByIsbn(String isbn);
//更新数的库存. 使书号对应的库存 - 1
public void updateBookStock(String isbn);
//更新用户的账户余额: 使 username 的 balance - price
public void updateUserAccount(String username, int price);
}
BookShopDaoImpl.java
@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int findBookPriceByIsbn(String isbn) {
String sql = "select price from book where isbn=?";
return jdbcTemplate.queryForObject(sql, Integer.class, isbn);
}
@Override
public void updateBookStock(String isbn) {
// 检查书的库存是否足够,如果不够抛出异常
String sql2 = "select stock from book_stock where isbn=?";
int num = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
if(num == 0){
throw new BookStockException("库存不足");
}
String sql = "update book_stock set stock=stock-1 where isbn=?";
jdbcTemplate.update(sql, isbn);
}
@Override
public void updateUserAccount(String username, int price) {
//判断账户余额,如果不足则抛出异常
String sql2 = "select balance from account where username=?";
int num = jdbcTemplate.queryForObject(sql2, Integer.class, username);
if(num < price){
throw new UserAccountException("余额不足");
}
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
jdbcTemplate.update(sql, price, username);
}
}
BookShopService.java
public interface BookShopService {
public void purchase(String username, String isbn);
}
BookShopServiceImpl.java
@Service
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
//添加事务注解
@Transactional
@Override
public void purchase(String username, String isbn) {
//1. 获取图书单价
int price = bookShopDao.findBookPriceByIsbn("1001");
//2. 减去图书库存
bookShopDao.updateBookStock("1001");
//3. 减去账户余额
bookShopDao.updateUserAccount(username, price);
}
}
applicationContext.xml
<context:component-scan base-package="com.atguigu.spring.*"></context:component-scan>
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 配置C3P0数据源 -->
<bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!-- 配置Spring的Template -->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 启用事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
SpringTranscationTest.java
public class SpringTranscationTest {
private ApplicationContext ctx = null;
private BookShopDao bookShopDao = null;
private BookShopService bookShopService = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
bookShopDao = (BookShopDao) ctx.getBean("bookShopDao");
bookShopService = ctx.getBean(BookShopService.class);
}
@Test
public void testBookShopService(){
bookShopService.purchase("AA", "1001");
}
}
事务的传播行为
当事务方法被另一个事务方法调用时, 必须指定事务应该如何传播. 例如: 方法可能继续在现有事务中运行, 也可能开启一个新事务, 并在自己的事务中运行.
事务的传播行为可以由传播属性指定. Spring 定义了 7 种类传播行为.
REQUIRED 传播行为
当 bookService 的 purchase() 方法被另一个事务方法 checkout() 调用时, 它默认会在现有的事务内运行. 这个默认的传播行为就是 REQUIRED. 因此在 checkout() 方法的开始和终止边界内只有一个事务. 这个事务只在 checkout() 方法结束的时候被提交, 结果用户一本书都买不了
事务传播属性可以在 @Transactional 注解的 propagation 属性中定义
REQUIRED_NEW 传播行为
另一种常见的传播行为是 REQUIRES_NEW. 它表示该方法必须启动一个新事务, 并在自己的事务内运行. 如果有事务在运行, 就应该先挂起它.
根据刚才声明式事务的例子,增加结账的功能
Cashier.java
public interface Cashier {
public void checkout(String username, List<String> isbns);
}
CashierImpl.java
@Service("cashier")
public class CashierImpl implements Cashier{
@Autowired
private BookShopService bookShopService;
@Transactional
@Override
public void checkout(String username, List<String> isbns) {
for(String isbn : isbns){
bookShopService.purchase(username, isbn);
}
}
}
SpringTranscationTest.java
public class SpringTranscationTest {
private ApplicationContext ctx = null;
private BookShopDao bookShopDao = null;
private BookShopService bookShopService = null;
private Cashier cashier = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
bookShopDao = (BookShopDao) ctx.getBean("bookShopDao");
bookShopService = ctx.getBean(BookShopService.class);
cashier = (Cashier) ctx.getBean("cashier");
}
@Test
public void testTransactionPropagation(){
cashier.checkout("AA", Arrays.asList("1001", "1002"));
}
}
事务的其他属性
隔离级别
使用 isolation 指定事务的隔离级别, 最常用的取值为 READ_COMMITTED
回滚
默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚. 也可以通过对应的属性进行设置. 通常情况下取默认值即可.
只读
使用 readOnly 指定事务是否为只读. 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务. 若真的事一个只读取数据库值的方法, 应设置 readOnly=true
过期
使用 timeout 指定强制回滚之前事务可以占用的时间.
BookShopServiceImpl.java
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
//添加事务注解
// @Transactional(propagation=Propagation.REQUIRES_NEW,
// isolation=Isolation.READ_COMMITTED,
// noRollbackFor={UserAccountException.class})
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
readOnly=false,
timeout=3)
@Override
public void purchase(String username, String isbn) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {}
//1. 获取书的单价
int price = bookShopDao.findBookPriceByIsbn(isbn);
//2. 更新数的库存
bookShopDao.updateBookStock(isbn);
//3. 更新用户余额
bookShopDao.updateUserAccount(username, price);
}
}
使用XML文件的方式配置事务
applicationContext-tx-xml.xml
<!-- 配置 bean -->
<bean id="bookShopDao" class="com.atguigu.spring.tx.xml.BookShopDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="bookShopService" class="com.atguigu.spring.tx.xml.service.impl.BookShopServiceImpl">
<property name="bookShopDao" ref="bookShopDao"></property>
</bean>
<bean id="cashier" class="com.atguigu.spring.tx.xml.service.impl.CashierImpl">
<property name="bookShopService" ref="bookShopService"></property>
</bean>
<!-- 1. 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 2. 配置事务属性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase" propagation="REQUIRES_NEW"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 3. 配置事务切入点, 以及把事务切入点和事务属性关联起来 -->
<aop:config>
<aop:pointcut expression="execution(* com.atguigu.spring.tx.xml.service.*.*(..))"
id="txPointCut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>