Mybatis源码解析准备工作
tips:mybatis
文章目录
一、搭建Mybatis源码环境
1、环境要求:
JDK1.8、Maven、Eclipse或者IntelliJ IDEA
2、获取源码
- Mybatis源码:
https://github.com/mybatis/mybatis-3
- Mybatis和Spring整合源码:
https://github.com/mybatis/spring
- Mybatis父类源码:
https://github.com/mybatis/parent
3、导入源码到IDE中
-
File => New => Project… => Empty Project => Next => 命名 => Finish
-
File => New => Module from Existing Sources… => 分别选择三个源码和一个测试代码,分别导入
4、HSQLDB简介
- Mybatis内嵌HSQLDB的内存模式作为单元测试的数据库,因此不用安装Mysql等数据库。
- 内存模式用户名和密码可以随便指定,但是如果是需要持久化在本地,则需要指定用户名和密码。
- 单元测试不用在意,可以随便指定。默认用户名是sa,密码为空。
- ScriptRunner用来执行(XXX.sql)脚本,SqlRunner用来执行sql语句(增删改查语句)。
5、源码解析入口:
public class Example01 {
private Connection conn = null;
@Before
public void initData() {
try {
//Example02 example02 = new Example02();//調用方法的時候回提示,引入依賴,但是調用common裡面的文件的時候卻不用。
// 加载HSQLDB驱动
Class.forName("org.hsqldb.jdbcDriver");
// 获取Connection对象,用户名和密码可以随便指定
conn = DriverManager.getConnection("jdbc:hsqldb:mem:mybatis",
"satestlalla", "");
// 使用Mybatis的ScriptRunner工具类执行数据库脚本
ScriptRunner scriptRunner = new ScriptRunner(conn);
scriptRunner.setLogWriter(null);
//文件都放在mybatis-common module的resource下面
scriptRunner.runScript(Resources.getResourceAsReader("create-table.sql"));
scriptRunner.runScript(Resources.getResourceAsReader("init-data.sql"));
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void testHsqldbQuery() {
// SqlRunner是Mybatis封装的操作数据库的工具类
SqlRunner sqlRunner = new SqlRunner(conn);
try {
//调用SqlRunner类的selectAll()方法查询数据
List<Map<String, Object>> results = sqlRunner.selectAll("select * from user");
results.forEach(System.out::println);//jdk8之后新特性
sqlRunner.closeConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
二、JDBC规范
1、jdbc规范文档
- JDBC4.2规范文档下载地址:https://download.oracle.com/otndocs/jcp/jdbc-4_2-mrel2-spec/index.html
2、操作数据库步骤
- 建立与数据连接
- 执行SQL语句
- 检索SQL执行结果
- 关闭连接
3、建立与数据库连接
三种方式:
-
DriverManager——JDBC1.0提供
需要显示加载驱动类:
Class.forName("org.hsqldb.jdbcDriver");
-
DataSource——JDBC2.0提供(建议使用,配置更加灵活)
- DataSource:最基本类
- DataSourceFactory:工厂类
- ConnectionPoolDataSource:支持缓存和复用Connection对象
- XADataSource:支持分布式事务
源码如下:
//第一种方式
// 加载驱动 到JDBC4之后不需要直接声明驱动,可以在META-INF.services目录下面创建一个跟驱动同名的文件即可,参考chapter02.SPIExample
/*原理如下
ServiceLoader<Driver> drivers = ServiceLoader.load(java.sql.Driver.class);
for (Driver driver : drivers ) {
System.out.println(driver.getClass().getName());
}
*/
Class.forName("org.hsqldb.jdbcDriver");
// 获取Connection对象
Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:mybatis",
"sa", "");
//第二种方式
// 创建DataSource实例
DataSource dataSource = new UnpooledDataSource("org.hsqldb.jdbcDriver","jdbc:hsqldb:mem:mybatis", "sa", "");
// 获取Connection对象
Connection connection = dataSource.getConnection();
//第三种方式
// 创建DataSource实例
DataSourceFactory dsf = new UnpooledDataSourceFactory();
Properties properties = new Properties();
InputStream configStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("database.properties");
properties.load(configStream);
dsf.setProperties(properties);
DataSource dataSource = dsf.getDataSource();
// 获取Connection对象
Connection connection = dataSource.getConnection();
//插入操作
Statement statement = connection.createStatement();
statement.addBatch("insert into " +
"user(create_time, name, password, phone, nick_name) " +
"values('2010-10-24 10:20:30', 'User1', 'test', '18700001111', 'User1');");
statement.addBatch("insert into " +
"user (create_time, name, password, phone, nick_name) " +
"values('2010-10-24 10:20:30', 'User2', 'test', '18700002222', 'User2');");
statement.executeBatch();
//PreparedStatement可配置参数方式操作数据库
PreparedStatement stmt = connection.prepareStatement("insert into " +
"user(create_time, name, password, phone, nick_name) " +
"values(?,?,?,?,?);");
stmt.setString(1,"2010-10-24 10:20:30");
stmt.setString(2,"User1");
stmt.setString(3,"test");
stmt.setString(4,"18700001111");
stmt.setString(5,"User1");
ParameterMetaData pmd = stmt.getParameterMetaData();
for(int i = 1; i <= pmd.getParameterCount(); i++) {
String typeName = pmd.getParameterTypeName(i);
String className = pmd.getParameterClassName(i);
System.out.println("第" + i + "个参数," + "typeName:" + typeName + ", className:" + className);
}
stmt.execute();
//自增长主键按,可以获取数据库里面自增长的当前值
Statement stmt = conn.createStatement();
String sql = "insert into user(create_time, name, password, phone, nick_name) " +
"values('2010-10-24 10:20:30','User1','test','18700001111','User1');";
ResultSet genKeys = stmt.getGeneratedKeys();
stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
genKeys = stmt.getGeneratedKeys();
if(genKeys.next()) {
System.out.println("自增长主键:" + genKeys.getInt(1));
}
//数据库配置信息
DatabaseMetaData dmd = conn.getMetaData();
System.out.println("数据库URL:" + dmd.getURL());
System.out.println("数据库用户名:" + dmd.getUserName());
System.out.println("数据库产品名:" + dmd.getDatabaseProductName());
System.out.println("数据库产品版本:" + dmd.getDatabaseProductVersion());
System.out.println("驱动主版本:" + dmd.getDriverMajorVersion());
System.out.println("驱动副版本:" + dmd.getDriverMinorVersion());
System.out.println("数据库供应商用于schema的首选术语:" + dmd.getSchemaTerm());
System.out.println("数据库供应商用于catalog的首选术语:" + dmd.getCatalogTerm());
System.out.println("数据库供应商用于procedure的首选术语:" + dmd.getProcedureTerm());
System.out.println("null值是否高排序:" + dmd.nullsAreSortedHigh());
System.out.println("null值是否低排序:" + dmd.nullsAreSortedLow());
System.out.println("数据库是否将表存储在本地文件中:" + dmd.usesLocalFiles());
System.out.println("数据库是否为每个表使用一个文件:" + dmd.usesLocalFilePerTable());
System.out.println("数据库SQL关键字:" + dmd.getSQLKeywords());
//保存节点savepoint,可以回滚使用。
String sql1 = "insert into user(create_time, name, password, phone, nick_name) " +
"values('2010-10-24 10:20:30','User1','test','18700001111','User1')";
String sql2 = "insert into user(create_time, name, password, phone, nick_name) " +
"values('2010-10-24 10:20:30','User2','test','18700001111','User2')";
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
stmt.executeUpdate(sql1);
// 创建保存点
Savepoint savepoint = conn.setSavepoint("SP1");
stmt.executeUpdate(sql2);
// 回滚到保存点
conn.rollback(savepoint);
conn.commit();
//后续打印统一代码
ResultSet rs = conn.createStatement().executeQuery("select * from user ");
//上面是下面的简单缩写
//Statement statement = connection.createStatement();
//ResultSet resultSet = statement.executeQuery("select * from user");
// 遍历ResultSet
ResultSetMetaData metaData = resultSet.getMetaData();
int columCount = metaData.getColumnCount();
while (resultSet.next()) {
for (int i = 1; i <= columCount; i++) {
String columName = metaData.getColumnName(i);
String columVal = resultSet.getString(columName);
System.out.println(columName + ":" + columVal);
}
System.out.println("--------------------------------------");
}
// 关闭连接
IOUtils.closeQuietly(statement);
IOUtils.closeQuietly(connection);
三、Mybatis常用工具
1、使用SQL类生成语句:
传统通过字符串拼接,需要注意空格等细节,非常麻烦,通过SQL封装类可以解决这个问题。
两种实现方式:
-
new SQL().SELECT().SELECT().FROM()....
-
new SQL(){{SELECT();SELECT();FROM();....}}
具体代码如下:
String insertSql = new SQL().
INSERT_INTO("PERSON").
VALUES("ID, FIRST_NAME", "#{id}, #{firstName}").
VALUES("LAST_NAME", "#{lastName}").toString();
/*结果是:
INSERT INTO PERSON
(ID, FIRST_NAME, LAST_NAME)
VALUES (#{id}, #{firstName}, #{lastName})
*/
String sql_area = new SQL()
{
{
SELECT("P.ID, P.USERNAME, P.PASSWORD");
SELECT("P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
WHERE("P.ID = #{id}");
WHERE("P.FIRST_NAME = ${first_name}");
}
}.toString();
/*结果是:
SELECT P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME
FROM PERSON P
WHERE (P.ID = #{id} AND P.FIRST_NAME = ${first_name})
*/
2、使用ScriptRunner执行脚本
源码逻辑:
- 调用setAuoCommit()方法,根据autoCommit属性的值设置事务是否自动提交。
- 判断sendFullScript属性值,如果为true,则调用executeFullScript()方法一次性读取文件内容,然后调用Statement对象的execute()方法一次性执行所有SQL语句。
- 如果sendFullScript属性值为false,则调用executeLineByLine()方法逐行读取SQL,以分好作为一条SQL语句结束的标志,逐行执行SQL语句。
3、使用SqlRunner执行脚本
-
类方法汇总:
-
closeConnection:关闭Connection连接。
-
selectOne、selectAll:查找一条记录(返回一条),查找所有记录(返回多条)。
-
insert、update、delete:插入、更新、删除操作
-
run:执行一条sql语句,最好是DDL
-
源码逻辑:
-
调用Connection对象的prepareStatement()方法获取PreparedStatement对象后,调用setParameters()方法为SQL中的参数占位符赋值,同时要判断每个参数的类型。
-
调用PreparedStatement的executeQuery()方法执行查询操作
-
调用getResult()方法,将ResultSet对象转换为List对象,其中List的每各Map对象对应一条记录。
四、涉及设计模式
-
Builder模式:例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;
-
工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;
-
单例模式:例如ErrorContext和LogFactory;
-
代理模式:Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果;
-
组合模式:例如SqlNode和各个子类ChooseSqlNode等;
-
模板方法模式:例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler;
-
适配器模式:例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现;
-
装饰者模式:例如Cache包中的cache.decorators子包中等各个装饰者的实现;
-
迭代器模式:例如迭代器模式PropertyTokenizer;
五、源码逻辑
XML解析 =》 获取Configuration =》 包含DateSource数据源、statementId和MappedStatement对应的Map变量 =》 MappedStatement包含sqlSource、参数和返回值类型等XML解析信息 =》 sqlSouce接口实现多种类,包含了SqlNode接口的类 (XML解析原始的sql,只有执行的时候才会知道具体sql,因为有判断和具体属性值)和获取BoundSql方法(执行时候原始sql经过解析的结果,只是封装在一个类中而已)=》 Sqlnode实现类就是一个列表,列表中元素是树形结构,存储sql标签下面的文本和标签循环。同时SqlSouce实现类有一个BoundSql属性值,Sqlsouce是获取的XML解析之后的,boundSql是执行时候获取的XML解析结果再解析的。=》根据解析结果执行sql