在講解MyBatis之前,先說下傳統JDBC連接數據庫的弊端:
1.JDBC底層沒有實現連接池,從而導致操作數據庫需要頻繁的創建和釋放,影響性能;
2.JDBC的代碼散落在Java代碼中,如果需要修改SQL語句,需要重新編譯Java類;
3.使用PreparedStatement設置參數繁,占位符和參數需要一一對應;
4.處理返回的結果集解析也很麻煩。
所以,在實際開發中,基本不會使用原生的JDBC來操作數據庫。
然后,說一下Hibernate和MyBatis的區別,現在互聯網公司基本上都是以MyBatis為主,因MyBatis能夠書寫比較復雜的SQL語句,比較靈活。
MyBatis的核心概念
類名 | 描述 |
---|---|
Configuration | 描述mybatis-config.xml全局配置關系類 |
SqlSessionFactory | Session管理工廠接口 |
SqlSession | SqlSession接口。 SqlSession中提供了操作數據庫的方法,完成一次數據庫的訪問和結果的映射.不是線程安全 |
Executor | 執行器。SqlSession通過執行器操作數據庫,調用StatementHandler房屋數據庫,並緩存查詢結果 |
MappedStatement | 底層封裝對象。對操作數據庫存儲封裝,包括sql語句、輸入輸出參數 |
StatementHandler | 具體操作數據庫相關的handler接口 |
ResultSetHandler | 具體操作數據庫返回結果的handler接口 |
下面我們新建一個項目,
首先創建一個表,並插入數據:
CREATE TABLE STUDENTS
(
stud_id int(11) NOT NULL AUTO_INCREMENT,
name varchar(50) NOT NULL,
email varchar(50) NOT NULL,
dob date DEFAULT NULL,
PRIMARY KEY (stud_id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = UTF8;
insert into students(stud_id, name, email, dob)
values (1, 'Student1', 'student1@gmail.com', '1990-06-25');
insert into students(stud_id, name, email, dob)
values (2, 'Student2', 'student2@gmail.com', '1990-06-25');
新建一個Student類:
package com.mybatis3.domain;
import java.util.Date;
public class Student {
private Integer studId;
private String name;
private String email;
private Date dob;
public Student() {
}
public Student(Integer studId) {
this.studId = studId;
}
public Student(Integer studId, String name, String email, Date dob) {
this.studId = studId;
this.name = name;
this.email = email;
this.dob = dob;
}
@Override
public String toString() {
return "Student [studId=" + studId + ", name=" + name + ", email="
+ email + ", dob=" + dob + "]";
}
// todo getter and setter
}
新建一個mapper類:
import java.util.List;
import com.mybatis3.domain.Student;
public interface StudentMapper {
Student findStudentById(Integer id);
}
新建Service類:
public class StudentService {
private Logger logger = LoggerFactory.getLogger(getClass());
public Student findStudentById(Integer studId) {
logger.debug("Select Student By ID :{}", studId);
SqlSession sqlSession = MyBatisSqlSessionFactory.getSqlSession();
try {
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
return studentMapper.findStudentById(studId);
//return sqlSession.selectOne("com.mybatis3.StudentMapper.findStudentById", studId);
} finally {
sqlSession.close();
}
}
}
新建一個SqlSessionFactory工具類:
package com.mybatis3.util;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;
import org.apache.ibatis.datasource.DataSourceFactory;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class MyBatisSqlSessionFactory {
private static SqlSessionFactory sqlSessionFactory;
private static final Properties PROPERTIES = new Properties();
static {
try {
InputStream is = DataSourceFactory.class.getResourceAsStream("/application.properties");
PROPERTIES.load(is);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSessionFactory getSqlSessionFactory() {
if (sqlSessionFactory == null) {
try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
throw new RuntimeException(e.getCause());
}
}
return sqlSessionFactory;
}
public static SqlSession getSqlSession() {
return getSqlSessionFactory().openSession();
}
public static Connection getConnection() {
String driver = PROPERTIES.getProperty("jdbc.driverClassName");
String url = PROPERTIES.getProperty("jdbc.url");
String username = PROPERTIES.getProperty("jdbc.username");
String password = PROPERTIES.getProperty("jdbc.password");
Connection connection = null;
try {
Class.forName(driver);
connection = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
throw new RuntimeException(e);
}
return connection;
}
}
編寫StudentMapper.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis3.mappers.StudentMapper">
<resultMap type="Student" id="StudentResult">
<id property="studId" column="stud_id"/>
<result property="name" column="name"/>
<result property="email" column="email"/>
<result property="dob" column="dob"/>
</resultMap>
<select id="findStudentById" parameterType="int" resultType="Student">
select stud_id as studId, name, email, dob
from Students
where stud_id = #{studId}
</select>
</mapper>
最后編寫test類:
package com.mybatis3.services;
import java.util.Date;
import java.util.List;
import org.junit.AfterClass;
import static org.junit.Assert.*;
import org.junit.BeforeClass;
import org.junit.Test;
import com.mybatis3.domain.Student;
public class StudentServiceTest {
private static StudentService studentService;
@Test
public void testFindStudentById() {
Student student = studentService.findStudentById(1);
assertNotNull(student);
}
}
整個代碼結構如下:
現在正式調試testFindStudentById方法,調用到 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream)方法,
在這里設置斷點,進入MyBatis的源碼里,
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
這行代碼沒有必要看,接着往下看:
return build(parser.parse());
parse方法點進去:
parseConfiguration(parser.evalNode("/configuration"));
進入到parseConfiguration方法里面:
這些屬性就是對應mybatis-config.xml里面的參數,這個方法就是解析xml里面所有的內容。
點擊進入mapperElement方法,這個方法,就是規定了mapper標簽的加載優先級順序:
如果配置重復,會拋出異常。
回到上面的bulid方法:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
可以看到就是返回我們SqlSessionFactory(是個接口)的實例對象,其參數就是我們的Configuration全局配置類。
上面到此為止告一段落,接下來看下sqlSessionFactory.openSession()方法,點進去:
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
上面的getDefaultExecutorType方法就是返回執行器類型,是個枚舉,默認是SIMPLE:
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
點進去openSessionFromDataSource方法,這個方法就是獲取環境信息,創建一個事務,再獲取一個執行器:
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, autoCommit);
return new DefaultSqlSession(configuration, executor);
} 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();
}
}
點進newExecutor方法:根據條件創建不同的執行器。
執行器的層級關系如下:
此方法里有個非常重要的一段代碼:
if (cacheEnabled) {
executor = new CachingExecutor(executor, autoCommit);
}
CachingExecutor是創建一級緩存的執行器。
到目前為止,已經拿到SqlSession,並對SimpleExecutor執行器進行初始化。
第三步:執行我們編寫的return sqlSession.selectOne("com.mybatis3.mappers.StudentMapper.findStudentById", studId);
方法。
點進去selectOne,selectOne會調用DefaultSqlSession.selectList方法,點進去:
看下statement的參數值,就是StudentMapper.xml里 mapper標簽名稱 + select標簽id的名稱,所以這里的id是要唯一的。
這里出現了一個新的核心類MappedStatement,對應的StudentMapper.xml里的select,update,insert,delete語句。
下面代碼:
List<E> result = executor.<E>query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
進入query方法:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
getBoundSql就是綁定sql語句的參數,具體值如下:
createCacheKey方法創建緩存key,點進去,一直找到createCacheKey方法,包括緩存參數的更新,給sql創建了一個緩存,緩存可以是由id + sql + limit + offset組成,具有相同的key會做緩存。
query方法會先拿到緩存,緩存如果不為空的話,判斷是否要清空緩存,如果使用緩存,且不是臟數據,則使用讀寫鎖進行加鎖,從不同的緩存策略中獲取緩存列表;
緩存為空的話,會查詢數據庫,
進入 List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
方法,
在真正查詢數據庫之前,會再次判斷已經存在一級緩存,如果沒有,執行queryFromDatabase方法,執行里面的doQuery方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
這里又出現了一個核心類StatementHandler,這個接口是設計具體數據庫的操作。 進入 return handler.<E>query(stmt, resultHandler);
方法:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
這里出現的JDBC的PreparedStatement 類,不用解釋了吧,已經調到Java JDBC的這層API了。
進入到handleResultSets方法,執行此方法,就已經返回數據集的結果了。如下圖:
現在總結一下:
至此,基本上就是整個MyBatis查詢數據的一個過程。