目录
MyBatis 是一个非常优秀的持久层应用框架,目前几乎已经一统天下。既然是持久层框架,那么一定是对于数据库的操作,Java 中谈到数据库操作,一定少不了 JDBC。那么 MyBatis 比传统的 JDBC 好在哪那?MyBatis 又在哪方面做了优化那?
如果我们需要查询所有用户,传统的 JDBC 会这样写。
Copy
public static void main(String[] args) { //声明Connection对象 Connection con = null; try { //加载驱动程序 Class.forName("com.mysql.jdbc.Driver"); //创建 connection 对象 con = DriverManager.getConnection("jdbc:mysql://localhost:3306/db","username","password"); //使用 connection 对象创建statement 或者 PreparedStatement 类对象,用来执行SQL语句 Statement statement = con.createStatement(); //要执行的SQL语句 String sql = "select * from user"; //3.ResultSet类,用来存放获取的结果集!! ResultSet rs = statement.executeQuery(sql); String job = ""; String id = ""; while(rs.next()){ //获取job这列数据 job = rs.getString("job"); //获取userId这列数据 id = rs.getString("userId"); //输出结果 System.out.println(id + "\t" + job); } } catch(ClassNotFoundException e) { e.printStackTrace(); } catch(SQLException e) { //数据库连接失败异常处理 e.printStackTrace(); }catch (Exception e) { e.printStackTrace(); }finally{ rs.close(); con.close(); } }
通过上面的代码,我们可以将 JDBC 对于数据库的操作总结为以下几个步骤:
传统的 JDBC 操作的问题也一目了然,整体非常繁琐,也不够灵活,执行一个 SQL 查询就要写一堆代码。
来看看 MyBatis 代码如何查询数据库。几行代码就完成了数据库查询操作,并且将数据库查询出来的结果映射到了 JavaBean 中了。我们的代码没有加入 Spring Mybatis,加入 Spring 后整体流程会复杂很多,不方便我们理解。
Copy
//获取 sqlSession,sqlSession 相当于传统 JDBC 的 Conection public static SqlSession getSqlSession(){ InputStream configFile = new FileInputStream(filePath); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile); return sqlSessionFactory.openSession(); } //使用 sqlSession 获得对应的 mapper,mapper 用来执行 sql 语句。 public static User get(SqlSession sqlSession, int id){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); return userMapper.selectByPrimaryKey(id); }
我们来对 MyBatis 操作数据库做一个总结:
大家平时应该经常使用 MyBatis 框架,对于 SqlSessionFactory、SqlSession、Mapper 等也有一些概念。下面我们从源码来分析怎么实现这些概念。
前置知识#
先给出一个大部分框架的代码流程,方便大家理解框架。下面的图片就说明了接口、抽象类和实现类的关系,我们自己写代码时也要多学习这种思想。
带着结果看过程
看源码对于很多人来说都是一个比较枯燥和乏味的过程,如果不做抽象和总结,会觉得非常乱。另外,看源码不要去扣某个细节,尽量从宏观上理解它。这样带着结果看过程你就会知道设计者为什么这么做。
先给出整个 MyBatis 框架的架构图,大家先有一个印象:
原理分析#
说明,我们讲解的是原生的 MyBatis 框架,并不是与 Spring 结合的 MyBatis 框架。
还是把上面 MyBatis 操作数据库的代码拿过来,方便我们与源码对照。
Copy
//获取 sqlSession,sqlSession 相当于传统 JDBC 的 Conection public static SqlSession getSqlSession(){ //步骤一 InputStream configFile = new FileInputStream(filePath); //步骤二 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile); return sqlSessionFactory.openSession(); } //使用 sqlSession 获得对应的 mapper,mapper 用来执行 sql 语句。 public static User get(SqlSession sqlSession, int id){ //步骤三 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); return userMapper.selectByPrimaryKey(id); }
MyBatis 框架的第一步就是加载我们数据库的相关信息,比如用户名、密码等。以及我们在 XML 文件中写的 SQL 语句。
Copy
//配置文件中指定了数据库相关的信息和写 sql 语句的 mapper 相关信息,稍后我们需要读取并加载到我们的配置类中。
第二步就是通过读取到的配置文件信息,构建一个 SqlSessionFactory。
通过 openSession 方法返回了一个 sqlSession,我们来看看 openSession 方法做了什么。
Copy
//我们来重点看看 openSession 做了什么操作, DefaultSqlSessionFactory.java @Override public SqlSession openSession() { return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false); } public Configuration getConfiguration() { return this.configuration; } //这个函数里面有着事务控制相关的代码。 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; DefaultSqlSession var8; try { Environment environment = this.configuration.getEnvironment(); TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment); //根据上面的参数得到 TransactionFactory,通过 TransactionFactory 生成一个 Transaction,可以理解为这个 SqlSession 的事务控制器 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 将这个事务控制器封装在 Executor 里 Executor executor = this.configuration.newExecutor(tx, execType); // 使用 configuration 配置类,Executor,和 configuration(是否自动提交) 来构建一个 DefaultSqlSession。 var8 = new DefaultSqlSession(this.configuration, executor, autoCommit); } catch (Exception var12) { this.closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12); } finally { ErrorContext.instance().reset(); } return var8; }
看了上面的一大段代码你可能会觉得蒙,没关系,我们来划重点,最终结果返回了一个 DefaultSqlsession。
Copy
// 使用 configuration 配置类(我们上面读取的配置文件就需要加载到这个类中),Executor(包含了数据事务控制相关信息),和 autoCommit(是否自动提交) 来构建一个 DefaultSqlSession。 var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
有了这个 sqlSession 之后,我们就可以实现所有对数据库的操作了,因为我们已经把所有的信息加载到这里面了。数据库信息、SQL 信息、SQL 语句执行器等。当然我们一般使用这个 sqlSession 获得对应的 mapper 接口类,然后用这个接口类查询数据库。
既然所有东西都封装在 sqlSession 中,先来看看 sqlSession 的组成部分。
SqlSession 的接口定义:里面定义了增删改查和提交回滚等方法。
Copy
public interface SqlSession extends Closeable {
接下来用 sqlSession 获取对应的 Mapper。
DefaultSqlSession 的 getMapper 实现:
Copy
public
MapperRegistry 里 getMapper 的最终实现,同时我们需要思考一个问题,我们的 sqlSession 接口里面只定义了抽象的增删改查,而这个接口并没有任何实现类,那么这个 XML 到底是如何与接口关联起来并生成实现类那?通过 MapperRegistry 可以得出答案,那就是动态代理。
Copy
public class MapperRegistry { private final Configuration config; // 用一个 Map 来存储接口和 xml 文件之间的映射关系,key 应该是接口,但是 value 是 MapperProxyFactory private final Map
最终的结果是生成一个 mapper 接口的动态代理类,通过这个类,我们实现对数据库的增删改查。
接下来我们看看 newInstance 的具体实现:
Copy
public T newInstance(SqlSession sqlSession) { // mapperInterface 就是接口 MapperProxy
为什么说这里的动态代理有一些不一样那?我们先看看正常流程的动态代理,接口,和接口实现类是必须的。而我们的 Mapper 接口只有充满了 SQL 语句的 XML 文件,没有具体实现类。
与传统的动态代理相比,MyBatis 的 Mapper 接口是没有实现类的,那么它又是怎么实现动态代理的那?
我们来看一下 MapperProxy 的源码:
Copy
public class MapperProxy
来看 MapperMethod 的定义:
Copy
// command 里面包含了方法名,比如 com.paul.pkg.selectByPrimaryKey // type, 表示是 SELECT,UPDATE,INSERT,或者 DELETE // method 是方法的签名 public class MapperMethod { private final MapperMethod.SqlCommand command; private final MapperMethod.MethodSignature method; public MapperMethod(Class> mapperInterface, Method method, Configuration config) { this.command = new MapperMethod.SqlCommand(config, mapperInterface, method); this.method = new MapperMethod.MethodSignature(config, mapperInterface, method); } } ``` ```java public Object execute(SqlSession sqlSession, Object[] args) { Object result;//返回结果 //INSERT操作 if (SqlCommandType.INSERT == command.getType()) { //处理参数 Object param = method.convertArgsToSqlCommandParam(args); //调用sqlSession的insert方法 result = rowCountResult(sqlSession.insert(command.getName(), param)); } ..... ..... }
通过 sqlSession 来执行我们的 SQL 语句,返回结果,动态代理的方法调用结束。
进入 DefaultSqlSession 执行对应的 SQL 语句。
Copy
public
Executor 的实现类里面执行 query 方法。
Copy
public
整体流程
项目整体使用 Maven 构建,mybatis-demo 是脱离 Spring 的 MyBatis 使用的例子,大家可以先熟悉以下 Mybatis 框架如何使用,代码就不在讲解了。paul-mybatis 是我们自己实现的 MyBatis 框架。
首先按照我们以前的使用 MyBatis 代码时的流程,创建 Mapper 接口、XML 文件,和 POJO 以及集一些配置文件,这几个文件我们和 mybatis-demo 创建一样的即可,方便我们比较结果。
Mapper 接口,这里面定义两个抽象方法,根据主键查找用户和查找所有用户:
Copy
package com.paul.mybatis.mapper; import com.paul.mybatis.entity.User; import java.util.List; public interface UserMapper { User selectByPrimaryKey(long userId); List
XML 文件,里面是上面两个抽象方法的具体 SQL 实现,完全消防官方 XML 文件的写法,需要注意 namespace、id、resultType、SQL 语句这几个点,都是我们后面代码需要处理的。
Copy
最后是我们的实体类,它的属性与数据库的表相对应:
Copy
package com.paul.mybatis.entity; public class User { private long userId; private String userName; private int sex; private String role; public long getUserId() { return userId; } public void setUserId(long userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public int getSex() { return sex; } public void setSex(int sex) { this.sex = sex; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } }
最后一个配置文件,数据库连接配置文件 db.propreties:
Copy
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
配置文件和一些测试的必须类已经写完了,首先我们需要把这些配置信息加载到 Configuration 配置类中。
先定义一个类来加载写 SQL 语句的 XML 文件,上面我们说过要注意四个点,namespace、id、resultType、SQL 语句,我们写对应的属性来保存它,代码很简单,就不多讲了。
Copy
package com.paul.mybatis.confiuration; /** * * XML 中的 sql 配置信息加载到这个类中 * */ public class MappedStatement { private String namespace; private String id; private String resultType; private String sql; 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 getResultType() { return resultType; } public void setResultType(String resultType) { this.resultType = resultType; } public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } }
接下来我们定义一个 Configuration 总配置类,来保存 db.propeties 里面的属性和 XML 文件的 SQL 信息,Configuration 类里面的文件对应我们配置文件中的属性。
Copy
package com.paul.mybatis.confiuration; import java.util.HashMap; import java.util.List; import java.util.Map; /** * * 所有的配置信息 * */ public class Configuration { private String jdbcDriver; private String jdbcUrl; private String jdbcPassword; private String jdbcUsername; private Map
按照上面的流程图,我们来创建一个 SqlSessionFactory 工厂类,这个类有两个功能,一个是加载配置文件信息到 Configuration 类中,另一个是创建 SqlSession。
SqlSessionFactory 抽象模版:
Copy
package com.paul.mybatis.factory; import com.paul.mybatis.sqlsession.SqlSession; public interface SqlSessionFactory { SqlSession openSession(); }
创建 SqlSessionFactory 的 Default 实现类,Default 实现类主要完成了两个功能,加载配置信息到 Configuration 对象里,实现创建 SqlSession 的功能。
Copy
package com.paul.mybatis.factory; import com.paul.mybatis.confiuration.Configuration; import com.paul.mybatis.confiuration.MappedStatement; import com.paul.mybatis.sqlsession.DefaultSqlSession; import com.paul.mybatis.sqlsession.SqlSession; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Properties; /** * * 1.初始化时就完成了 configuration 的实例化 * 2.工厂类,生成 sqlSession * */ public class DefaultSqlSessionFactory implements SqlSessionFactory{ private final Configuration configuration = new Configuration(); // xml 文件存放的位置 private static final String MAPPER_CONFIG_LOCATION = "mappers"; // 数据库信息存放的位置 private static final String DB_CONFIG_FILE = "db.properties"; public DefaultSqlSessionFactory() { loadDBInfo(); loadMapperInfo(); } private void loadDBInfo() { InputStream db = this.getClass().getClassLoader().getResourceAsStream(DB_CONFIG_FILE); Properties p = new Properties(); try { p.load(db); } catch (IOException e) { e.printStackTrace(); } //将配置信息写入Configuration 对象 configuration.setJdbcDriver(p.get("jdbc.driver").toString()); configuration.setJdbcUrl(p.get("jdbc.url").toString()); configuration.setJdbcUsername(p.get("jdbc.username").toString()); configuration.setJdbcPassword(p.get("jdbc.password").toString()); } //解析并加载xml文件 private void loadMapperInfo(){ URL resources = null; resources = this.getClass().getClassLoader().getResource(MAPPER_CONFIG_LOCATION); File mappers = new File(resources.getFile()); //读取文件夹下面的文件信息 if(mappers.isDirectory()){ File[] files = mappers.listFiles(); for(File file:files){ loadMapperInfo(file); } } } private void loadMapperInfo(File file){ SAXReader reader = new SAXReader(); //通过read方法读取一个文件转换成Document 对象 Document document = null; try { document = reader.read(file); } catch (DocumentException e) { e.printStackTrace(); } //获取根结点元素对象
在 SqlSessionFactory 里创建了 DefaultSqlSession,我们看看它的具体实现。SqlSession 里面应该封装了所有数据库的具体操作和一些获取 mapper 实现类的方法。
SqlSession 接口,定义模版方法
Copy
package com.paul.mybatis.sqlsession; import java.util.List; /** * * 封装了所有数据库的操作 * 所有功能都是基于 Excutor 来实现的,Executor 封装了 JDBC 操作 * * */ public interface SqlSession { /** * 根据传入的条件查询单一结果 * @param statement namespace+id,可以用做 key,去 configuration 里面获取 sql 语句,resultType * @param parameter 要传入 sql 语句中的查询参数 * @param
Default 的 SqlSession 实现类。里面需要传入 Executor,这个 Executor 里面封装了 JDBC 操作数据库的流程。我们重点关注 getMapper 方法,使用动态代理生成一个加强类。这里面最终还是把数据库的相关操作转给 SqlSession,使用 Mapper 能使编程更加优雅。
Copy
package com.paul.mybatis.sqlsession; import com.paul.mybatis.bind.MapperProxy; import com.paul.mybatis.confiuration.Configuration; import com.paul.mybatis.confiuration.MappedStatement; import com.paul.mybatis.executor.Executor; import com.paul.mybatis.executor.SimpleExecutor; import java.lang.reflect.Proxy; import java.util.List; public class DefaultSqlSession implements SqlSession { private final Configuration configuration; private Executor executor; public DefaultSqlSession(Configuration configuration) { super(); this.configuration = configuration; executor = new SimpleExecutor(configuration); } @Override public
来看看我们的 InvocationHandler 如何实现 invoke 方法:
Copy
package com.paul.mybatis.bind; import com.paul.mybatis.sqlsession.SqlSession; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; /** * * 将请求转发给 sqlSession * */ public class MapperProxy implements InvocationHandler { private SqlSession sqlSession; public MapperProxy(SqlSession sqlSession) { this.sqlSession = sqlSession; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getDeclaringClass().getName()+"."+method.getName()); //最终还是将执行方法转给 sqlSession,因为 sqlSession 里面封装了 Executor //根据调用方法的类名和方法名以及参数,传给 sqlSession 对应的方法 if(Collection.class.isAssignableFrom(method.getReturnType())){ return sqlSession.selectList(method.getDeclaringClass().getName()+"."+method.getName(),args==null?null:args[0]); }else{ return sqlSession.selectOne(method.getDeclaringClass().getName()+"."+method.getName(),args==null?null:args[0]); } } }
获取 Mapper 接口的实现类我们已经实现了,通过动态代理调用 sqlSession 的方法。那么就剩最后一个重要的工作了,那就是实现 Exectuor 类去操作数据库,封装 JDBC。
Executor 抽象模版,我们只实现了 query、update 等操作慢慢增加。
Copy
package com.paul.mybatis.executor; import com.paul.mybatis.confiuration.MappedStatement; import java.util.List; /** * * mybatis 核心接口之一,定义了数据库操作的最基本的方法,JDBC,sqlSession的所有功能都是基于它来实现的 * */ public interface Executor { /** * * 查询接口 * @param ms 封装sql 语句的 mappedStatemnet 对象,里面包含了 sql 语句,resultType 等。 * @param parameter 传入sql 参数 * @param
到目前未知,我们简单版的 MyBatis 框架已经实现了,我们来写一个测试类测试一下。
Copy
package com.paul.mybatis; import com.paul.mybatis.entity.User; import com.paul.mybatis.factory.DefaultSqlSessionFactory; import com.paul.mybatis.factory.SqlSessionFactory; import com.paul.mybatis.mapper.UserMapper; import com.paul.mybatis.sqlsession.SqlSession; public class TestDemo { public static void main(String[] args) { SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = mapper.selectByPrimaryKey(121312312313L); System.out.println(user.toString()); } }
看一下测试的结果,整个 MyBatis 框架已经实现完成了,当然有很多地方需要完善,比如 XML 中的 SQL 语句处处理还缺很多功能,目前只支持 select 等,希望大家能通过源码解读和自己写的过程明白 MyBatis 的具体实现要点。
最后给出源码地址:源码,码字不易,如果您觉得学到了东西,请在 Chat 或 GitHub 点赞,不明白的可以评论或者留言。QQ 群:725758660