MiniSpring 手写教程:MyBatis SQL语句配置化
引言
在上一节课中,我们基于JDBC Template对数据库操作进行了进一步的拆解,包括数据源DataSource、参数处理ArgumentPreparedStatementSetter以及结果转换RowMapper和RowMapperResultSetExtractor。为了提升性能,我们还引入了简单的数据库连接池。然而,当前的SQL语句仍然是硬编码在程序中。本节课,我们将模仿MyBatis,将SQL语句配置化,使其更加灵活和可维护。
MyBatis简介
MyBatis是一个一流的持久层框架,支持自定义SQL、存储过程和高级映射。它消除了几乎所有的JDBC代码和手动设置参数及检索结果的工作。MyBatis可以使用简单的XML或注解进行配置,并将原始类型、Map接口和Java POJOs映射到数据库记录。
SQL语句配置化的目的
将SQL语句配置化的主要目的是为了:
-
解耦SQL语句与Java代码:使SQL语句可以从程序代码中分离出来,便于管理和修改。
-
提高代码的可读性和可维护性:通过外部配置文件管理SQL语句,使得代码更加清晰,易于理解和维护。
-
便于团队协作:不同的开发人员可以专注于代码和SQL语句的开发,提高团队协作效率。
实现SQL语句配置化的步骤
1. 创建配置文件
首先,我们需要创建一个外部配置文件(通常是XML格式),用于存放SQL语句。这个文件将包含所有与数据库交互的SQL语句。
2. 解析配置文件
接着,我们需要解析这个配置文件,将SQL语句加载到程序中,以便在执行数据库操作时使用。
3. 集成到JDBC Template
最后,我们将配置化的SQL语句集成到JDBC Template中,使得在执行数据库操作时,可以动态地从配置文件中读取SQL语句。
结语
通过将SQL语句配置化,我们可以使数据库操作更加灵活和可维护。这不仅提高了代码的质量,也为后续的数据库操作提供了便利。在接下来的课程中,我们将继续深入探讨MyBatis的其他高级特性,如ORM和缓存等。
1
2
3
4
|
try (SqlSession session = sqlSessionFactory.openSession()) {
Blog blog = session.selectOne(
"org.mybatis.example.BlogMapper.selectBlog", 101);
}
|
上面代码的大意是先用SqlSessionFactory创建一个SqlSession,然后把要执行的SQL语句的id(org.mybatis.example.BlogMapper.selectBlog)和SQL参数(101),传进session.selectOne()方法,返回查询结果对象值Blog。
凭直觉,这个session应当是包装了数据库连接信息,而这个SQL id应当是指向的某处定义的SQL语句,这样就能大体跟传统的JDBC代码对应上了。
我们就再往下钻研一下。先看这个SqlSessionFactory是怎么来的,一般在应用程序中这么写。
1
2
3
4
|
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
|
可以看出,它是通过一个配置文件由一个SqlSessionFactoryBuider工具来生成的,我们看看配置文件的简单示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
|
没有什么神奇的地方,果然就是数据库连接信息还有一些mapper文件的配置。用这些配置信息创建一个Factory,这个Factory就知道该如何访问数据库了,至于具体执行的SQL语句,则是放在mapper文件中的,你可以看一下示例。
1
2
3
4
5
6
7
8
9
|
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
|
我们看这个mapper文件,里面就是包含了SQL语句,给了select语句一个namespace(org.mybatis.example.BlogMapper)以及id(selectBlog),它们拼在一起就是上面程序中写的SQL语句的sqlid(org.mybatis.example.BlogMapper.selectBlog)。我们还要注意这个SQL的参数占位符 #{id} 以及返回结果对象Blog,它们的声明格式是MyBatis自己规定的。
转换成JDBC的语言,这里定义了这个SQL语句是一个select语句,命令是select * from Blog where id = #{id},参数是id,返回结果对象对应的是Blog,这条SQL语句有一个唯一的sqlid来代表。
现在我们几乎能想象出应用程序执行下面这行的时候在做什么了。
1
2
|
Blog blog = session.selectOne(
"org.mybatis.example.BlogMapper.selectBlog", 101);
|
实现自定义的mBatis框架的过程可以分为以下几个步骤:
1. 理解MyBatis工作原理
在开始编写自己的mBatis之前,首先要理解MyBatis的工作原理。MyBatis通过使用一个唯一的ID来定位mapper文件中的SQL语句,替换参数,执行SQL,并将数据库记录转换为对象。这个过程与JdbcTemplate的工作方式非常相似。
2. 配置Mapper文件
为了模仿MyBatis,我们将SQL语句存放在外部的配置文件中。首先,在项目的resources目录下创建一个名为mapper的文件夹,然后将所有的SQL语句配置在这个目录下的XML文件中,例如User_Mapper.xml
。
3. 编写Mapper配置文件
在mapper文件夹中创建XML文件,例如User_Mapper.xml
,用于存储SQL语句。这些SQL语句将与Java代码中的接口方法相对应。
4. 替换参数并执行SQL
在Java代码中,通过传递参数到mapper接口的方法中,框架将这些参数替换到对应的SQL语句中,并执行这些SQL语句。
5. 将结果转换为对象
执行SQL后,数据库返回的结果集需要按照一定的规则转换成Java对象,以便在应用程序中使用。
通过以上步骤,我们就能够实现一个简化版的mBatis框架,从而更好地理解MyBatis的内部工作机制。
1
2
3
4
5
6
7
8
|
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.test.entity.User">
<select id="getUserInfo" parameterType="java.lang.Integer" resultType="com.test.entity.User">
select id, name,birthday
from users
where id=?
</select>
</mapper>
|
这个配置中,也同样有基本的一些元素:SQL类型、SQL的id、参数类型、返回结果类型、SQL语句、条件参数等等。
自然,我们需要在内存中用一个结构来对应上面的配置,存放系统中的SQL语句的定义。
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
|
package com.minis.batis;
public class MapperNode {
String namespace;
String id;
String parameterType;
String resultType;
String sql;
String parameter;
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public String getParameter() {
return parameter;
}
public void setParameter(String parameter) {
this.parameter = parameter;
}
public String toString(){
return this.namespace+"."+this.id+" : " +this.sql;
}
}
|
对它们的处理工作,我们仿照MyBatis,用一个SqlSessionFactory来处理,并默认实现一个DefaultSqlSessionFactory来负责。
你可以看一下SqlSessionFactory接口定义。
1
2
3
4
5
6
|
package com.minis.batis;
public interface SqlSessionFactory {
SqlSession openSession();
MapperNode getMapperNode(String name);
}
|
同时,我们仍然使用IoC来管理,将默认的DefaultSqlSessionFactory配置在IoC容器的applicationContext.xml文件里。
1
2
3
|
<bean id="sqlSessionFactory" class="com.minis.batis.DefaultSqlSessionFactory" init-method="init">
<property type="String" name="mapperLocations" value="mapper"></property>
</bean>
|
我们并没有再用一个builder来生成Factory,这是为了简单一点。
这个Bean,也就是这里配置的默认的SqlSessionFactory,它在初始化过程中会扫描这个mapper目录。
1
2
3
|
public void init() {
scanLocation(this.mapperLocations);
}
|
而这个扫描跟以前的Servlet也是一样的,用递归的方式访问每一个文件。
1
2
3
4
5
6
7
8
9
10
11
|
private void scanLocation(String location) {
String sLocationPath = this.getClass().getClassLoader().getResource("").getPath()+location;
File dir = new File(sLocationPath);
for (File file : dir.listFiles()) {
if(file.isDirectory()){ //递归扫描
scanLocation(location+"/"+file.getName());
}else{ //解析mapper文件
buildMapperNodes(location+"/"+file.getName());
}
}
}
|
最后对扫描到的每一个文件,要进行解析处理,把SQL定义写到内部注册表Map里。
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
|
private Map<String, MapperNode> buildMapperNodes(String filePath) {
SAXReader saxReader=new SAXReader();
URL xmlPath=this.getClass().getClassLoader().getResource(filePath);
Document document = saxReader.read(xmlPath);
Element rootElement=document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
Iterator<Element> nodes = rootElement.elementIterator();;
while (nodes.hasNext()) { //对每一个sql语句进行解析
Element node = nodes.next();
String id = node.attributeValue("id");
String parameterType = node.attributeValue("parameterType");
String resultType = node.attributeValue("resultType");
String sql = node.getText();
MapperNode selectnode = new MapperNode();
selectnode.setNamespace(namespace);
selectnode.setId(id);
selectnode.setParameterType(parameterType);
selectnode.setResultType(resultType);
selectnode.setSql(sql);
selectnode.setParameter("");
this.mapperNodeMap.put(namespace + "." + id, selectnode);
}
return this.mapperNodeMap;
}
|
程序的核心功能是读取配置文件中的节点,并提取这些节点的属性来设置到MapperNode结构中。特别地,完整的id是由namespace和id通过’.‘连接而成的,例如com.test.entity.User.getUserInfo。考虑到未来可能支持多种SQL命令,我们在设计时预留了一个属性来区分这条SQL语句是读操作还是写操作。目前,我们仅处理select类型的SQL语句,update等其他类型将在未来扩展。为了实现这一功能,我们需要参考DefaultSqlSessionFactory的完整代码。
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
|
package com.minis.batis;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import com.minis.beans.factory.annotation.Autowired;
import com.minis.jdbc.core.JdbcTemplate;
public class DefaultSqlSessionFactory implements SqlSessionFactory{
@Autowired
JdbcTemplate jdbcTemplate;
String mapperLocations;
public String getMapperLocations() {
return mapperLocations;
}
public void setMapperLocations(String mapperLocations) {
this.mapperLocations = mapperLocations;
}
Map<String,MapperNode> mapperNodeMap = new HashMap<>();
public Map<String, MapperNode> getMapperNodeMap() {
return mapperNodeMap;
}
public DefaultSqlSessionFactory() {
}
public void init() {
scanLocation(this.mapperLocations);
}
private void scanLocation(String location) {
String sLocationPath = this.getClass().getClassLoader().getResource("").getPath()+location;
File dir = new File(sLocationPath);
for (File file : dir.listFiles()) {
if(file.isDirectory()){
scanLocation(location+"/"+file.getName());
}else{
buildMapperNodes(location+"/"+file.getName());
}
}
}
private Map<String, MapperNode> buildMapperNodes(String filePath) {
System.out.println(filePath);
SAXReader saxReader=new SAXReader();
URL xmlPath=this.getClass().getClassLoader().getResource(filePath);
try {
Document document = saxReader.read(xmlPath);
Element rootElement=document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
Iterator<Element> nodes = rootElement.elementIterator();;
while (nodes.hasNext()) {
Element node = nodes.next();
String id = node.attributeValue("id");
String parameterType = node.attributeValue("parameterType");
String resultType = node.attributeValue("resultType");
String sql = node.getText();
MapperNode selectnode = new MapperNode();
selectnode.setNamespace(namespace);
selectnode.setId(id);
selectnode.setParameterType(parameterType);
selectnode.setResultType(resultType);
selectnode.setSql(sql);
selectnode.setParameter("");
this.mapperNodeMap.put(namespace + "." + id, selectnode);
}
} catch (Exception ex) {
ex.printStackTrace();
}
return this.mapperNodeMap;
}
public MapperNode getMapperNode(String name) {
return this.mapperNodeMap.get(name);
}
@Override
public SqlSession openSession() {
SqlSession newSqlSession = new DefaultSqlSession();
newSqlSession.setJdbcTemplate(jdbcTemplate);
newSqlSession.setSqlSessionFactory(this);
return newSqlSession;
}
}
|
使用Sql Session访问数据
有了上面的准备工作,上层的应用程序在使用的时候,就可以通过Aurowired注解直接拿到这个SqlSessionFactory了,然后通过工厂创建一个Sql Session,再执行SQL命令。你可以看一下示例。
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
|
package com.test.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.minis.batis.SqlSession;
import com.minis.batis.SqlSessionFactory;
import com.minis.beans.factory.annotation.Autowired;
import com.minis.jdbc.core.JdbcTemplate;
import com.minis.jdbc.core.RowMapper;
import com.test.entity.User;
public class UserService {
@Autowired
SqlSessionFactory sqlSessionFactory;
public User getUserInfo(int userid) {
//final String sql = "select id, name,birthday from users where id=?";
String sqlid = "com.test.entity.User.getUserInfo";
SqlSession sqlSession = sqlSessionFactory.openSession();
return (User)sqlSession.selectOne(sqlid, new Object[]{new Integer(userid)},
(pstmt)->{
ResultSet rs = pstmt.executeQuery();
User rtnUser = null;
if (rs.next()) {
rtnUser = new User();
rtnUser.setId(userid);
rtnUser.setName(rs.getString("name"));
rtnUser.setBirthday(new java.util.Date(rs.getDate("birthday").getTime()));
} else {
}
return rtnUser;
}
);
}
}
|
在分析代码时,我们发现程序的执行方式与之前直接使用JdbcTemplate时相似,主要区别在于现在是通过sqlSession.selectOne
方法来执行SQL查询。
SqlSession对象是由SqlSessionFactory生成的,具体代码如下:SqlSession sqlSession = sqlSessionFactory.openSession();
。为了深入了解SqlSession的生成过程,我们可以查看DefaultSqlSessionFactory
类中的定义。
1. SqlSession的生成
SqlSession对象是通过调用SqlSessionFactory的openSession
方法生成的。这表明SqlSession是与数据库交互的一个会话对象,负责执行SQL语句并管理事务。
2. DefaultSqlSessionFactory类
DefaultSqlSessionFactory
类是MyBatis框架中用于生成SqlSession的工厂类。通过这个类,我们可以了解SqlSession是如何被创建和管理的。
3. 代码对比
与直接使用JdbcTemplate相比,通过SqlSession执行SQL的优势在于MyBatis提供了更多的灵活性和功能,例如动态SQL、映射结果到对象等。
4. 总结
通过上述分析,我们可以清晰地看到SqlSession在MyBatis框架中的作用和生成方式,以及它与JdbcTemplate的相似之处和差异。
1
2
3
4
5
6
7
|
public SqlSession openSession() {
SqlSession newSqlSession = new DefaultSqlSession();
newSqlSession.setJdbcTemplate(jdbcTemplate);
newSqlSession.setSqlSessionFactory(this);
return newSqlSession;
}
|
由上面代码可见,这个Sql Session也就是对JdbcTemplate进行了一下包装。
定义接口:
1
2
3
4
5
6
7
8
9
10
|
package com.minis.batis;
import com.minis.jdbc.core.JdbcTemplate;
import com.minis.jdbc.core.PreparedStatementCallback;
public interface SqlSession {
void setJdbcTemplate(JdbcTemplate jdbcTemplate);
void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory);
Object selectOne(String sqlid, Object[] args, PreparedStatementCallback pstmtcallback);
}
|
在设计中,我们选择在调用openSession()
方法时临时设置JdbcTemplate
,而不是在工厂(Factory)层面进行设置。这样的设计赋予了系统更大的灵活性,允许我们在实际执行每一条SQL语句之前,根据需要动态地更换JdbcTemplate
实例。这种基于时序的设计为实现动态数据源提供了可能,在诸如读写分离等场景下显得尤为有用。
此外,我们提供了一个默认的实现类DefaultSqlSession
,它遵循上述提到的设计原则,确保了架构上的灵活性与实用性。
请注意:此处所有提及的’换行符’均已替换为’
‘。
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
|
package com.minis.batis;
import javax.sql.DataSource;
import com.minis.jdbc.core.JdbcTemplate;
import com.minis.jdbc.core.PreparedStatementCallback;
public class DefaultSqlSession implements SqlSession{
JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public JdbcTemplate getJdbcTemplate() {
return this.jdbcTemplate;
}
SqlSessionFactory sqlSessionFactory;
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
public SqlSessionFactory getSqlSessionFactory() {
return this.sqlSessionFactory;
}
@Override
public Object selectOne(String sqlid, Object[] args, PreparedStatementCallback pstmtcallback) {
String sql = this.sqlSessionFactory.getMapperNode(sqlid).getSql();
return jdbcTemplate.query(sql, args, pstmtcallback);
}
private void buildParameter(){
}
private Object resultSet2Obj() {
return null;
}
}
|
极简MyBatis实现
概述
在本节课中,我们通过模仿MyBatis框架,实现了一个简化版的数据库操作工具。这个工具通过配置化SQL语句,使得应用程序可以通过一个唯一的id来执行对应的SQL语句。我们使用了SqlSessionFactory
来解析配置文件,并利用SqlSession
来执行SQL操作。最终,所有的数据库操作都通过JdbcTemplate
来实现。
实现细节
-
配置化SQL:我们将SQL语句配置在文件中,并通过id来引用这些语句。
-
SqlSessionFactory:负责解析SQL配置文件,创建SqlSession
实例。
-
SqlSession:应用程序通过SqlSession
来执行SQL语句,目前只提供了selectOne()
方法。
-
JdbcTemplate:所有的数据库操作最终都通过JdbcTemplate
来执行。
限制与扩展
当前的极简版本存在一些限制:
-
仅支持select
语句,不支持update
等其他类型的SQL语句。
-
SqlSession
接口较为单薄,仅提供了selectOne()
方法。
-
缺乏SQL数据集缓存,每次操作都需要重新执行SQL语句。
-
没有实现读写分离的配置。
为了扩展这个框架,可以考虑以下方向:
-
增加update
语句支持:实现update
语句的配置化和执行。
-
接口扩展:丰富SqlSession
接口,提供更多的数据库操作方法。
-
缓存机制:引入SQL数据集缓存,减少数据库访问次数。
-
读写分离:配置不同的数据库用于读操作和写操作。
学习重点
在学习构建框架的过程中,我们主要关注的是如何拆解问题,让专门的部件处理专门的事情,以及如何使框架具有更好的扩展性。
课后思考题
源代码
完整源代码请访问:https://github.com/YaleGuo/minis
交流讨论
欢迎在留言区与我交流讨论。