MyBatis框架及原理分析
MyBatis 是支持定制化 SQL、存儲過程以及高級映射的優秀的持久層框架,其主要就完成2件事情:
- 封裝JDBC操作
- 利用反射打通Java類與SQL語句之間的相互轉換
MyBatis的主要設計目的就是讓我們對執行SQL語句時對輸入輸出的數據管理更加方便,所以方便地寫出SQL和方便地獲取SQL的執行結果才是MyBatis的核心競爭力。
MyBatis的配置
MyBatis框架和其他絕大部分框架一樣,需要一個配置文件,其配置文件大致如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="false"/>
<!--<setting name="logImpl" value="STDOUT_LOGGING"/> <!– 打印日志信息 –>-->
</settings>
<typeAliases>
<typeAlias type="com.luo.dao.UserDao" alias="User"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/> <!--事務管理類型-->
<dataSource type="POOLED">
<property name="username" value="luoxn28"/>
<property name="password" value="123456"/>
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.1.150/ssh_study"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="userMapper.xml"/>
</mappers>
</configuration>
以上配置中,最重要的是數據庫參數的配置,比如用戶名密碼等,如果配置了數據表對應的mapper文件,則需要將其加入到<mappers>節點下。
MyBatis的主要成員
- Configuration MyBatis所有的配置信息都保存在Configuration對象之中,配置文件中的大部分配置都會存儲到該類中
- SqlSession 作為MyBatis工作的主要頂層API,表示和數據庫交互時的會話,完成必要數據庫增刪改查功能
- Executor MyBatis執行器,是MyBatis 調度的核心,負責SQL語句的生成和查詢緩存的維護
- StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設置參數等
- ParameterHandler 負責對用戶傳遞的參數轉換成JDBC Statement 所對應的數據類型
- ResultSetHandler 負責將JDBC返回的ResultSet結果集對象轉換成List類型的集合
- TypeHandler 負責java數據類型和jdbc數據類型(也可以說是數據表列類型)之間的映射和轉換
- MappedStatement MappedStatement維護一條<select|update|delete|insert>節點的封裝
- SqlSource 負責根據用戶傳遞的parameterObject,動態地生成SQL語句,將信息封裝到BoundSql對象中,并返回
- BoundSql 表示動態生成的SQL語句以及相應的參數信息
以上主要成員在一次數據庫操作中基本都會涉及,在SQL操作中重點需要關注的是SQL參數什么時候被設置和結果集怎么轉換為JavaBean對象的,這兩個過程正好對應StatementHandler和ResultSetHandler類中的處理邏輯。
MyBatis的初始化
MyBatis的初始化的過程其實就是解析配置文件和初始化Configuration的過程,MyBatis的初始化過程可用以下幾行代碼來表述:
String resource = "mybatis.xml";
// 加載mybatis的配置文件(它也加載關聯的映射文件)
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
// 構建sqlSession的工廠
sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
首先會創建SqlSessionFactory建造者對象,然后由它進行創建SqlSessionFactory。這里用到的是建造者模式,建造者模式最簡單的理解就是不手動new對象,而是由其他類來進行對象的創建。
// SqlSessionFactoryBuilder類
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse()); // 開始進行解析了 :)
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
XMLConfigBuilder對象會進行XML配置文件的解析,實際為configuration節點的解析操作。
// XMLConfigBuilder類
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
/* 處理environments節點數據 */
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
在configuration節點下會依次解析properties/settings/.../mappers等節點配置。在解析environments節點時,會根據transactionManager的配置來創建事務管理器,根據dataSource的配置來創建DataSource對象,這里面包含了數據庫登錄的相關信息。在解析mappers節點時,會讀取該節點下所有的mapper文件,然后進行解析,并將解析后的結果存到configuration對象中。
// XMLConfigBuilder類
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
/* 創建事務管理器 */
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
/* 建造者模式 設計模式 */
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
// 解析單獨的mapper文件
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse(); // 開始解析mapper文件了 :)
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
解析完MyBatis配置文件后,configuration就初始化完成了,然后根據configuration對象來創建SqlSession,到這里時,MyBatis的初始化的征程已經走完了。
// SqlSessionFactoryBuilder類
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
MyBatis的SQL查詢流程
SQL語句的執行才是MyBatis的重要職責,該過程就是通過封裝JDBC進行操作,然后使用Java反射技術完成JavaBean對象到數據庫參數之間的相互轉換,這種映射關系就是有TypeHandler對象來完成的,在獲取數據表對應的元數據時,會保存該表所有列的數據庫類型,大致邏輯如下所示:
/* Get resultSet metadata */
ResultSetMetaData metaData = resultSet.getMetaData();
int column = metaData.getColumnCount();
for (int i = 1; i <= column; i++) {
JdbcType jdbcType = JdbcType.forCode(metaData.getColumnType(i));
typeHandlers.add(TypeHandlerRegistry.getTypeHandler(jdbcType));
columnNames.add(metaData.getColumnName(i));
}
使用如下代碼進行SQL查詢操作:
sqlSession = sessionFactory.openSession();
User user = sqlSession.selectOne("com.luo.dao.UserDao.getUserById", 1);
System.out.println(user);
創建sqlSession的過程其實就是根據configuration中的配置來創建對應的類,然后返回創建的sqlSession對象。調用selectOne方法進行SQL查詢,selectOne方法最后調用的是selectList,在selectList中,會查詢configuration中存儲的MappedStatement對象,mapper文件中一個sql語句的配置對應一個MappedStatement對象,然后調用執行器進行查詢操作。
// DefaultSqlSession類
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
執行器在query操作中,優先會查詢緩存是否命中,命中則直接返回,否則從數據庫中查詢。
// CachingExecutor類
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
/* 獲取關聯參數的sql,boundSql */
BoundSql boundSql = ms.getBoundSql(parameterObject);
/* 創建cache key值 */
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
/* 獲取二級緩存實例 */
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
/**
* 先往localCache中插入一個占位對象,這個地方
*/
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
/* 往緩存中寫入數據,也就是緩存查詢結果 */
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
真正的doQuery操作是由SimplyExecutor代理來完成的,該方法中有2個子流程,一個是SQL參數的設置,另一個是SQL查詢操作和結果集的封裝。
// SimpleExecutor類
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(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
/* 子流程1: SQL查詢參數的設置 */
stmt = prepareStatement(handler, ms.getStatementLog());
/* 子流程2: SQL查詢操作和結果集封裝 */
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
子流程1 SQL查詢參數的設置:
首先獲取數據庫connection連接,然后準備statement,然后就設置SQL查詢中的參數值。打開一個connection連接,在使用完后不會close,而是存儲下來,當下次需要打開連接時就直接返回。
// SimpleExecutor類
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
/* 獲取Connection連接 */
Connection connection = getConnection(statementLog);
/* 準備Statement */
stmt = handler.prepare(connection, transaction.getTimeout());
/* 設置SQL查詢中的參數值 */
handler.parameterize(stmt);
return stmt;
}
// DefaultParameterHandler類
public void setParameters(PreparedStatement ps) {
/**
* 設置SQL參數值,從ParameterMapping中讀取參數值和類型,然后設置到SQL語句中
*/
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
子流程2 SQL查詢結果集的封裝:
// SimpleExecutor類
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 執行查詢操作
ps.execute();
// 執行結果集封裝
return resultSetHandler.<E> handleResultSets(ps);
}
// DefaultReseltSetHandler類
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
/**
* 獲取第一個ResultSet,同時獲取數據庫的MetaData數據,包括數據表列名、列的類型、類序號等。
* 這些信息都存儲在了ResultSetWrapper中了
*/
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
ResultSetWrapper是ResultSet的包裝類,調用getFirstResultSet方法獲取第一個ResultSet,同時獲取數據庫的MetaData數據,包括數據表列名、列的類型、類序號等,這些信息都存儲在ResultSetWrapper類中了。然后調用handleResultSet方法來來進行結果集的封裝
// DefaultResultSetHandler類
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
這里調用handleRowValues方法進行結果值的設置。
// DefaultResultSetHandler類
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
// 封裝數據
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
skipRows(rsw.getResultSet(), rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
Object rowValue = getRowValue(rsw, discriminatedResultMap);
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// createResultObject為新創建的對象,數據表對應的類
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
// 這里把數據填充進去,metaObject中包含了resultObject信息
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = (foundValues || configuration.isReturnInstanceForEmptyRow()) ? rowValue : null;
}
return rowValue;
}
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
boolean foundValues = false;
if (autoMapping.size() > 0) {
// 這里進行for循環調用,因為user表中總共有7列,所以也就調用7次
for (UnMappedColumnAutoMapping mapping : autoMapping) {
// 這里將esultSet中查詢結果轉換為對應的實際類型
final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
// gcode issue #377, call setter on nulls (value is not 'found')
metaObject.setValue(mapping.property, value);
}
}
}
return foundValues;
}
mapping.typeHandler.getResult會獲取查詢結果值的實際類型,比如我們user表中id字段為int類型,那么它就對應Java中的Integer類型,然后通過調用statement.getInt("id")來獲取其int值,其類型為Integer。metaObject.setValue方法會把獲取到的Integer值設置到Java類中的對應字段。
// MetaObject類
public void setValue(String name, Object value) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
if (value == null && prop.getChildren() != null) {
// don't instantiate child path if value is null
return;
} else {
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
metaValue.setValue(prop.getChildren(), value);
} else {
objectWrapper.set(prop, value);
}
}
metaValue.setValue方法最后會調用到Java類中對應數據域的set方法,這樣也就完成了SQL查詢結果集的Java類封裝過程。最后貼一張調用棧到達Java類的set方法中的快照:
MyBatis緩存
MyBatis提供查詢緩存,用于減輕數據庫壓力,提高性能。MyBatis提供了一級緩存和二級緩存。
一級緩存是SqlSession級別的緩存,每個SqlSession對象都有一個哈希表用于緩存數據,不同SqlSession對象之間緩存不共享。同一個SqlSession對象對象執行2遍相同的SQL查詢,在第一次查詢執行完畢后將結果緩存起來,這樣第二遍查詢就不用向數據庫查詢了,直接返回緩存結果即可。MyBatis默認是開啟一級緩存的。
二級緩存是mapper級別的緩存,二級緩存是跨SqlSession的,多個SqlSession對象可以共享同一個二級緩存。不同的SqlSession對象執行兩次相同的SQL語句,第一次會將查詢結果進行緩存,第二次查詢直接返回二級緩存中的結果即可。MyBatis默認是不開啟二級緩存的,可以在配置文件中使用如下配置來開啟二級緩存:
<pre style="margin: 0px; padding: 0px; overflow: auto; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"><settings>
<setting name="cacheEnabled" value="true"/>
</settings></pre>
當SQL語句進行更新操作(刪除/添加/更新)時,會清空對應的緩存,保證緩存中存儲的都是最新的數據。MyBatis的二級緩存對細粒度的數據級別的緩存實現不友好,比如如下需求:對商品信息進行緩存,由于商品信息查詢訪問量大,但是要求用戶每次都能查詢最新的商品信息,此時如果使用mybatis的二級緩存就無法實現當一個商品變化時只刷新該商品的緩存信息而不刷新其它商品的信息,因為mybaits的二級緩存區域以mapper為單位劃分,當一個商品信息變化會將所有商品信息的緩存數據全部清空。解決此類問題需要在業務層根據需求對數據有針對性緩存,具體業務具體實現。
以上內容轉自https://www.cnblogs.com/luoxn28/p/6417892.html 轉載只是為了學習 并無他意
mybatis源碼解析圖
源碼編譯與下載
https://github.com/mybatis/mybatis-3
https://github.com/mybatis/parent(依賴)
可以直接idea導入 也可以直接下載為zip包(推薦這種)。
Mybatis源碼襲來parent工程。需要先編譯parent工程在編譯mybatis,具體如下
解決parent依賴問題:
在構建的過程中會出現找不到pom.xml中依賴的父模塊mybatis-parent
我們需要將paren工程克隆到本地目錄中:git clone https://github.com/mybatis/parent.git ,然后先進入parent工程下進行mvn clean install 將parent工程依賴的包下載下來、并保證parent工程編譯通過,這步不會出現問題,在編譯的輸出信息中我們會看到parent工程的版本號,如圖所示:
提交事務
- 調用session.close關閉會話
MyBatis的工作原理
- 讀取MyBatis配置文件。(獲取MyBatis的運行環境等信息)
- 加載映射文件。(SQL映射文件,其中配置了操作數據庫的SQL語句)
- 構造會話工廠:通過MyBatis的環境等配置信息構建會話工廠SqlSessionFactory
- 創建會話對象:有會話工廠創建SqlSession對象,該對象包括了執行SQL語句的所有方法
- Executor執行器:根據SqlSession傳遞的參數動態的生成需要執行的SQL語句,同時負責查詢緩存的維護
- Mappedstatement對象:用于存儲要映射的SQL語句的id、參數等信息
- 輸入參數映射:參數類型可以為Map、List等集合類型也可以使用基本數據類型和POJO類型
- 輸出結果映射:和輸入類似。
MyBatis的優點
- 基于SQL語句編程,相當靈活。SQL寫在XML中,解除sql與程序代碼的耦合,便于統一管理。提供XML標簽,支持編寫動態SQL語句,并可重用
- 消除了 JDBC 大量冗余的代碼,不需要手動開關連接;
- 很好的與各種數據庫兼容
- 能夠與 Spring 很好的集成;
- 提供映射標簽,支持對象與數據庫的 ORM 字段關系映射;提供對象關系映射標簽,支持對象關系組件維護。
MyBatis框架的缺點
- SQL 語句的編寫工作量較大,尤其當字段多、關聯表多時,對開發人員編寫SQL 語句的功底有一定要求。
- SQL 語句依賴于數據庫,導致數據庫移植性差,不能隨意更換數據庫。
MyBatis與Hibernate有哪些不同
- MyBatis是一個半ORM框架,需要自己編寫sql語句,靈活性高,但是需要自定義多套sql映射文件,工作量大
- Hibernate數據庫無關性好,節省代碼,提高效率
{}和${}的區別
{}:是預編譯處理,會把sql中的#{}替換為?,調用PreoaredStatement的set方法來賦值
{}替換為變量的值。
使用#{}可以有效的防止SQL注入,提高系統的安全性 。
當實體類的屬性名和表中的字段名不一致如何處理
通過在查詢的sql語句中定義字段名的別名,讓字段名的別名和實體類的屬性名一致。
通過<resultMap>類映射字段名和實體類屬性名的一一對應的關系
模糊查詢like怎么編寫
- 在Java代碼中添加sql通配符
- 在sql語句中拼接通配符(但是會引起sql注入問題)
通常一個Xml映射文件,都會寫一個Dao接口與之對應。那么Dao接口的工作原理是什么。
Dao接口就是Mapper接口。
- 接口的全限定名就是映射文件的namespace的值
- 接口的方法名就是映射文件中Mapper的Statement的id值
- 接口方法內的參數就是傳遞給sql的參數
Mapper接口是沒有實現類的,當調用接口方法的時候,接口的全限定名+方法名拼接字符串作為key值,可以唯一定位一個MapperStatement。在MyBatis中,每一個<select>、<insert>、<update>、<delete>標簽都會被解析為一個MapperStatement對象
Dao接口的方法,參數不同時,方法能重載嗎
Mapper接口里的方法,是不能重載的,因為使用全限定名+方法名的保存和尋找策略。所以不能重載。
Mapper接口的工作原理是JDK動態代理,MyBatis運行時會使用JDK動態代理為Mapper接口生成代理對象proxy,代理對象會攔截接口方法,轉而執行MapperStatement所代表的sql,然后將sql執行結果返回。
MyBatis是如何進行分頁的以及分頁插件的原理是什么
MyBatis使用RowBounds對象進行分頁,它是針對ResultSet結果集執行的內存分頁,而非物理分頁。可以在sql內直接書寫帶有物理分頁的參數來完成物理的分頁功能,或者使用分頁插件來完成物理分頁
分頁插件的基本原理就是使用MyBatis提供的插件接口,實現自定義插件,在插件的攔截方法內攔截待執行的sql,然后重寫sql,根據dialect,添加對應的物理分頁語句和物理分頁參數。
Mybati是如何將sql執行機構封裝為對象并返回的?有哪些映射形式
- 使用<resultMap>標簽,逐一定義數據庫列名和對象屬性名之間的映射關系。
- 使用sql列的別名功能,將列別名書寫為對象屬性名
有了列名和屬性名的映射關系后,MyBatis通過反射創建對象,同時使用反射給對象的屬性逐一賦值并返回,那些找不到映射關系的屬性,是無法完成賦值的
如何進行批量插入
如何獲取自動生成的主鍵值
- 使用JDBC內置方法
- 插入后查詢獲取
- 插入前查詢獲取主鍵
如何在mapper中傳遞多個參數
- Dao層函數
- 使用@param注解
- 多個參數封裝成map
MyBatis動態sql
MyBatis動態sql可以在Xml映射文件內,以標簽的形式編寫動態sql,執行原理是根據表達式的值完成邏輯判斷并動態拼接sql功能
MyBatis提供了九種sql標簽
- trim
- where
- set
- foreach
- if
- choose
- when
- otherwise
- bind
XML文件標簽
- select
- insert
- update
- delete
- resultMap
- parameterMap
- sql
- include
- selectKey
MyBatis的Xml映射文件中,不同的Xml文件id是否可以重復
如果配置了namespace那么id可以重復,要是沒有配置namespace,id就不可以重復
MyBatis實現一對一有幾種方式
聯合查詢
- 幾個表聯合查詢,只查詢一次,通過配置collection節點
嵌套查詢
- 先查一個表,再根據查出的id去另外一個表里查詢數據,也是通過配置collection,但是另外一個表的查詢通過select節點配置
MyBatis是否支持延遲加載,以及如何實現
Mybatis僅支持association關聯對象和collection關聯集合對象的延遲加載。可以通過配置lazyLoadingEnabled來進行配置。
原理
使用 CGLIB 創建目標對象的代理對象,當調用目標方法時,進入攔截器方法,比如調用 a.getB().getName(),攔截器 invoke()方法發現 a.getB()是null 值,那么就會單獨發送事先保存好的查詢關聯 B 對象的 sql,把 B 查詢上來,然后調用 a.setB(b),于是 a 的對象 b 屬性就有值了,接著完成 a.getB().getName()方法的調用。這就是延遲加載的基本原理。
MyBatis的緩存
- 一級緩存: 基于 PerpetualCache 的 HashMap 本地緩存,其存儲作用域為Session,當 Session flush 或 close 之后,該 Session 中的所有 Cache 就將清空,默認打開一級緩存。
- 二級緩存與一級緩存其機制相同,默認也是采用 PerpetualCache,HashMap存儲,不同在于其存儲作用域為 Mapper(Namespace),并且可自定義存儲源,如 Ehcache。默認不打開二級緩存,要開啟二級緩存,使用二級緩存屬性類需要實現 Serializable 序列化接口(可用來保存對象的狀態),可在它的映射文件中配置<cache/> ;
緩存更新
- 進行增刪改
- 調用清除方法
- 設置清除屬性
MyBatis的接口綁定,以及實現方式
接口綁定:就是在MyBatis中任意定義接口,然后把接口里面的方法和SQL語句進行綁定,我們在使用的時候直接調用接口方法即可
實現方式
- 通過注解綁定
- 通過xml里面寫sql語句來綁定,需要指定xml中namespace必須為接口的全路徑名。
Mybatis的mapper接口調用時有哪些要求
- Mapper接口的方法名和mapper.xml中sql的id相同
- Mapper 接口方法的輸入參數類型和 mapper.xml 中定義的每個 sql 的parameterType 的類型相同
- Mapper 接口方法的輸出參數類型和 mapper.xml 中定義的每個 sql 的resultType 的類型相同
- Mapper.xml 文件中的 namespace 即是 mapper 接口的類路徑
Mybatis 的插件運行原理,以及如何編寫一個插件。
Mybatis 僅可以編寫針對 ParameterHandler、ResultSetHandler、StatementHandler、Executor 這 4 種接口的插件,Mybatis 使用 JDK 的動態代理,為需要攔截的接口生成代理對象以實現接口方法攔截功能,每當執行這 4 種接口對象的方法時,就會進入攔截方法,具體就是 InvocationHandler 的 invoke()方法,當然,只會攔截那些你指定需要攔截的方法。
編寫插件:實現 Mybatis 的 Interceptor 接口并復寫 intercept()方法,然后在給插件編寫注解,指定要攔截哪一個接口的哪些方法即可,記住,別忘了在配置文件中配置你編寫的插件。
預編譯
定義
SQL預編譯是指數據庫驅動在發送SQL語句和參數給DBMS之前對SQL語句進行編譯,這樣DBMS執行SQL時,就不需要重現編譯
預編譯作用
可以優化SQL的執行。預編譯后的大多數SQL可以直接運行,同時預編譯語句對象可以重復利用。還可以防止SQL注入
MyBatis有哪些Executor執行器,他們之間的區別是什么
有三種基本的Executor執行器:SimpleExecutor、ReuseExxecutor、BatchExecutor
SimpleExecutor
每執行一次update或者select就開啟一個Statement對象,用完就立即關閉
ReuseRxecutor
執行update或者select,以sql為key查找Statement對象。為了重復使用Statement對象
BatchExecutor
執行update,將所有sql都添加到批處理中,等待同一執行。其魂村了多個Statement對象。
當實體類中的屬性名和表中的字段名不一樣,怎么辦
- 通過在查詢的SQL語句中定義字段名的別名,讓字段名和實體類的屬性名一致
- 通過resultMap來映射字段名和實體類屬性名的一一對應的關系
ResultType和ResultMap的區別
首先MyBatis在查詢進行select映射的時候,返回類型可以用resultType也可以用resultMap,其中resultType是直接表示返回類型的,而resultMap則是對外部ResultMap的引用。這兩不能同時存在
在MyBatis進行查詢映射的時候,其實查詢出來的每一個屬性都是放在一個對應的Map中,鍵是屬性名、值是對應的值。
3. MyBatis是如何進行分頁的?分頁插件的原理是什么?
Mybatis使用RowBounds對象進行分頁,它是針對ResultSet結果集執行的內存分頁,而非物理分頁。也可以直接編寫SQL實現分頁或者使用分頁插件。
分頁插件的原理:實現MyBatis提供的接口來實現自定義插件,然后在插件的攔截方法內攔截待執行的SQL,最后重寫SQL。
4. Dao 接口里的方法,參數不同時,方法能重載嗎?
Mapper 接口里的方法,是不能重載的,因為是使用全限名+方法名的保存和尋找策略。Mapper 接口的工作原理是 JDK 動態代理,Mybatis 運行時會使用 JDK動態代理為 Mapper 接口生成代理對象 proxy,代理對象會攔截接口方法,轉而執行 MapperStatement 所代表的 sql,然后將 sql 執行結果返回。
5. MyBatis是如何將sql執行結果封裝為目標對象并返回的?都有哪些映射形式?
使用resultMap標簽,定義數據庫列名和對象屬性名之間的映射關系。
使用sql列的別名功能,將列的別名書寫為對象屬性名。
有了列名與屬性名的映射關系后,MyBatis通過反射創建對象,同時使用反射給對象的屬性逐一賦值并返回。
6. Mybaits 的優缺點?
優點:
基于SQL語句編程,相當靈活,不會對應用程序或者數據庫的現有設計造成任何影響,SQL寫在XML里,解除sql與程序代碼的耦合,便于統一管理;提供XML標簽,支持編寫動態SQL語句,并可重用。
與JDBC相比,減少了50%以上的代碼量,消除了JDBC大量冗余的代碼,不需要手動開關連接;
很好的與各種數據庫兼容,能夠與 Spring 很好的集成;
提供映射標簽,支持對象與數據庫的ORM字段關系映射;提供對象關系映射標簽,支持對象關系組件維護。
缺點:
SQL語句的編寫工作量較大,尤其當字段多、關聯表多時,對開發人員編寫SQL語句的功底有一定要求。
QL語句依賴于數據庫,導致數據庫移植性差,不能隨意更換數據庫。
7. 談談你對MyBatis的理解?
MyBatis是一個可以自定義SQL、存儲過程和高級映射的持久層框架,它內部封裝了JDBC,開發時只需要關注SQL語句本身。程序員直接編寫原生態SQL,可以嚴格控制sql執行性能,靈活度高。
8. MyBatis的Xml映射文件中,不同的Xml映射文件,id是否可以重復?
不同的Xml映射文件,如果配置了namespace,id可以重復。如果沒有配置namespace,id不能重復。原因是namespace+id是作為Map<String,MapperStatement>的key使用的,如果沒有namespace,id重復會導致數據互相覆蓋。有了namespace,id就可以重復,namespace不同,namespace+id自然也就不同。
9. #{}和${}的區別是什么?
是預編譯處理,KaTeX parse error: Expected 'EOF', got '#' at position 19: …串替換。 Mybatis在處理#?時,會將sql中的#{}替換為…時,就是把$替換成變量的值。
使用#可以有效的防止SQL注入,提高系統安全性。
10. 當實體類中的屬性名和表中的字段名不一樣 ,怎么辦?
通過在查詢的sql語句中定義字段名的別名。
通過resultMap標簽來映射字段名和實體類屬性名的一一對應的關系。
11. Mybatis映射文件中,如果A標簽通過include引用了B標簽的內容,請問B標簽能否定義在A標簽的后面,還是說必須定義在A標簽的前面?
Mybatis解析Xml映射文件是按照順序解析的,但是被引用的B標簽定義在任何地方Mybatis都可以正確識別。原理是Mybatis解析A標簽,發現A標簽引用了B標簽,但是B標簽尚未解析到,此時會將A標簽標記為未解析狀態,然后繼續解析余下的標簽,包含B標簽。待所有標簽解析完畢,Mybatis會重新解析那些被標記為未解析的標簽,此時再解析A標簽時,B標簽已經存在,A標簽也就可以正常解析完成了。
12. MyBats中的 一對一、一對多的關聯查詢 ?
定義一個resultMap,使用association屬性實現一對一的關聯查詢;使用collection屬性實現一對多的關聯查詢。
13. MyBatis實現一對一有幾種方式?具體怎么操作的?
聯合查詢是幾個表聯合查詢,只查詢一次, 通過在resultMap里面配置association節點配置一對一的類就可以完成;
嵌套查詢先查一個表,根據這個表里面的結果的外鍵id,去再另外一個表里面查詢數據,也是通過association配置,但另外一個表的查詢通過select屬性配置。
14. MyBatis 的好處是什么?
把sql語句從 Java 源程序中獨立出來,放在單獨的XML文件中編寫,給程序的維護帶來了很大便利。
能自動將結果集轉換成Java Bean對象,大大簡化了Java數據庫編程的重復工作。
程序員可以結合數據庫自身的特點靈活控制sql語句,能夠完成復雜查詢。
15. 講一下MyBatis的一級、二級緩存(說說MyBatis的緩存)?
MyBatis的緩存分為一級緩存和二級緩存,一級緩存放在session里面,默認就有。二級緩存放在它的命名空間里,默認是不打開的,使用二級緩存屬性類需要實現Serializable序列化接口,可在它的映射文件中配置cache標簽
16. Mapper 編寫有哪幾種方式?
接口實現類繼承 SqlSessionDaoSupport,需要編寫mapper 接口,mapper 接口實現類、mapper.xml 文件。
在 sqlMapConfig.xml 中配置 mapper.xml 的位置
定義 mapper 接口
實現類集成 SqlSessionDaoSupportmapper 方法中可以 this.getSqlSession()進行數據增刪改查。
spring 配置
使用 org.mybatis.spring.mapper.MapperFactoryBean,在 sqlMapConfig.xml 中配置 mapper.xml 的位置,定義 mapper 接口:
mapper.xml 中的 namespace 為 mapper 接口的地址
mapper 接口中的方法名和 mapper.xml 中的定義的 statement 的 id 保持一致
Spring 中定義
第三種:使用mapper掃描器:
mapper.xml 文件編寫:
定義 mapper 接口,注意 mapper.xml 的文件名和 mapper 的接口名稱保持一致,且放在同一個目錄
配置 mapper 掃描器:
使用掃描器后從 spring 容器中獲取 mapper 的實現對象。
17. MyBatis在mapper中如何傳遞多個參數?
直接在方法中傳遞參數,xml文件用#符號來獲取
使用@param注解或者多個參數封裝成 map
18. MyBatis 是否支持延遲加載?如果支持,它的實現原理是什么?
MyBatis僅支持association關聯對象和collection關聯集合對象的延遲加載,association指的就是一對一,collection指的就是一對多查詢。在 MyBatis配置文件中,可以通過 lazyLoadingEnabled=true/false 配置是否啟用延遲加載。
原理是:使用CGLIB創建目標對象的代理對象,當調用目標方法時會進入攔截器方法,比如調用a.getB().getName(),攔截器invoke()方法發現a.getB()是null值,那么就會單獨發送事先保存好的查詢關聯B對象的sql,把B查詢上來,然后調用a.setB(),于是a的對象b屬性就有值了,接著完成a.getB().getName()方法的調用。
19. Xml 映射文件中,除了常見的 select、insert、updae、delete標簽之外,還有哪些標簽?
resultMap、parameterMap、sql、include、selectKey,加上動態sql的9個標簽,其中sql為sql片段標簽,通過include標簽標簽引入sql片段,selectKey標簽為不支持自增的主鍵生成策略標簽。
20. 通常一個Xml映射文件,都會寫一個Dao接口與之對應,請問這個 Dao 接口的工作原理是什么,是否可以重載?
不能重載,因為通過Dao尋找Xml對應sql的時候,使用全限名+方法名的保存和尋找策略。Dao接口工作原理為jdk動態代理原理,運行時會為dao生成proxy,代理對象會攔截接口方法,去執行對應的sql返回數據。
21. 什么是MyBatis的接口綁定?有哪些實現方式?
接口綁定是在MyBatis中任意定義接口,然后把接口里面的方法和SQL語句綁定。
接口綁定有兩種實現方式:
通過注解綁定,在接口的方法上面加上@Select、@Update等注解,里面包含Sql語句來綁定;
通過xml里面寫SQL來綁定,要指定xml映射文件里面的namespace必須為接口的全路徑名。
當Sql語句比較簡單時候,用注解綁定;當SQL語句比較復雜時候,用xml綁定,一般用 xml 綁定的比較多。
22. MyBatis如何獲取自動生成的(主)鍵值?
在insert標簽中使用useGeneratedKeys和keyProperty兩個屬性來獲取自動生成的主鍵值。
23. 使用MyBatis的mapper接口調用時有哪些要求?
Mapper 接口方法名和 mapper.xml 中定義的每個 sql 的 id 相同;
Mapper 接口方法的輸入參數類型和 mapper.xml 中定義的每個 sql 的parameterType 的類型相同;
Mapper 接口方法的輸出參數類型和 mapper.xml 中定義的每個 sql 的resultType 的類型相同;
Mapper.xml 文件中的 namespace 即是 mapper 接口的類路徑。
24. MyBatis 動態sql有什么用?執行原理?有哪些動態sql?
MyBatis動態sql可以在Xml映射文件內,以標簽的形式編寫動態sql。
執行原理是根據表達式的值完成邏輯判斷并動態拼接 sql 的功能。
MyBatis提供了9種動態sql標簽:trim、where、set、foreach、if、choose、when、otherwise、bind。
25. MyBatis是否可以映射 Enum 枚舉類?
MyBatis不單可以映射枚舉類,還可以映射任何對象到表的一列上。映射方式為自定義一個TypeHandler,實現TypeHandler的setParameter()和getResult()接口方法。TypeHandler有兩個作用,一是完成從javaType至jdbcType的轉換,二是完成jdbcType至javaType的轉換,體現為setParameter()和getResult()兩個方法,分別代表設置sql問號占位符參數和獲取列查詢結果。
26. Mybatis能執行一對一、一對多的關聯查詢嗎?都有哪些實現方式,以及它們之間的區別?
Mybatis不僅可以執行一對一、一對多的關聯查詢,還可以執行多對一,多對多的關聯查詢。多對一查詢,其實就是一對一查詢,只需要把selectOne()修改為selectList()即可;多對多查詢,其實就是一對多查詢,只需要把selectOne()修改為selectList()即可。
關聯對象查詢有兩種實現方式,一種是單獨發送一個sql去查詢關聯對象,賦給主對象,然后返回主對象。另一種是使用嵌套查詢,嵌套查詢的含義為使用join查詢,一部分列是A對象的屬性值,另外一部分列是關聯對象B的屬性值,好處是只發一個sql查詢,就可以把主對象和其關聯對象查出來。
27. MyBatis里面的動態Sql是怎么設定的?用什么語法?
MyBatis里面的動態Sql一般是通過if節點來實現,通過OGNL語法來實現,但是如果要寫的完整,必須配合where,trim節點,where節點是判斷包含節點有內容就插入where,否則不插入,trim節點是用來判斷如果動態語句是以and或or開始,那么會自動把這個and或者or取掉。
28. 簡述MyBatis的插件運行原理,以及如何編寫一個插件?
運行原理:MyBatis只能編寫針對ParameterHandler、ResultSetHandler、StatementHandler、Executor四種接口的插件,MyBatis通過動態代理,為需要攔截的接口生成代理對象以實現接口方法攔截功能,每當執行這四種接口對象的方法時,就會進入攔截方法。具體就是調用InvocationHandler的invoke()方法,只會攔截指定需要攔截的方法。
編寫插件:實現MyBatis的Interceptor接口并復寫 ntercept()方法,然后給插件編寫注解,指定要攔截哪一個接口的哪些方法,最后在配置文件中配置編寫的插件。
29. 為什么說Mybatis是半自動 ORM 映射工具?它與全自動的區別在哪里?
Mybatis在查詢關聯對象或關聯集合對象時,需要手動編寫sql來完成,所以稱之為半自動ORM映射工具。
30. resultType和resultMap的區別?
類的名字和數據庫相同時,可以直接設置resultType參數為Pojo類。若不同,需要設置resultMap將結果名字和Pojo名字進行轉換。