layout | title | date | categories | tags | series | series_index | comments | copyrights | draft |
---|---|---|---|---|---|---|---|---|---|
post |
JDBC 和事务 |
2024-02-03 00:00:00 +0800 |
编程 |
java spring |
深入 Spring 源码 |
3 |
true |
原创 |
true |
Spring 是一个开源的轻量级 JavaEE 框架。它的核心是控制反转(IoC)和面向切面编程(AOP)。Spring 的 IoC 容器负责管理 JavaBean 的生命周期,而 AOP 容器负责管理切面。Spring 还提供了一系列的模块,如 Spring MVC、Spring JDBC、Spring Security 等。
在使用 JDBC 之前,我们需要先配置 jdbc.properties
文件:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/bookstore
jdbc.username=root
jdbc.password=password
使用配置类:
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); // 驱动类
dataSource.setUrl("jdbc:mysql://localhost:3306/bookstore"); // 数据库连接 URL
dataSource.setUsername("root"); // 用户名
dataSource.setPassword("password"); // 密码
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate() {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource());
return jdbcTemplate;
}
}
这里,我们创建了 DataSource
来连接数据库,创建了 JdbcTemplate
来操作数据库。
查看使用 XML 配置
配置 XML 文件:
<?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:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
然后,我们进行具体的增删改查操作:
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void addBook(Book book) {
String sql = "INSERT INTO book VALUES(?, ?)";
jdbcTemplate.update(sql, book.getTitle(), book.getAuthor());
}
public void updateBookAuthorByTitle(String title, String author) {
String sql = "UPDATE book SET author = ? WHERE title = ?";
jdbcTemplate.update(sql, author, title);
}
public Book getBookByTitle(String title) {
String sql = "SELECT * FROM book WHERE title = ?";
return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), title);
}
public List<Book> getAllBooks() {
String sql = "SELECT * FROM book";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));
}
public int countBooks() {
String sql = "SELECT COUNT(*) FROM book";
return jdbcTemplate.queryForObject(sql, Integer.class);
}
}
我们其实只关心 JdbcTemplate
的 update
和 query
方法。
点击查看 update 源码解读
update
方法封装在 JdbcOperations
中:
@Override
public int update(String sql, @Nullable PreparedStatementSetter pss) throws DataAccessException {
return update(new SimplePreparedStatementCreator(sql), pss);
}
@Override
public int update(String sql, @Nullable Object... args) throws DataAccessException {
return update(sql, newArgPreparedStatementSetter(args));
}
跟踪 update
方法:
protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss)
throws DataAccessException {
logger.debug("Executing prepared SQL update");
return updateCount(execute(psc, ps -> {
try {
if (pss != null) {
// 设置参数
pss.setValues(ps);
}
// 执行更新
int rows = ps.executeUpdate();
if (logger.isTraceEnabled()) {
logger.trace("SQL update affected " + rows + " rows");
}
return rows;
}
finally {
if (pss instanceof ParameterDisposer parameterDisposer) {
parameterDisposer.cleanupParameters();
}
}
}, true));
}
-
这里,
updateCount
方法会返回更新的行数。因此,我们来看execute
方法:@Nullable private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action, boolean closeResources) throws DataAccessException { Assert.notNull(psc, "PreparedStatementCreator must not be null"); Assert.notNull(action, "Callback object must not be null"); if (logger.isDebugEnabled()) { String sql = getSql(psc); logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : "")); } // 获取连接 Connection con = DataSourceUtils.getConnection(obtainDataSource()); PreparedStatement ps = null; try { ps = psc.createPreparedStatement(con); applyStatementSettings(ps); // 执行回调 T result = action.doInPreparedStatement(ps); handleWarnings(ps); return result; } catch (SQLException ex) { if (psc instanceof ParameterDisposer parameterDisposer) { parameterDisposer.cleanupParameters(); } if (ps != null) { handleWarnings(ps, ex); } String sql = getSql(psc); psc = null; JdbcUtils.closeStatement(ps); ps = null; DataSourceUtils.releaseConnection(con, getDataSource()); con = null; throw translateException("PreparedStatementCallback", sql, ex); } finally { if (closeResources) { if (psc instanceof ParameterDisposer parameterDisposer) { parameterDisposer.cleanupParameters(); } JdbcUtils.closeStatement(ps); DataSourceUtils.releaseConnection(con, getDataSource()); } } }
-
再看
setValues
方法:@Override public void setValues(PreparedStatement ps) throws SQLException { if (this.args != null) { for (int i = 0; i < this.args.length; i++) { Object arg = this.args[i]; doSetValue(ps, i + 1, arg); } } }
跟踪
doSetValue
方法:protected void doSetValue(PreparedStatement ps, int parameterPosition, @Nullable Object argValue) throws SQLException { if (argValue instanceof SqlParameterValue paramValue) { StatementCreatorUtils.setParameterValue(ps, parameterPosition, paramValue, paramValue.getValue()); } else { StatementCreatorUtils.setParameterValue(ps, parameterPosition, SqlTypeValue.TYPE_UNKNOWN, argValue); } }
我们就不继续跟踪下去了。最后实际上就是根据数据类型,设置参数。
-
executeUpdate
方法实际上是java.sql
包中的方法,跟框架无关。
在实际开发中,我们经常会遇到这样的情况:一个业务操作需要执行多个 SQL 语句,如果其中一个 SQL 语句执行失败,那么其他 SQL 语句也应该回滚。例如,购书时,需要先扣除用户的余额,然后再减少书的库存。如果书没了,但是余额却扣除了,那么就会出现问题;同理,如果余额不够,书却减少了,也会出现问题。
在最原始的编程式事务管理中,我们需要先关闭自动提交,然后手动提交或回滚事务:
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
public void buyBook(String title, String username) {
Book book = bookDao.getBookByTitle(title);
int price = book.getPrice();
try {
DataSourceUtils.getConnection(dataSource).setAutoCommit(false);
bookDao.updateBookStockByTitle(title, -1);
bookDao.updateUserBalanceByUsername(username, -price);
DataSourceUtils.getConnection(dataSource).commit();
} catch (Exception e) {
DataSourceUtils.getConnection(dataSource).rollback();
} finally {
DataSourceUtils.getConnection(dataSource).setAutoCommit(true);
}
}
}
可以看到,这一方法实现了事务管理的 ACID 特性,但是这样做有几个问题:
-
代码冗余
每个方法都需要写高度雷同事务代码,导致了代码冗余。
-
耦合度高
业务代码和事务代码耦合在一起,导致了耦合度过高,不利于集中维护。
因此,我们需要事务管理来解决这些问题。
Spring 提供了两种事务管理方式:编程式事务管理和声明式事务管理。声明式事务管理可以有效解决上面提到的问题。
使用注解配置:
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example")
public class AppConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/bookstore");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate() {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource());
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource());
return transactionManager;
}
}
查看使用 XML 配置
在配置文件中开启事务管理:
<?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:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
然后,我们可以使用 @Transactional
注解来声明事务:
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Transactional
public void buyBook(String title, String username) {
Book book = bookDao.getBookByTitle(title);
int price = book.getPrice();
bookDao.updateBookStockByTitle(title, -1);
bookDao.updateUserBalanceByUsername(username, -price);
}
}
这时,我们就不需要手动提交或回滚事务了,Spring 会自动为我们处理。
@Transactional
注解有以下几个属性:
-
propagation
事务传播行为,确定了有包含关系的两个事务如何执行,默认值为
Propagation.REQUIRED
。它有以下几种取值:Propagation.REQUIRED
:如果当前没有事务,就新建一个事务;如果当前有事务,就加入到当前事务中Propagation.SUPPORTS
:如果当前有事务,就加入到当前事务中;如果当前没有事务,就以非事务方式执行Propagation.MANDATORY
:如果当前有事务,就加入到当前事务中;如果当前没有事务,就抛出异常Propagation.REQUIRES_NEW
:新建一个事务,如果当前有事务,就将当前事务挂起Propagation.NOT_SUPPORTED
:以非事务方式执行,如果当前有事务,就将当前事务挂起Propagation.NEVER
:以非事务方式执行,如果当前有事务,就抛出异常Propagation.NESTED
:如果当前没有事务,就新建一个事务;如果当前有事务,就在当前事务中嵌套一个事务
-
isolation
事务隔离级别,确定了不同事务之间如何互相影响,默认值为
Isolation.DEFAULT
。它有以下几种取值:Isolation.DEFAULT
:使用数据库默认的隔离级别Isolation.READ_UNCOMMITTED
:读未提交,即一个事务可以读取另一个事务已经修改但还未提交的数据Isolation.READ_COMMITTED
:读已提交,即一个事务只能读取另一个事务已经提交的数据Isolation.REPEATABLE_READ
:可重复读,即一个事务在多次读取同一数据时,读到的数据是一样的,在此期间,其他事务对该数据的修改是不可见的Isolation.SERIALIZABLE
:串行化,即一个事务在执行时,另一个事务不能对其进行修改
-
timeout
事务超时时间,默认值为
-1
,单位为秒。如果程序卡住,超过了指定时间,事务会自动回滚 -
readOnly
是否只读事务,默认值为
false
。如果设置为true
,则只能进行查询操作,不能进行增删改操作 -
rollbackFor
、rollbackForClassName
、noRollbackFor
、noRollbackForClassName
设置哪些异常会回滚事务,哪些异常不会回滚事务。参数为
Class
类型或者String
类型
查看使用 XML 配置
声明式事务管理同样能够基于 XML 配置:
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="buy*" propagation="REQUIRED" isolation="DEFAULT" timeout="-1" read-only="false"/>
</tx:attributes>
</tx:advice>