Skip to content

Latest commit

 

History

History
464 lines (364 loc) · 15.8 KB

2024-02-03-JDBC和事务.md

File metadata and controls

464 lines (364 loc) · 15.8 KB
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 之前,我们需要先配置 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);
    }
}

JDBC 源码解读

我们其实只关心 JdbcTemplateupdatequery 方法。

update

点击查看 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 包中的方法,跟框架无关。

Transaction

为什么需要事务

在实际开发中,我们经常会遇到这样的情况:一个业务操作需要执行多个 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,则只能进行查询操作,不能进行增删改操作

  • rollbackForrollbackForClassNamenoRollbackFornoRollbackForClassName

    设置哪些异常会回滚事务,哪些异常不会回滚事务。参数为 Class 类型或者 String 类型

基于 XML 配置

查看使用 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>