在看完老杜的mybatis手写框架之后也对mybatis底层运行逻辑有了更深的了解,在这里我仅做mybatis快速入门以及手撕mybatis框架的详细步骤,其他的关于mybatis框架使用规则及技巧会在后期的文章中写。
MyBatis入门程序
(1)既然要使用mybatis框架,我们就要使用mybatis相关jar包,同时mybatis框架是关联数据库的一个框架,那自然还需要引入mysql相关jar包,这就需要使用到maven来引入这些jar包。<artifactId>分别为mybatis和mysql-connector-j。
(2)一般想要成功使用框架,我们都会取配置相关XML配置文件,就和Spring框架一样。我们从mybatis提供的入门手册可知我们要从XML中配置SqlSessionFactory,这一步需要编写mybatis核心配置文件:mybatis-config.xml。
mybatis中有两个主要的配置文件:
其中一个是mybatis-config.xml,这是核心配置文件,主要配置连接数据库的信息等(一个配置文件)
另一个是XxxxMapper.xml,这个文件是专门用来编写SQL语句的配置文件(一个表对应一个配置文件)
(3)配置mybatis-config.xml和XxxMapper.xml配置文件,我们先配置mybatis-config.xml,又称为mybatis核心配置文件:
<?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="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/t_car"/>
<property name="username" value="root"/>
<property name="password" value="ryy"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
mybatis-config.xml中标签的解释
(1)<environments>和<environment>都是配置数据库环境,<environments>中的default参数意为创建SqlSessionFactory对象时,未指定环境的话,默认就使用哪个环境,这里的值是和<environment>中的id对应的,一个<environment>对应了一个数据库。通俗来说就是默认会匹配哪个数据库进行连接。
(2)<transactionManager>:这里配置的是mybatis的事务管理机制,数据库中都有事务管理,因此在mybatis框架中也必须配置。这里的type属性值包括两个:JDBC和MANAGED。
JDBC:mybatis框架自己管理事务,自己采用原生的JDBC代码去管理事务
conn.setAutoCommit(false); //开启事务
conn.commit(); //手动提交事务
Managed:mybatis不再负责事务的管理了,事务管理交给其他容器来负责,例如Spring。
(3)<dataSource>:dataSource的作用是为程序提供Connection对象的,又称为配置数据源。
我们自己也可以编写数据源组件,只要实现javax.sql.DataSource接口就行了,实现接口当中的所有方法,这样就有了自己的数据源(比如可以写一个属于自己的数据库连接池)【在后面手写框架会使用到】。
dataSource的type属性指定数据源类星星其实就是指定使用哪个数据库连接池,type属性有三个值,必须是三选一:UNPOOLED,POOLED,JNDI。
UNPOOLED:不使用数据库连接池技术,每次请求过来就创建新的Connection对象。
POOLED:使用mybatis自己实现的数据库连接池。
JNDI:即成其他第三方的数据库连接池。(*JNDI是一套规范,大部分的web容器实现了JNDI规范,如tomcat,Jetty等服务器)
(4)<property>:这个标签就是配置连接数据库的具体信息了,具体写法和JDBC的写法一致。
(5)<mapper>:这个标签就是指定XxxMapper.xml文件的路径,而等会会讲到XxxMapper.xml里面配置的就是各种sql语句。核心配置文件通过这个标签来给数据库发送信息或接收信息。
XxxMapper.xml中标签解释
<?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">
<!--insert语句,id是这条sql语句的唯一标识,这个id就代表了这条sql语句-->
<insert id="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,'1003','丰田霸道',30.0,'2000-10-11','燃油车')
</insert>
</mapper>
namespace作用:如果不同的配置文件中出现了id相同的sql,此时可用namespace+id的方式来指定对应的sql语句(即防止id重复,这里命名就是BlogMapper.insertCar)
<insert><update><delete><select>这些标签都是执行CRUD操作的标签,这在mysql数据库中也很常见。在标签中我们写上sql语句,但是一般sql语句不按照我上面那个例子写,一般的写法如下:
<mapper namespace="user">
<!--insert语句,id是这条sql语句的唯一标识,这个id就代表了这条sql语句-->
<insert id="insertUser">
insert into t_user values(#{id},#{name},#{age})
</insert>
<!--select查询语句-->
<select id="selectById" resultType="org.god.ibatis.pojo.User">
select * from t_user where id = #{id}
</select>
</mapper>
在mybatis中,我们使用#{}的形式来代替JDBC的带有?的写法。
MyBatis实现CRUD:
我们在这里需要知道一个思想叫ORM(对象关系映射思想):
O指Object,JVM中的java对象;
R指Relational,关系型数据库;
M指Mapping,映射。
由上图可知我们需要一个POJO类,数据库表中的字段应该和POJO类属性一一对应,然后用setter, getter等方法对数据进行封装。
以上面我提到的user表的例子来看,我们可以清晰得看到我们创建得pojo类中需要三个属性分别是id,name,age。
注:#{}里要写的是pojo类中创建得属性名:#{id},#{name},#{age}。后期手撕框架的时候我们会知道实际底层找的是属性对应的getter方法并把get去掉后把剩余单词首字母变小写,也就是说mybatis在底层给?赋值的时候先调用pojo类的getter方法来获取值。
然后我们可以在测试类中创建pojo对象并给对象赋值发送给数据库:(执行sql语句)
User user = new User("1112", "zhangsan", "20"); int count = sqlSession.insert("user.insertUser", user);
编写MyBatis程序:
(5)编写mybatis程序:
在mybatis中,负责执行sql语句的那个对象叫做SqlSession(Java程序和数据库之间的一次会话)。
想要获取SqlSession对象,需要先获取SqlSessionFactory对象,通过该工厂类来生产SqlSession对象。
通过SqlSessionFactoryBuilder的build方法,来获取一个SqlSessionFactory对象。
以这三步曲去编写主程序代码:
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisIntroduction {
public static void main(String[] args) throws IOException {
//获取SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
//读取配置文件作为输入流
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
//获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//执行sql语句
int count = sqlSession.insert("insertCar");
System.out.println(count);
//手动提交事务
sqlSession.commit();
}
}
手写MyBatis框架
在编写完mybatis程序后,我们也应该要了解一下其底层运行逻辑是什么,接下来我将通过总共13步来带着大家一起手写mybatis框架,在这之前我们需要有的预备知识是如何通过dom4j解析XML文件。
dom4j解析XML文件
(1)创建SAXReader对象
SAXReader reader = new SAXReader();
(2)获取输入流
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml") ;
(3)读XML文件,返回document对象,代表了整个XML文件
Document document = reader.read(is);
(4)获取default默认的环境id
//xpath是做标签路径匹配的,能够让我们快速定位XML文件中的元素
//如下的xpath代表了:从根路径下开始找configuration标签,然后找configuration标签下的子标签environments
String xpath = "/configuration/environments";
Element environments = (Element)document.selectSingleNode(xpath);//Element为Node的子类,方法更多
(5)获取属性的值
String defaultEnvironmentId = environments.attributeValue("default");
(6)获取具体的环境environment
xpath = "/configuration/environments/environment[@id = ' " +defaultEnvironmentId+" ']";
Element environment = (Element)document.selectSingleNode(xpath);
(7)获取environment节点下的子节点transactionManager节点(获取所有子节点的方式是elements)
Element transactionManager = environment.element("transactionManager");
手写框架
(1)根据之间写的第一个mybatis入门程序可知:
//读取配置文件作为输入流
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
我们在此处需要一个Resources工具类来帮助我们获取配置文件信息的输入流
import java.io.InputStream;
/**
* godbatis框架提供的一个工具类
* 这个工具类专门完成“类路径”中资源的加载
* @author ryy
* @since 1.0
* @version 1.0
*/
public class Resources {
/**
* 工具类当中的构造方法都建议私有化
* 因为工具类中的方法都是静态的,不需要创建对象就能够调用
* 为了避免new对象,所有构造方法私有化
*/
private Resources(){}
/**
* 从类路径当中加载资源
* @param resource 放在类路径当中的资源文件
* @return 指向资源文件的一个输入流
*/
public static InputStream getResourceAsStream(String resource){
return ClassLoader.getSystemClassLoader().getResourceAsStream(resource);
}
}
(2)创建一个SqlSessionFactoryBuilder类然后实现其中的build方法来读取输入流,build方法的返回类型是SqlSessionFactory,因此还需要创建一个SqlSessionFactory类
import java.io.InputStream;
/**
* SqlSessionFactory对象构建器
*/
public class SqlSessionFactoryBuilder {
/**
* 创建构建器对象
*/
public SqlSessionFactoryBuilder() {
}
/**
* 获取SqlSessionFactory对象
* 该⽅法主要功能是:读取godbatis核⼼配置⽂件,并构建SqlSessionFactory对象
* @param inputStream 指向核⼼配置⽂件的输⼊流
* @return SqlSessionFactory对象
*/
public SqlSessionFactory build(InputStream inputStream){
// 解析配置⽂件,创建数据源对象
// 解析配置⽂件,创建事务管理器对象
// 解析配置⽂件,获取所有的SQL映射对象
// 将以上信息封装到SqlSessionFactory对象中
// 返回
return null;
}
}
(3)SqlSessionFactory类中应该有哪些属性?根据先前写的配置文件来看,配置文件中有事务管理器<transactionManager>属性,有数据源<dataSource>属性,还有<Mapper>属性,<Mapper>标签中封装了其他的存放sql语句的xml配置文件,因此在这里我们需要一个map集合用于存放sql语句的id和sql语句标签下的其他属性:
但是我们注意到sql语句标签下的其他属性很丰富,无法直接封装到value当中去,这时我们需要一个新的类对象来封装这些属性,然后将这个新创建的类对象放到value当中去:
(4)然后就是另一个属性:事务管理器。事务管理器属性有两个可选项:JDBC和Managed两种,在这两种模式中灵活切换我们就需要定义接口来实现切换。定义Transaction接口,然后创建JDBCTransaction和ManagedTransaction实现类实现该接口。而事务相关的代码我们是需要先获取到数据库连接对象connection的,因此在该类中我们必须要有一个数据源属性,此时我们会发现在JDBCTransaction类和SqlSessionFactory类中都有数据源属性,分析通过SqlSessionFactory类中的Transaction接口创建事务管理器能够获取数据源因而SqlSessionFactory类中的数据源属性就是多余的了。
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* JDBC事务管理器实现类
*/
public class JDBCTransaction implements Transaction{
/**
* 数据源属性
* 也是面向接口编程,通过传入的类对象返回对应的属性
*/
private DataSource dataSource;
/**
* 自动提交标志,true表示自动提交,false表示不采用自动提交
*/
private boolean autoCommit;
/**
* 我们在这边还需要对外提供一个connection的getter方法以便后续在执行sql语句的时候能获取到该数据源连接对象
* @return
*/
@Override
public Connection getConnection() {
return connection;
}
/**
* 创建事务管理器对象给属性赋值使之不为空
* @param dataSource
* @param autoCommit
*/
public JDBCTransaction(DataSource dataSource, boolean autoCommit) {
this.dataSource = dataSource;
this.autoCommit = autoCommit;
}
@Override
public void commit() {
try {
connection.commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void rollback() {
try {
connection.rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void close() {
try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
(5)由于需要获取数据源属性才能继续编写事务管理器属性,我们需要先编写数据源相关代码。由于所有的数据源都要实现JDK带的规范:javax.sql.DataSource,因此这里无需像事务管理器那样再去创建一个接口了,创建三个实现类:UnPooledDataSource,PooledDataSource,JNDIDataSource。
然后正常做JDBC注册驱动和获取连接的操作,只需要在getConnection()方法中写获取连接对象的代码即可。
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* 不使用连接池,每一次都新建connection对象(重点实现)
*/
public class UnPooledDataSource implements javax.sql.DataSource{
private String url;
private String username;
private String password;
/**
* 创建一个数据源对象
* @param driver
* @param url
* @param username
* @param password
*/
public UnPooledDataSource(String driver, String url, String username, String password) {
try {
//直接注册驱动
Class.forName(driver);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
this.url = url;
this.username = username;
this.password = password;
}
@Override
public Connection getConnection() throws SQLException {
Connection connection = DriverManager.getConnection(url, username, password);
return connection;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
(6)回到事务管理器这时我们会发现一个问题:在同一个事务里连接对象必须是同一个,因此我们不能把dataSource.getConnection()在每一个事务方法里都写一个,这相当于连接对象不是同一个了,解决方法是通过创建一个方法来判断连接是否为空来判断是否要创建新的连接对象,只有连接对象为空才允许创建新的连接对象,不然就继续使用老的连接对象,创建一个方法为openConnection(),并且创建一个属性Connection连接对象,代码添加如下:在(4)的步骤上做新增
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* JDBC事务管理器实现类
*/
public class JDBCTransaction implements Transaction{
/**
* 数据源属性
* 也是面向接口编程,通过传入的类对象返回对应的属性
*/
private DataSource dataSource;
/**
* 自动提交标志,true表示自动提交,false表示不采用自动提交
*/
private boolean autoCommit;
/**
* 连接对象
*/
private Connection connection;
/**
* 我们在这边还需要对外提供一个connection的getter方法以便后续在执行sql语句的时候能获取到该数据源连接对象
* @return
*/
@Override
public Connection getConnection() {
return connection;
}
/**
* 创建事务管理器对象给属性赋值使之不为空
* @param dataSource
* @param autoCommit
*/
public JDBCTransaction(DataSource dataSource, boolean autoCommit) {
this.dataSource = dataSource;
this.autoCommit = autoCommit;
}
@Override
public void commit() {
try {
connection.commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void rollback() {
try {
connection.rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void close() {
try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void openConnection(){
if(connection == null){
try {
connection = dataSource.getConnection();
//开启事务
connection.setAutoCommit(autoCommit);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
(7)然后就是通过dom4j来读取配置文件信息,将读取到的事务管理器和mappedStatements构建给SqlSessionFactory,为了避免臃肿,我们创建两个方法来获取数据源对象和事务管理器对象,先不获取mappedStatements,暂时将其设置为null。这时我们就要使用到我们刚才创建的那些类了,UnPooledDataSource用于通过JDBC来获取数据库连接的操作,JDBCTransaction用来进行获取事务管理器对象的操作。
在SqlSessionFactoryBuilder中:
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.god.ibatis.utils.Resources;
import javax.sql.DataSource;
import java.io.InputStream;
import java.util.*;
/**
* SqlSessionFactory构建器对象
* 通过SqlSessionFactoryBuilder的build方法来解析godbatis-config.xml配置文件,
* 然后创建SqlSessionFactory对象
*/
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 {
//解析godbatis-config.xml配置文件
SAXReader reader = new SAXReader();
Document document = reader.read(in);
Element environments = (Element) document.selectSingleNode("/configuration/environments");
String defaultId = environments.attributeValue("default");
Element environment = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultId + "']");
Element transactionElt = environment.element("transactionManager");
Element dataSourceElt = environment.element("dataSource");
//在此处我们创建一个集合用来接收<mapper>当中的路径名,即resource属性
List<String> sqlMapperXMLPathList = new ArrayList<>();
List<Node> nodes = document.selectNodes("//mapper"); //获取整个配置文件中所有的mapper标签
nodes.forEach(node -> {
Element mapper = (Element) node;
String resource = mapper.attributeValue("resource");
sqlMapperXMLPathList.add(resource);
});
//获取数据源对象,因为后面在获取事务管理器的时候需要数据源对象
DataSource dataSource = getDataSource(dataSourceElt);
//获取事务管理器
Transaction transaction = getTransaction(transactionElt, dataSource);
//获取mappedStatements
Map<String, MappedStatement> mappedStatements = null;
//解析完成之后,构建SqlSessionFactory对象
factory = new SqlSessionFactory(transaction, mappedStatements);
} catch (Exception e) {
e.printStackTrace();
}
return factory;
}
/**
* 获取事务管理器的
*
* @param transactionElt 事务管理器标签元素
* @param dataSource 数据源对象
* @return
*/
private Transaction getTransaction(Element transactionElt, DataSource dataSource) {
//定义一个接口类型用于灵活接收参数
Transaction transaction = null;
String type = transactionElt.attributeValue("type").trim().toUpperCase();
if (Const.JDBC_TRANSACTION.equals(type)) {
transaction = new JDBCTransaction(dataSource, false); //第二个参数设置为false默认开启事务的,未来需要手动提交
}
if (Const.MANAGED_TRANSACTION.equals(type)) {
transaction = new ManagedTransaction();
}
return transaction;
}
/**
* 获取数据源对象
*
* @param dataSourceElt 数据源标签元素
* @return
*/
private DataSource getDataSource(Element dataSourceElt) {
Map<String, String> map = new HashMap<>();
//获取所有的property
List<Element> propertyElts = dataSourceElt.elements("property");
propertyElts.forEach(propertyElt -> {
String name = propertyElt.attributeValue("name");
String value = propertyElt.attributeValue("value");
map.put(name, value);
});
//定义一个接口类型用于灵活接收参数
DataSource dataSource = null;
String type = dataSourceElt.attributeValue("type").trim().toUpperCase(); //转换成大写
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;
}
}
(8)然后处理mappedStatements集合当中的元素,先获取到mapper.xml文件的路径,然后根据路径获取其中的各个标签以及标签的子标签,然后就是把id作为key,其他的子标签封装进入MappedStatement作为value放入到最开始创建的大的mappedStatements集合当中。
创建MappedStatement类:里面的属性有sql语句和resultType(即pojo类,用于封装select语句查询到的值)
/**
* 普通的java类,POJO,封装了一个SQL标签
* 一个MappedStatement对象对应一个SQL标签
* 一个SQL标签中的所有信息封装到MappedStatement对象当中
* 面向对象编程思想。
*/
public class MappedStatement {
/**
* sql语句
*/
private String sql;
/**
* 要封装的结果集类型,有的时候resultType是null
* 比如:insert,delete,update语句的时候resultType是null
*/
private String resultType;
public MappedStatement(String sql, String resultType) {
this.sql = sql;
this.resultType = resultType;
}
public MappedStatement() {
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
@Override
public String toString() {
return "MappedStatement{" +
"sql='" + sql + '\'' +
", resultType='" + resultType + '\'' +
'}';
}
}
在(7)的代码基础上新增getMappedStatements()方法:
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.god.ibatis.utils.Resources;
import javax.sql.DataSource;
import java.io.InputStream;
import java.util.*;
/**
* SqlSessionFactory构建器对象
* 通过SqlSessionFactoryBuilder的build方法来解析godbatis-config.xml配置文件,
* 然后创建SqlSessionFactory对象
*/
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 {
//解析godbatis-config.xml配置文件
SAXReader reader = new SAXReader();
Document document = reader.read(in);
Element environments = (Element) document.selectSingleNode("/configuration/environments");
String defaultId = environments.attributeValue("default");
Element environment = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultId + "']");
Element transactionElt = environment.element("transactionManager");
Element dataSourceElt = environment.element("dataSource");
//在此处我们创建一个集合用来接收<mapper>当中的路径名,即resource属性
List<String> sqlMapperXMLPathList = new ArrayList<>();
List<Node> nodes = document.selectNodes("//mapper"); //获取整个配置文件中所有的mapper标签
nodes.forEach(node -> {
Element mapper = (Element) node;
String resource = mapper.attributeValue("resource");
sqlMapperXMLPathList.add(resource);
});
//获取数据源对象,因为后面在获取事务管理器的时候需要数据源对象
DataSource dataSource = getDataSource(dataSourceElt);
//获取事务管理器
Transaction transaction = getTransaction(transactionElt, dataSource);
//获取mappedStatements
Map<String, MappedStatement> mappedStatements = getMappedStatements(sqlMapperXMLPathList);
//解析完成之后,构建SqlSessionFactory对象
factory = new SqlSessionFactory(transaction, mappedStatements);
} catch (Exception e) {
e.printStackTrace();
}
return factory;
}
/**
* 解析所有的SqlMapper.xml文件,然后构建map集合
*
* @param sqlMapperXMLPathList
* @return
*/
private Map<String, MappedStatement> getMappedStatements(List<String> sqlMapperXMLPathList) {
Map<String, MappedStatement> mappedStatements = new HashMap<>();
sqlMapperXMLPathList.forEach(sqlMapperXMLPath -> {
try {
SAXReader reader = new SAXReader();
Document document = reader.read(Resources.getResourceAsStream(sqlMapperXMLPath));
//获取到sqlMapper.xml文件之后,我们需要获取其中的mapper标签以及其下所有的子节点
Element mapper = (Element) document.selectSingleNode("mapper");
String namespace = mapper.attributeValue("namespace");
List<Element> elements = mapper.elements();
elements.forEach(element -> {
String id = element.attributeValue("id");
//这里进行了namespace和id的拼接,生成最终的sqlId
String sqlId = namespace + "." + id;
//然后获取标签中的resultType,没有就返回空
String resultType = element.attributeValue("resultType");
//以下方法能成功获取sql语句文本并清除文本当中的空白
String sql = element.getTextTrim();
//下一步就是封装MappedStatement对象
MappedStatement mappedStatement = new MappedStatement(sql, resultType);
mappedStatements.put(sqlId, mappedStatement);
});
} catch (Exception e) {
e.printStackTrace();
}
});
return mappedStatements;
}
/**
* 获取事务管理器的
*
* @param transactionElt 事务管理器标签元素
* @param dataSource 数据源对象
* @return
*/
private Transaction getTransaction(Element transactionElt, DataSource dataSource) {
//定义一个接口类型用于灵活接收参数
Transaction transaction = null;
String type = transactionElt.attributeValue("type").trim().toUpperCase();
if (Const.JDBC_TRANSACTION.equals(type)) {
transaction = new JDBCTransaction(dataSource, false); //第二个参数设置为false默认开启事务的,未来需要手动提交
}
if (Const.MANAGED_TRANSACTION.equals(type)) {
transaction = new ManagedTransaction();
}
return transaction;
}
/**
* 获取数据源对象
*
* @param dataSourceElt 数据源标签元素
* @return
*/
private DataSource getDataSource(Element dataSourceElt) {
Map<String, String> map = new HashMap<>();
//获取所有的property
List<Element> propertyElts = dataSourceElt.elements("property");
propertyElts.forEach(propertyElt -> {
String name = propertyElt.attributeValue("name");
String value = propertyElt.attributeValue("value");
map.put(name, value);
});
//定义一个接口类型用于灵活接收参数
DataSource dataSource = null;
String type = dataSourceElt.attributeValue("type").trim().toUpperCase(); //转换成大写
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;
}
}
(9)先进行测试看看刚刚的这些代码有无问题,几个注意事项就是需要检查是否安装了mysql驱动,mybatis核心配置文件中的type属性是否设置为了UNPOOLED,只有UNPOOLED才能成功注册数据库驱动。
(10)再接下去一步就是sqlSessionFactory中有一个openSession方法能够返回SqlSession对象(专门执行sql语句的会话对象),在SqlSessionFactory类下编译,在这里有一个比较难想到的点:SqlSession未来是需要执行sql语句的,因此必须要有的属性是和数据库的连接以及mappedStatements来获取到其中的sql语句才能执行,那么这两个参数又正好是SqlSessionFactory类下已有的两个属性,因此作为参数传给SqlSession构建对象的时候可以直接使用this来传参。
SqlSessionFactory:
import java.util.Map;
/**
* SqlSessionFactory对象:
* 一个数据库对应一个SqlSessionFactory对象
* 通过SqlSessionFactory对象可以获取SqlSession对象(开启会话)
* 一个SqlSessionFactory对象可以开启多个SqlSession会话
*/
public class SqlSessionFactory {
/**
* 事务管理器属性
* 事务管理器是可以灵活切换的,SqlSessionFactory类中的事务管理器应该是面向接口编程的
*/
private Transaction transaction;
/**
* 存放sql语句的Map集合
* key是sqlId
* value是对应sql标签信息对象
*/
private Map<String, MappedStatement> mappedStatements;
public SqlSessionFactory() {
}
public SqlSessionFactory(Transaction transaction, Map<String, MappedStatement> mappedStatements) {
this.transaction = transaction;
this.mappedStatements = mappedStatements;
}
public Transaction getTransaction() {
return transaction;
}
public void setTransaction(Transaction transaction) {
this.transaction = transaction;
}
public Map<String, MappedStatement> getMappedStatements() {
return mappedStatements;
}
/**
* 获取Sql会话对象
*/
public SqlSession openSession(){
//开启会话的前提是开启连接(打开事务连接)
transaction.openConnection();
//创建SqlSession对象
SqlSession sqlSession = new SqlSession(this);
return sqlSession;
}
public void setMappedStatements(Map<String, MappedStatement> mappedStatements) {
this.mappedStatements = mappedStatements;
}
}
(11)下面一步就是来到SqlSession中写各种CRUD的方法以实现执行sql语句的操作,这里只做selectOne和insert语句的实现。我们在先前所写的mybatis测试程序中可以看到sqlSession.commit(),sqlSession.close()等操作,意味着在SqlSession类中也要有关于事务提交,回滚,关闭的方法。
import java.sql.SQLException;
import java.util.Map;
/**
* 数据库会话对象
*/
public class SqlSession {
private TransactionManager transactionManager;
private Map<String, GodMappedStatement> mappedStatements;
public SqlSession(TransactionManager transactionManager, Map<String, G
odMappedStatement> mappedStatements) {
this.transactionManager = transactionManager;
this.mappedStatements = mappedStatements;
}
public void commit(){
try {
transactionManager.getConnection().commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void rollback(){
try {
transactionManager.getConnection().rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void close(){
try {
transactionManager.getConnection().close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
(12)下一步给?占位符传值,这个过程中会遇到两个难点:1,我不知道有多少个?2,我不知道该将pojo对象中的哪个属性赋值给哪个?
这里不死扣数据类型问题了,这里统一用String类型,即对应到数据库表当中的varchar类型
ps.String(第几个问号,传什么值);//这里都是String类型,所以数据库表中的字段类型要求都是varchar才行。这是godbatis比较失败的地方
我们可以根据sql语句从左到右分析将#{}的位置得到然后去掉#{}得到其内部的属性名(如:id,name等)并存放到propertyName中,而index属性是用来记录?数量的,每次检测到一个#{}就记录一个?数量,这样就能清晰的知道要给第几个问号赋值了。然后我们通过反射机制能够通过propertyName中存储的这些属性名获取到他们对应的getter方法,然后调用这些getter方法就能够将要传入数据库表格中的数据封装起来到propertyValue属性中。
insert方法的具体实现:
/**
* 执行insert语句,向数据库表中插入记录
*
* @param sqlId sql语句的id
* @param Pojo 插入的数据
* @return
*/
public int insert(String sqlId, Object Pojo) {
int count = 0;
try {
//JDBC代码,执行insert语句,完成插入操作
Connection connection = factory.getTransaction().getConnection();
//获取带有#{}的sql语句
String godBatisSql = factory.getMappedStatements().get(sqlId).getSql();
//insert into t_user values(?,?,?),正则表达式将#{}替换成?
String sql = godBatisSql.replaceAll("#\\{[a-zA-Z0-o_$]*}", "?");
PreparedStatement ps = connection.prepareStatement(sql);
//给?占位符赋值
//ps.String(第几个问号,传什么值);//这里都是String类型,所以数据库表中的字段类型要求都是varchar才行。这是godbatis比较失败的地方
int fromIndex = 0;
int index = 1;
while (true) {
int jingIndex = godBatisSql.indexOf("#", fromIndex);
if (jingIndex < 0) {
break;
}
int youKuoHaoIndex = godBatisSql.indexOf("}", fromIndex);
String propertyName = godBatisSql.substring(jingIndex + 2, youKuoHaoIndex).trim();
fromIndex = youKuoHaoIndex + 1;
//有属性名id,怎么获取id的属性值呢?调用get方法,以下获取方法名的方法在手写String框架中使用过
String getMethodName = "get" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
//获取到get方法的方法名之后用反射获取get方法
Method getMethod = Pojo.getClass().getDeclaredMethod(getMethodName);
Object propertyValue = getMethod.invoke(Pojo);
ps.setString(index, propertyValue.toString()); //给第几个?传什么值
index++;
}
//执行sql语句
count = ps.executeUpdate();
} catch (Exception e) {
throw new RuntimeException(e);
}
return count;
}
(13)写完了insert方法,下一步就是写selectOne方法,从mappedStatements当中获取表中数据并返回。需要得到sqlMapper配置文件中的resultType属性,即需要得到那个POJO类,将查询到的结果集封装进入pojo类中的各个属性:解决问题的关键在于:将查询结果的列名作为属性名。如列名是id,那么属性名就是id;列名是name,属性名就是name。
在这里需要用到两个api:getMetaData()//查询结果集的原数据,getColumnCount()//查询结果集中列的数量,然后做循环遍历通过getColumnName(i)拿到列名(即为属性名)。然后通过反射机制获取到对应类(这里就是user类)的setter方法,调用set方法赋值,传入的值就是之前结果集的列名。
selectOne代码的具体实现:
/**
* 执行查询语句,返回一个对象,该方法只适合返回一条记录的sql语句
*
* @param sqlId
* @param param
* @return
*/
public Object selectOne(String sqlId, Object param) {
Object obj = null;
try {
Connection connection = factory.getTransaction().getConnection();
MappedStatement mappedStatement = factory.getMappedStatements().get(sqlId);
//这是那个DQL查询语句
//select * from t_user where id = #{id}
String godbatisSql = mappedStatement.getSql();
String sql = godbatisSql.replaceAll("#\\{[a-zA-Z0-9_$]*}", "?");
PreparedStatement ps = connection.prepareStatement(sql);
//给占位符传值(这里只有一个问号)
ps.setString(1, param.toString());
//查询返回结果集
ResultSet rs = ps.executeQuery();
//要封装的结果类型
String resultType = mappedStatement.getResultType();
//从结果集中取数据,封装java对象
if (rs.next()) {
//获取resultType的Class类对象
Class<?> resultTypeClass = Class.forName(resultType);
//调用无参构造方法创建对象
obj = resultTypeClass.getDeclaredConstructor().newInstance();
//给User类的id,name,age属性赋值
//查询结果集的原数据
ResultSetMetaData rsmd = rs.getMetaData();
//查询结果集中列的数量
int columnCount = rsmd.getColumnCount();
for (int i = 0; i < columnCount; i++) {
String propertyName = rsmd.getColumnName(i + 1);
//拼接方法名获取setter方法
String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
//获取set方法
Method setMethod = resultTypeClass.getDeclaredMethod(setMethodName, String.class);
//调用set方法给对象obj赋值,传入的值就是得到的结果集的列名
setMethod.invoke(obj, rs.getString(propertyName));
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return obj;
}
其他:我们在上面的代码中看到了一些全用大写字母写的属性,这些是定义的常量,代码如下:
/**
* 整个godbatis框架的常量类
*/
public class Const {
public static final String UN_POOLED_DATASOURCE = "UNPOOLED";
public static final String POOLED_DATASOURCE = "POOLED";
public static final String JNDI_DATASOURCE = "JNDI";
public static final String JDBC_TRANSACTION = "JDBC";
public static final String MANAGED_TRANSACTION = "MANAGED";
}
最后给大家展示一下手写的mybatis框架的最终结构: