目录
前言
接下来的几篇文章是为了讲 Spring 事务,但是在此之前还是要提一下基本的一些操作数据库的方式。因为框架虽然方便了我们的操作,但是其还是依赖底层的一些操作数据库的方式的,无非是对底层一些基本的操作方式进行一些封装,如果对底层的一些知识都了然于胸,那么再往下学的话就会容易很多。因为这篇文章主要是为了给后面讲事务做铺垫的,所以举的例子和讲的内容也会相对往事务上靠。
你一定知道的 JDBC
JDBC 是 Java Database Connectivity 的缩写,也就是 java 数据库连接的意思,既然都说了是 java 数据库连接,那么无论框架怎么变,无论框架实现的如何简单,但是底层代码一看可能还是这几行代码实现了增删改查。框架只是帮你写了一些逻辑,简化了你的操作,但是该有的其实还是全部都在那,只不过不用你来写了而已,比如你可以直接调用 List 的排序接口进行排序,表面上你不用写排序算法,但是其底层还是使用快速排序这个算法来帮你排的。首先了解一下 jdbc。
public class JdbcTest {
private static final String DRIVER = "com.mysql.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/transaction?useSSL=false";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
Savepoint savepoint = null;
try {
// 加载驱动
Class<?> clazz = Class.forName(DRIVER);
Driver driver = (Driver) clazz.newInstance();
//配置用户名,密码
Properties info = new Properties();
info.setProperty("user", USERNAME);
info.setProperty("password", PASSWORD);
// 创建出一个数据库连接
connection = driver.connect(URL, info);
//设置自动提交为 false
connection.setAutoCommit(false);
// 获取一个 statement 实例,用于操作数据库
statement = connection.createStatement();
//执行查询操作
ResultSet searchRes = statement.executeQuery("SELECT userId, balance FROM AccountInfo");
while (searchRes.next()) {
System.out.println(searchRes.getLong("userId") + " " + searchRes.getString("balance"));
}
//模拟更新操作
statement.execute("update AccountInfo set balance = balance + 1");
//设置保存点
savepoint = connection.setSavepoint();
//再次模拟更新
statement.execute("update AccountInfo set balance = balance + 1");
//模拟异常
int a = 1 / 0;
} catch (Exception e) {
try {
//回退到保存点,也可以调用 connection.rollback() 回退上面的所有操作
connection.rollback(savepoint);
connection.commit();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
通过上面的代码,相信大家都知道如何使用了,需要注意一点的是,我设置了connection.setAutoCommit(false) ,这表示执行更新数据库的 sql 之后,数据库中的数据不会立刻改变,而是在 connection.commit() 之后,数据才会发生改变。而这就是事务,即获取到connection 到 commit 之间的操作,是一个事务,也可以理解为是一个整体,只有中间的所有操作执行完毕,数据库中才会真正的执行更新操作。大家也可以试一下不设置 AutoCommit 或者设置为 true, 那么执行完一条更新操作之后,数据库里的数据就会立刻更新了。
此外需要强调的一点是关于保存点的设置。设置了保存点,就相当于设置了一个标记,如果程序执行过程中发生错误了,可以通过标记来回退到这个标记点。而不用完全撤回所有的操作。比如上面的程序执行结果,在出错之后如果回滚到保存点的话数据库中的数据就只会更新一次,但是如果调用 connection.rollback(), 那么这两条更新一次也不会生效。数据库中的数据不变。后面要说的事物的传播就可以通过保存点来控制当多个事务其中有一个出错的时候,应该怎么回滚。
你见过但是可能不了解的 DataSource
因为这个需要讲的篇幅比较多,所以我给单独写了一篇文章 ,可以先去看下这篇文章,然后再回来看本篇的内容,什么是 DataSource?什么又是 DruidDataSource?
你一定用过的 JdbcTemplate
相信看了 DataSource 的朋友已经知道 DataSource 的作用了,它主要是管理数据库连接。那么它管理的连接得有人用呀,不可能像我们在上篇文章上的样例那样先获取实例 bean,然后再调用 connect 方法再执行 sql 的,那也太不智能了。所以 DataSource 一般都需要和其他工具结合起来才能真正发挥出它的价值,其中一个我们很常用的它的伙伴就是 JdbcTemplate, JdbcTemplate是负责拿到数据库连接之后对数据库进行操作的一个工具类。
先看一下如何使用
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setUrl("jdbc:mysql://localhost:3306/transaction?useSSL=false");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setMinIdle(5);
dataSource.setMaxActive(20);
dataSource.setInitialSize(10);
return dataSource;
}
//添加 JdbcTemplate 实例交由 Spring 管理
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
//测试类
public class DataSourceMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(DataSourceConfig.class);
//获取 dataSource 实例
/*DataSource dataSource = (DataSource) applicationContext.getBean("dataSource");
try {
//通过数据源获取数据库连接
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
statement.execute("update AccountInfo set balance = balance + 1 where id = 1");
} catch (SQLException throwables) {
throwables.printStackTrace();
}*/
JdbcTemplate jdbcTemplate = (JdbcTemplate) applicationContext.getBean("jdbcTemplate");
String sql = "update AccountInfo set balance = balance + 1 where userId = 1";
jdbcTemplate.update(sql);
}
}
从上面样例可以看出来,只需要在配置类里配置 DataSource 和 JdbcTemplate,然后就可以直接使用 JdbcTemplate 实例对数据库进行操作了。这样用起来实在是太简单以至于我们会认为 jdbc 就是垃圾,DataSource 上面已经分析过了,这里就不再分析。就以本例 update 为例,讲一下 JdbcTemplate 怎么操作数据库的。
public int update(final String sql) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL update [" + sql + "]");
}
/**
* Callback to execute the update statement.
*/
//定义函数型接口,在后面逻辑获得 statement 实例之后会回调该方法来执行sql语句
class UpdateStatementCallback implements StatementCallback<Integer>, SqlProvider {
//真正调用的逻辑
@Override
public Integer doInStatement(Statement stmt) throws SQLException {
//通过 statement 实例来执行sql语句
int rows = stmt.executeUpdate(sql);
if (logger.isTraceEnabled()) {
logger.trace("SQL update affected " + rows + " rows");
}
return rows;
}
@Override
public String getSql() {
return sql;
}
}
// execute 传入了函数型接口实例,所以上面函数型接口的方法一定在execute中被调用
return updateCount(execute(new UpdateStatementCallback()));
}
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
//获取数据库连接
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
//通过 connection 获取 statement
stmt = con.createStatement();
applyStatementSettings(stmt);
//回调上面函数型接口自定义的方法
T result = action.doInStatement(stmt);
handleWarnings(stmt);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw translateException("StatementCallback", sql, ex);
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
可以看到上面有两部分比较重要,第一部分获取数据库连接,第二部分获取statement实例,然后调用函数型接口的回调方法,执行操作数据库的语句。从上面代码可以看出来获取 connection 之后的逻辑跟上面 jdbc 的样例一模一样。那么现在再看一下怎么获取数据库连接
//这里就是获取到我们配置的 DataSource 实例
protected DataSource obtainDataSource() {
DataSource dataSource = getDataSource();
Assert.state(dataSource != null, "No DataSource set");
return dataSource;
}
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
//真正获取数据库连接的代码
return doGetConnection(dataSource);
}
catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
}
catch (IllegalStateException ex) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + ex.getMessage());
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
//尝试从 ThreadLocal<Map<Object, Object>> resources 中获取连接,这是个线程缓存,如果已经获取到过一次,就会放入缓存中
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
//当从缓存中没获取到时,就会调用该方法获取数据库连接
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
try {
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
//讲数据库连接放入缓存中
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
catch (RuntimeException ex) {
// Unexpected exception from external delegation call -> close Connection and rethrow.
//释放连接
releaseConnection(con, dataSource);
throw ex;
}
}
return con;
}
private static Connection fetchConnection(DataSource dataSource) throws SQLException {
//这个就是上篇文章分析的获取数据库连接的方法,这里不分析了
Connection con = dataSource.getConnection();
if (con == null) {
throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
}
return con;
}
可以看到,jdbcTemplate 获取数据库连接也是从 DataSource 中获取的,具体逻辑上篇文章已经讲过,此外它又新增了一个缓存,用来缓存当前线程已经获取到的数据库连接,因为一个线程反正也就只能执行一个任务,没必要再去获取到一个新的连接。所以说这个设计也是挺巧妙的。
需要的一些依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
</dependencies>
总结
框架的发展让我们对数据库的操作越来越方便,但是其实万变不离其宗,该运行的代码一行也没有少,只不过那些代码不用你自己写了而已,所以没必要觉得 jdbc 已经没什么用了。其实恰恰是它在底层默默承担了所有。