手写 MyBatis 框架 - GodBatis 实现上的问题与改进

GodBatis 实现上的问题

GodBatis 在事务管理器的实现上存在问题:一个 SqlSessionFactory 只能对应一个 Transaction 事务管理器。如果 SqlSessionFactory.openSession() 开启多次会话,那么每个会话 SqlSession 底层都是使用的同一个事务管理器 Transaction,从而每个会话使用的是同一个 Connection 连接对象。

在 MyBatis 中也有 Transaction 接口,也有 JdbcTransactionManagedTransaction 两个实现类。

假设事务管理器配置是 <transactionManager type="JDBC"/>,那么每次开启会话 SqlSessionFactory.openSession(),底层就会相应创建一个 JdbcTransaction。因此:

  • 一个 SqlSessionFactory 能够创建多个 SqlSession SQL 会话。
  • 一个 SqlSession 对应一个 Transaction 事务管理器。
  • 一个 Transaction 对应一个 Connection 连接对象。

因此,在 MyBatis 中,一个 SqlSessionFactory 在多次 openSession 后,其实是创建了多个 Transaction 事务管理器,每个事务管理器管理一个独立的自己的 Connection 对象。

但在 GodBatis 中,一个 SqlSessionFactory 只能对应一个 Transaction 事务管理器。这个问题出现的根本原因在于,设计 SqlSessionFactory 类时,直接将 Transaction 作为属性。

GodBatis 问题溯源

学习到 036 - 手写 godbatis 框架第二步 - 分析 SqlSessionFactory 类该有的属性 时,老杜分析说,SqlSessionFactory 的类中应该包含如下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SqlSessionFactory {
/**
* 事务管理器属性
*/

/**
* 数据源属性
*/

/**
* SQL 映射信息属性
* 存放 sql 语句的 map 集合
* key 为 sqlId
* value 为 sqlId 相应的标签信息对象(MappedStatement)
*/
private Map<String, MappedStatement> mappedStatements;
}

按我的理解,到这一步还是没什么太大问题的,前提是:

  • 事务管理器属性应该对应核心配置文件中的 <transactionManager> 标签,而非一个事务管理器实现。

但在学习到下一集 037 - 手写 godbatis 框架第三步 - 抽取事务管理器接口 就有些不对劲了。
老杜分析说,SqlSessionFactory 中的事务管理器属性应该面向接口编程,应如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SqlSessionFactory {
/**
* 事务管理器属性
* 事务管理器应该根据核心配置文件中的相应配置,进行相应的灵活切换,故该属性应该面向接口编程
*/
private Transaction transaction;

/**
* 数据源属性
*/

/**
* SQL 映射信息属性
* 存放 sql 语句的 map 集合
* key 为 sqlId
* value 为 sqlId 相应的标签信息对象(MappedStatement)
*/
private Map<String, MappedStatement> mappedStatements;
}

其中接口 Transaction 具有两个实现类 JdbcTransactionManagedTransaction,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 事务管理器接口:提供事务管理相关方法
* godbatis 中所有事务管理器都应该遵循该规范。
* JDBC 事务管理器,MANAGED 事务管理器都必须实现该接口
*/
public interface Transaction {
/**
* 提交事务
*/
void commit();

/**
* 回滚事务
*/
void rollback();

/**
* 关闭事务
*/
void close();
}

看到这就觉得有些问题了。

SqlSessionFactory 中,事务管理器属性应该更像是一个事务管理器工厂,而非一个具体的事务管理器实现
但在老杜的设计中,事务管理器属性显然是被设计为一个具体的事务管理器实现。
主要体现在 Transaction 接口中提供了 commit rollbackclose 这样的事务管理方法。
如果是事务管理器工厂,显然无需提供这样的方法,而是提供类似 newTransaction 这样的方法。


继续往下学习,会越来越确定,GodBatis 的事务管理器就是存在设计问题。

  • 038 - 手写 godbatis 框架第四步 - 事务管理器的实现 中老杜计划重点实现 JdbcTransaction 事务管理器,但 Transaction 接口中的 commit rollbackclose 需要 Connection 连接对象,才能完成操作。获取连接对象需要数据源,因此干脆将原先计划定义在 SqlSessionFactory 中的数据源属性,移动到 JdbcTransaction 接口中。

  • 039 - 手写 godbatis 框架第五步 - 数据源的实现 中老杜重点实现了 UnPooledDataSource 数据源。

  • 040 - 手写 godbatis 框架第六步 - 事务管理器改造 中老杜在 JdbcTransaction 中新增了两个属性:连接对象 Connection connection 和自动提交标志 boolean autoCommit,新增了开启数据库连接的方法 openConnection() 给属性 connection 赋值。最后使用属性 connection 实现了接口中的 commit rollbackclose 方法。

到此为止,GodBatis 从 SqlSessionFactoryJdbcTransaction 再到 Connection 的整个脉络基本已经清晰,一个 SqlSessionFactory 对象确实只会对应一个 Transaction,从而只对应一个 Connection

后续在实现 SqlSessionFactory 类的 openSession() 方法,是直接通过该类的属性 transactiontransaction.openConnection 开启连接,因此,多次开启会话只有第一次开启有用。

SqlSessioninsert()selectOne() 方法实现中,但凡需要用到连接对象,无一例外,也都是直接从 SqlSessionFactory 类中的 transaction 属性来获取连接对象,因此每个 SqlSession 中的连接其实是同一个,都是第一次开启会话的那个连接。

参考 MyBatis 的实现

先看 SqlSessionFactoryBuilder,其 build 方法底层实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse()); // 关键行
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

// more code...
}

可以看到,执行 build 方法,最后实际返回的是 DefaultSqlSessionFactory(该类是 SqlSessionFactory 接口的实现类)。

创建 DefaultSqlSessionFactory 对象需要一个 Configuration config,而 configDefaultSqlSessionFactory 中唯一定义的一个属性,该属性即 xml 文件解析的结果,其中包含了所有的配置信息,自然就包括了事务管理器相关的配置信息。

1
2
3
4
5
6
7
8
9
10
public class DefaultSqlSessionFactory implements SqlSessionFactory {

private final Configuration configuration;

public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}

// more code...
}

得到 DefaultSqlSessionFactory factory 后,再看 factory.openSession() 方法的底层实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DefaultSqlSessionFactory implements SqlSessionFactory {

private final Configuration configuration;

@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

// more code...
}

关注 openSessionFromDataSource 方法,重点梳理一下 try 代码块中的内容。

1
2
3
4
5
6
7
8
9
// 从完整配置信息 configuration 中获取默认环境 environment(包含事务管理器工厂类和数据源)
final Environment environment = configuration.getEnvironment();
// 从默认环境 environment 中获取事务管理器工厂(如果配置是 JDBC,那么这里获取的就是 JDBC 事务管理器工厂)
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 从默认环境 environment 中获取数据源,用以创建新事务管理器(如果配置是 JDBC,工厂将创建 JdbcTransaction)
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 事务管理器和执行类型(是否批量执行)被封装到执行器 executor 中
final Executor executor = configuration.newExecutor(tx, execType);
// 执行器 executor 被封装到 SqlSession 实现类 DefaultSqlSession 对象中然后返回

可以看到,每次 openSession() 执行,实际上底层都会创建一个新的事务管理器,并将该事务管理器封装到 DefaultSqlSession 即 SQL 会话对象中返回(同样 SqlSession 只是接口,DefaultSqlSession 才是具体实现)。

为什么 configuration.getEnvironment() 获取的是默认环境呢?这需要深入到 XML 解析的过程。
具体看 XMLConfigBuilder 中的 environmentsElement 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// context 对应 <environments> 标签
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// 这里获取了默认环境 id,保存到属性 environment 中
environment = context.getStringAttribute("default");
}
// 遍历所有子节点 <environment>
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
// isSpecifiedEnvironment 比对当前环境 id 是否与默认环境 id 一致,如果一致,才进入 if 语句
if (isSpecifiedEnvironment(id)) {
// 解析 <environment> 标签内容,创建事务管理器工厂 txFactory 和数据源工厂 dsFactory
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
// 与事务管理器不同,数据源只需要一个就够了,因此只将环境 id,事务管理器工厂 txFactory 和数据源 dataSource 进行封装
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 封装得到的 Environment 对象设置到完整配置信息 configuration 中去
configuration.setEnvironment(environmentBuilder.build());
break;
}
}
}
}


综上梳理可得到如下关系:

  • DefaultSqlSessionFactory sqlSessionFactory:SQL 会话工厂

    • Configuration config:全局配置信息,包含所有 XML 解析结果
      • Environment environment:环境信息,包含环境 id,事务管理器,数据源
        • TransactionFactory transactionFactory:具体类型的事务管理器工厂类,每次 openSession 时通过 newTransaction 可以创建具体类型事务管理器
        • DataSource dataSource:具体类型的数据源
      • Map<String, MappedStatement> mappedStatements:SQL 标签映射信息
  • DefaultSqlSession sqlSession:SQL 会话

    • Configuration configuration:全局配置信息,主要用来获取 SQL 标签映射信息
    • Executor executor:执行器对象,主要用来获取事务管理器
      • Transaction transaction:具体类型的事务管理器对象
        • Connection connection:连接对象
        • DataSource dataSource:具体类型的数据源

改进 GodBatis

MyBatis 的设计相对复杂,这里仅借鉴其思想来改进 GodBatis 在事务管理器上的设计问题,改造为:

  • SqlSessionFactory sqlSessionFactory:SQL 会话工厂

    • TransactionFactory transactionFactory:具体类型的事务管理器工厂类,每次 openSession 时通过 newTransaction 可以创建具体类型事务管理器
    • DataSource dataSource:具体类型的数据源
    • Map<String, MappedStatement> mappedStatements:SQL 标签映射信息
  • SqlSession sqlSession:SQL 会话

    • Transaction transaction:具体类型的事务管理器对象
      • Connection connection:连接对象
      • DataSource dataSource:具体类型的数据源
    • Map<String, MappedStatement> mappedStatements:SQL 标签映射信息

新增事务管理器工厂相关类和接口

新增 TransactionFactory 接口,接口提供 newTransaction 方法:

1
2
3
public interface TransactionFactory {
Transaction newTransaction(DataSource dataSource, boolean autoCommit);
}

提供接口实现类 JdbcTransactionFactoryManagedTransactionFactory

1
2
3
4
5
6
public class JdbcTransactionFactory implements TransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, boolean autoCommit) {
return new JdbcTransaction(dataSource, autoCommit);
}
}
1
2
3
4
5
6
public class ManagedTransactionFactory implements TransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, boolean autoCommit) {
return null;
}
}

改进 SqlSessionFactory 实现

SqlSessionFactory 中不再直接定义 Transaction 属性(注释掉),而是定义 TransactionFactory 属性。在 openSession() 中,需要 transactionFactory.newInstance() 来创建事务管理器对象,newInstance() 方法需要数据源对象,因此额外新增定义 DataSource 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class SqlSessionFactory {

// /**
// * 事务管理器属性
// * 事务管理器应该根据核心配置文件中的相应配置,进行相应的灵活切换,故该属性应该面向接口编程
// */
// private Transaction transaction;

/**
* 事务管理器工厂属性
* 事务管理器工厂应该根据核心配置文件中的相应配置,进行相应的灵活切换,故该属性应该面向接口编程
*/
private TransactionFactory transactionFactory;

/**
* 数据源属性
* 数据源应该根据核心配置文件中的相应配置,进行相应的灵活切换,故该属性应该面向接口编程
*
* JDBC 事务管理器需要 Connection 连接对象进行事务管理,而 Connection 对象需要从
* 数据源中获取。因此在开启新会话,通过事务管理器工厂创建事务管理器时,需要传入该属性。
*/
private DataSource dataSource;

/**
* SQL 映射信息属性
* 存放 sql 语句的 map 集合
* key 为 sqlId
* value 为 sqlId 相应的标签信息对象(MappedStatement)
*/
private Map<String, MappedStatement> mappedStatements;

/**
* 获取 SQL 会话对象(开启事务)
*/
public SqlSession openSession() {
Transaction transaction = transactionFactory.newTransaction(dataSource, false);
SqlSession sqlSession = new SqlSession(transaction, mappedStatements);
return sqlSession;
}

public SqlSessionFactory() {
}

public SqlSessionFactory(TransactionFactory transactionFactory, DataSource dataSource, Map<String, MappedStatement> mappedStatements) {
this.transactionFactory = transactionFactory;
this.dataSource = dataSource;
this.mappedStatements = mappedStatements;
}
}

注意,事务管理器 Transaction 是在 openSession() 时才创建,创建后封装到 SqlSession 对象中。

改进 JdbcTransaction 实现

老杜原来的 JdbcTransaction 实现其实没啥大问题,不过既然都看过 Mybatis 源码了,那就尽量和源码保持一致吧。

首先是事务管理器接口 Transaction

没必要把 openConnection() 方法也定义到接口中,然后在 openSession() 中手动开启连接,这样显得有些多余了,这个方法完全可以隐藏在 getConnection() 实现中,对外只提供 getConnection() 方法即可(外层更关心的是获取连接对象来执行 SQL 语句)!

所有方法都加上了 SQLException,异常直接抛出,不再内部捕获打印了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface Transaction {
/**
* 提交事务
*/
void commit() throws SQLException ;

/**
* 回滚事务
*/
void rollback() throws SQLException ;

/**
* 关闭事务
*/
void close() throws SQLException ;

/**
* 获取事务所管理的连接对象
*/
Connection getConnection() throws SQLException;
}

然后 JdbcTransaction 实现作相应调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class JdbcTransaction implements Transaction {

/**
* 数据源属性
* 该数据源属性值是 SqlSessionFactory 中的数据源的副本
*/
private DataSource dataSource;

/**
* 自动提交标志
* true 表示自动提交,不开启事务
* false 表示自动提交,开启事务
*/
private boolean autoCommit;

/**
* 连接对象
*/
private Connection connection;

public JdbcTransaction(DataSource dataSource, boolean autoCommit) {
this.dataSource = dataSource;
this.autoCommit = autoCommit;
}

@Override
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
connection.commit();
}
}

@Override
public void rollback() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
connection.rollback();
}
}

@Override
public void close() throws SQLException {
if (connection != null) {
connection.close();
}
}

@Override
public Connection getConnection() throws SQLException {
if (connection == null) {
openConnection();
}
return connection;
}

private void openConnection() throws SQLException {
connection = dataSource.getConnection();
connection.setAutoCommit(autoCommit);
}
}

改进 SqlSessionFactory.build() 实现

SqlSessionFactory 的属性有变化,SqlSessionFactoryBuilderbuild 方法需要略作调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public class SqlSessionFactoryBuilder {
public SqlSessionFactoryBuilder() {
}

/**
* 解析 godbatis-config.xml 核心配置文件,根据其中配置信息创建 SqlSessionFactory 对象
* @param in 指向 godbatis-config.xml 核心配置文件
* @return SqlSessionFactory 对象
*/
public SqlSessionFactory build(InputStream in) {
SqlSessionFactory factory = null;
try {
// 解析核心配置文件
SAXReader reader = new SAXReader();
Document document = reader.read(in);
Element environments = (Element) document.selectSingleNode("/configuration/environments");
String defaultEnvId = environments.attributeValue("default");
Element environment = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultEnvId + "']");

Element transactionManagerElt = environment.element("transactionManager");
Element dataSourceElt = environment.element("dataSource");
List<String> resourceXmlPathList = environment.selectNodes("//mapper").stream()
.map(node -> ((Element) node).attributeValue("resource"))
.collect(Collectors.toList());

// 获取事务管理器工厂对象,数据源对象和 SQL 映射信息 Map 集合
TransactionFactory transactionFactory = getTransactionFactory(transactionManagerElt);
DataSource dataSource = getDataSource(dataSourceElt);
Map<String, MappedStatement> mappedStatements = getMappedStatements(resourceXmlPathList);

// 解析完成之后,创建 SqlSessionFactory 对象,并进行属性赋值,最后返回
factory = new SqlSessionFactory(transactionFactory, dataSource, mappedStatements);
} catch (Exception e) {
e.printStackTrace();
}
return factory;
}

/**
* 获取数据源对象
* @param dataSourceElt 数据源标签元素
* @return 数据源对象
*/
private DataSource getDataSource(Element dataSourceElt) {
DataSource dataSource = null;

String type = dataSourceElt.attributeValue("type").trim().toUpperCase();
List<Element> propertyElts = dataSourceElt.elements();
HashMap<String, String> map = new HashMap<>();
propertyElts.forEach(propertyElt -> {
String name = propertyElt.attributeValue("name");
String value = propertyElt.attributeValue("value");
map.put(name, value);
});

if (Const.UN_POOLED_DATASOURCE.equals(type)) {
dataSource = new UnPooledDataSource(map.get("driver"), map.get("url"), map.get("username"), map.get("password"));
}
if (Const.POOLED_DATASOURCE.equals(type)) {
dataSource = new PooledDataSource();
}
if (Const.JNDI_DATASOURCE.equals(type)) {
dataSource = new JndiDataSource();
}
return dataSource;
}

/**
* 获取事务管理器工厂对象
* @param transactionManagerElt 事务管理器标签元素
* @return 事务管理器工厂对象
*/
private TransactionFactory getTransactionFactory(Element transactionManagerElt) {
TransactionFactory transactionFactory = null;

String type = transactionManagerElt.attributeValue("type").trim().toUpperCase();
if (Const.JDBC_TRANSACTION.equals(type)) {
transactionFactory = new JdbcTransactionFactory();
}
if (Const.MANAGED_TRANSACTION.equals(type)) {
transactionFactory = new ManagedTransactionFactory();
}

return transactionFactory;
}

/**
* 获取 SQL 映射信息 Map 集合
* @param resourceXmlPathList 所有 SQL 映射文件的类路径
* @return SQL 映射信息 Map 集合
*/
private Map<String, MappedStatement> getMappedStatements(List<String> resourceXmlPathList) {
Map<String, MappedStatement> mappedStatements = new HashMap<>();

resourceXmlPathList.forEach(resourceXmlPath -> {
try {
SAXReader reader = new SAXReader();
Document document = reader.read(Resources.getResourceAsStream(resourceXmlPath));
Element mapper = document.getRootElement();
String namespace = mapper.attributeValue("namespace");
List<Element> elements = mapper.elements();
elements.forEach(element -> {
String id = element.attributeValue("id");
String sqlId = namespace + "." + id;
String resultType = element.attributeValue("resultType");
String sql = element.getTextTrim();
MappedStatement mappedStatement = new MappedStatement(sql, resultType);
mappedStatements.put(sqlId, mappedStatement);
});
} catch (Exception e) {
e.printStackTrace();
}
});
return mappedStatements;
}
}

改进 SqlSession 实现

SqlSession 的实现也需要调整,原来老杜是直接将 SqlSessionFactory 定义为属性。这里修改为,定义 TransactionMap<String, MappedStatement> 两个属性,这样 insert()selectOne() 方法也要相应调整。

另外,insert() 方法中给 jdbcSql? 占位符传值这块,我是直接用正则表达式来提取 #{} 中的属性名的,区别于老杜那种反复 indexOf + subString 的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
public class SqlSession {

/**
* 事务管理器属性
* 事务管理器应该根据核心配置文件中的相应配置,进行相应的灵活切换,故该属性应该面向接口编程
*
* SqlSession 需要连接对象构建 PreparedStatement 对象来执行 SQL 语句。
* 另外 SqlSession 也需要事务的提交回滚和关闭功能。因此需要该属性。
*/
private Transaction transaction;

/**
* SQL 映射信息属性
* 该 SQL 映射信息属性值是 SqlSessionFactory 中的 SQL 映射信息的副本
*
* SqlSession 需要执行 SQL 语句,因此需要该属性。
* 如 insert() selectOne() 等方法都要接收一个 sqlId,需要该属性来获取对应的 sql 语句信息。
*/
private Map<String, MappedStatement> mappedStatements;

public SqlSession(Transaction transaction, Map<String, MappedStatement> mappedStatements) {
this.transaction = transaction;
this.mappedStatements = mappedStatements;
}

/**
* 执行 insert 语句,向数据库表中插入记录
* @param sqlId sql 语句 id
* @param pojo 要插入的数据
* @return 受影响的行数
*/
public int insert(String sqlId, Object pojo) {
int count = 0;
try {
Connection connection = transaction.getConnection();
String godbatisSql = mappedStatements.get(sqlId).getSql();
String jdbcSql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_$\\s]+}", "?");
PreparedStatement ps = connection.prepareStatement(jdbcSql);

// 难点:如何给 jdbcSql 中的各个 ? 传对应的 pojo 的属性值
// ps.setString(第几个问号,传这个问号对应的 pojo 属性值)
// 简化:正常情况下,pojo 中的属性值可能是 int,double,String 等
// 这样需要根据属性的类型,调用对应的 setInt,setDouble,setString 方法
// 这里我们不做处理,统一使用 ps.setString,这就要求数据库表中的所有字段都是字符串类型,如 varchar
// 思路:通过正则表达式先对 godbatisSql 进行 #{} 中属性名的提取,有多少个 #{},就有多少个属性名。
// 获取到属性名后,对每个属性名拼接其 getter 方法名字符串,通过反射调用 getter 获取属性值,将每个值传给对应的 ? 占位符
Pattern pattern = Pattern.compile("#\\{([a-zA-Z0-9_$\\s]+)}");
Matcher matcher = pattern.matcher(godbatisSql);
int index = 1;
while (matcher.find()) {
String propertyName = matcher.group(1).trim();
String getterName = "get" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
Method getter = pojo.getClass().getDeclaredMethod(getterName);
Object propertyValue = getter.invoke(pojo);
ps.setString(index, propertyValue.toString());
index++;
}
count = ps.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}

return count;
}

/**
* 执行 select 语句,返回一个封装好的 Java 对象。该方法要求查询结果集只有一条结果记录
* 注意:该方法要求 sqlId 对应的 sql 语句只存在一个占位符
* @param sqlId sql 语句 id
* @param param 查询参数
* @return 一条查询结果对应的一个 Java 对象,如果没有查询到,返回 null
*/
public Object selectOne(String sqlId, Object param) {
Object obj = null;
try {
Connection connection = transaction.getConnection();

MappedStatement mappedStatement = mappedStatements.get(sqlId);
String resultType = mappedStatement.getResultType();
String godbatisSql = mappedStatement.getSql();

String jdbcSql = godbatisSql.replaceAll("#\\{([a-zA-Z0-9_$]+)}", "?");
PreparedStatement ps = connection.prepareStatement(jdbcSql);

// 给 jdbcSql 中的 ? 传值
// select 查询语句存在多个条件的情况是普遍存在的,因此 jdbcSql 中可能存在多个 ? 占位符
// 给多个 ? 占位符传对象对应的属性值,这个操作已经在 insert() 中实现过一遍,因此这里就不再实现,
// 直接规定 jdbcSql 中只存在一个 ? 占位符来简化问题!
// 下面的语句也反映了,对于只有一个占位符的情况,为什么 #{} 中的内容可以随便写
ps.setString(1, param.toString());

// 执行查询,获取结果集
ResultSet rs = ps.executeQuery();

// 从结果集中的取数据,如果有数据,封装为 resultType 指定类型的对象
if (rs.next()) {
// 通过反射机制,调用无参构造,来创建 resultType 指定类型的空对象(属性值均为空)
Class<?> resultTypeClass = Class.forName(resultType);
obj = resultTypeClass.getDeclaredConstructor().newInstance();
// 难点:如何给 obj 对象中的各个属性赋查询结果中的对应值
// 思路:查询结果集中包含列名,而一般要求,查询结果的列名和对象的属性名保持一致
// 这样就可以对列名(即属性名)拼装 setter 方法名字符串,通过反射调用 setter 设置对象相应的属性值
// 这也反映了,为什么查询结果需要 as 起别名,这主要是为了和属性名保持一致,方便反射调用
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 0; i < columnCount; i++) {
String propertyName = metaData.getColumnName(i + 1);
String setterName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
Method setter = resultTypeClass.getDeclaredMethod(setterName, String.class);
setter.invoke(obj, rs.getString(propertyName));
}
}

} catch (Exception e) {
e.printStackTrace();
}
return obj;
}

public void commit() {
try {
transaction.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}

public void rollback() {
try {
transaction.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}

public void close() {
try {
transaction.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

其他

编写测试程序时,需要导入 junitmysql-connector-java 依赖,注意 mysql-connector-java 依赖也要加上 <scope>test</scope>,表示 msyql 驱动仅用于测试,后续打包时,mysql 驱动不应该被打包进去!后续 godbatis 框架给人使用时,由用户指定使用什么驱动,比如如 oracle 驱动或 mysql 驱动等。

另外,最好将原本放在 main/resources 下的配置文件,放到 test/resources(而不是删除掉),这些配置文件将用于测试程序,这样打包源码后,测试程序依然可以正常运行。