Spring JDBC 源码解析 —— save / update 的实现

基本思路分析

Spring JDBC 对数据库的操作大部分都通过 jdbcTemplete 实现,这也是我们分析 Spring JDBC 的切入点。一个简单的 update 函数的调用例程如下:

public void save(User user) {
	jdbcTemplate.update("insert into user(name, age, sex) values(?, ?, ?)",
			new Object[] {user.getName(), user.getAge(), user.getSex()},
			new int[] {Types.VARCHAR, Types.INTEGER, Types.VARCHAR});
}

我们只需要提供 sql 语句和对应的参数和参数类型就可以实现对应的 update 功能。进入 update 函数可以看到如下内容:

public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException {
	return update(sql, newArgTypePreparedStatementSetter(args, argTypes));
}

public int update(String sql, @Nullable PreparedStatementSetter pss) throws DataAccessException {
	return update(new SimplePreparedStatementCreator(sql), pss);
}

通过 ArgTypePreparedStatementSetter 以及 SimplePreparedStatementCreator 分别对参数与参数类型、SQL 语句进行封装。在此之后就进入的核心代码:

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) pss).cleanupParameters();
			}
		}
	}));
}

其核心就是调用了 execute 函数实现的基础操作,其他的数据库操作如 query 等方法就是传入不同的 PreparedStatementCallback 参数来执行不同的操作。

基础方法 execute

execute 函数作为数据库操作的核心入口,将大多数数据库操作相同的步骤同意分装,而将个性花得操作使用参数 PreparedStatementCallback 进行回调。(利用了模板方法的设计原则)

public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
			throws DataAccessException {

	...

	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) {
		// 释放数据库连接,避免异常转换器没有被初始化的时候可能出现的连接池死锁
		// Release Connection early, to avoid potential connection pool deadlock
		// in the case when the exception translator hasn't been initialized yet.
		if (psc instanceof ParameterDisposer) {
			((ParameterDisposer) psc).cleanupParameters();
		}
		String sql = getSql(psc);
		psc = null;
		JdbcUtils.closeStatement(ps);
		ps = null;
		DataSourceUtils.releaseConnection(con, getDataSource());
		con = null;
		throw translateException("PreparedStatementCallback", sql, ex);
	}
	finally {
		if (psc instanceof ParameterDisposer) {
			((ParameterDisposer) psc).cleanupParameters();
		}
		JdbcUtils.closeStatement(ps);
		DataSourceUtils.releaseConnection(con, getDataSource());
	}
}

获取数据库连接

数据库操作的第一步就是获取连接,这步中不仅仅是简单的 get 动作,还考虑的一些特殊情况。

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
	Assert.notNull(dataSource, "No DataSource specified");

	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;
}

此处主要考虑的是事务层面的需求,Sping 需要保证线程中的数据库操作都是使用同一个事务连接。

应用用户设定的输入参数

protected void applyStatementSettings(Statement stmt) throws SQLException {
	int fetchSize = getFetchSize();
	if (fetchSize != -1) {
		stmt.setFetchSize(fetchSize);
	}
	int maxRows = getMaxRows();
	if (maxRows != -1) {
		stmt.setMaxRows(maxRows);
	}
	DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout());
}

fetchSize 是为了减少网络交互次数设计的,访问 ResultSet 时,如果每一行都需要从服务器读取,会造成大量的开销。fetchSize 可以一次获取多行,这样在获取下一行数据的时候就可以直接从内存中获取,而不需要网络交互,提高了效率。但这个设置也会导致更大的内存使用量。
maxRows 规定了当前 Statement 对象生成的所有 ResultSet 对象中的最大行数。

调用回调函数

使用不同情况下的个性化操作,即调用 PreparedStatementCallback 类型的 doInPreparedStatement 函数。

警告处理

protected void handleWarnings(Statement stmt) throws SQLException {
	// 当设置为忽略警告的时候,只打印日志
	if (isIgnoreWarnings()) {
		if (logger.isDebugEnabled()) {
			// 日志开启
			SQLWarning warningToLog = stmt.getWarnings();
			while (warningToLog != null) {
				logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + "', error code '" +
						warningToLog.getErrorCode() + "', message [" + warningToLog.getMessage() + "]");
				warningToLog = warningToLog.getNextWarning();
			}
		}
	}
	else {
		handleWarnings(stmt.getWarnings());
	}
}

SQLWarning 类型用于承载数据库访问警告信息。警告可以来自 Connection、Statement
和 ResultSet。出现警告很可能会出现数据错误,但程序的执行不一定会受到影响,所以用户可以自定义处理警告的方式,默认是忽略警告,只打印日志,我们也可以设置为抛出异常。

资源释放

数据库连接释放不是直接调用 close 方法。考虑到事务的情况,如果当前线程存在事务,那么说明在当前线程中存在共用数据库连接,这种情况下直接使用 ConnectionHolder 中的 release方法进行连接数减一,而没有真正释放。

public static void releaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) {
	try {
		doReleaseConnection(con, dataSource);
	}
	catch (SQLException ex) {
		logger.debug("Could not close JDBC Connection", ex);
	}
	catch (Throwable ex) {
		logger.debug("Unexpected exception on closing JDBC Connection", ex);
	}
}

public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException {
	if (con == null) {
		return;
	}
	if (dataSource != null) {
		// 当前线程存在事务,则使用 ConnectionHolder 中的 release 方法
		ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
		if (conHolder != null && connectionEquals(conHolder, con)) {
			// It's the transactional Connection: Don't close it.
			conHolder.released();
			return;
		}
	}
	doCloseConnection(con, dataSource);
}

Update 中的回调函数

回顾一下,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) {
				// 设置 PreparedStatement 所需要的全部参数
				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) pss).cleanupParameters();
			}
		}
	}));
}

上述代码的核心之一在于通过 PreparedStatementSetter 设置了相关的参数,不需要繁琐的配置,实际JDBC的操作是比较繁琐的。对于以下的内容:

jdbcTemplate.update("insert into user(name, age, sex) values(?, ?, ?)",
		new Object[] {user.getName(), user.getAge(), user.getSex()},
		new int[] {Types.VARCHAR, Types.INTEGER, Types.VARCHAR});

需要这么做:

PreparedStatement updateState = con.prepareStatement("insert into user(name, age, sex) values(?, ?, ?)");
updateState.setString(1, user.getName());
updateState.setInt(2, user.getAge());
updateState.setString(3, user.getSex());

那么 Spring 是如何简化操作的呢?
首先,从 pss.setValues(ps) 开始。

public void  setValues(PreparedStatement ps) throws SQLException {
	int parameterPosition = 1;
	if (this.args != null && this.argTypes != null) {
		for (int i = 0; i < this.args.length; i++) {
			Object arg = this.args[i];
			if (arg instanceof Collection && this.argTypes[i] != Types.ARRAY) {
				Collection<?> entries = (Collection<?>) arg;
				for (Object entry : entries) {
					if (entry instanceof Object[]) {
						Object[] valueArray = ((Object[]) entry);
						for (Object argValue : valueArray) {
							doSetValue(ps, parameterPosition, this.argTypes[i], argValue);
							parameterPosition++;
						}
					}
					else {
						doSetValue(ps, parameterPosition, this.argTypes[i], entry);
						parameterPosition++;
					}
				}
			}
			else {
				doSetValue(ps, parameterPosition, this.argTypes[i], arg);
				parameterPosition++;
			}
		}
	}
}

其中 doSetValue(ps, parameterPosition, this.argTypes[i], entry); 完成了对单个参数的匹配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值