MyBatis架构原理
一、架构整体设计功能架构讲解:我们把Mybatis的功能架构分为三层:(1)API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。(2)数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。(3)...
一、架构整体设计
功能架构讲解:
我们把Mybatis的功能架构分为三层:
(1)API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
(2)数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
(3)基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
代码架构
1.1基础支持层
基础支持层包含整个Mybatis的基础模块,这些模块为核心处理层的功能提供了良好的支撑。下面简单描述各个模块的功能。
-
反射模块
Mybatis中专门提供了反射模块,该模块对Java原生的反射进行了良好的封装,提供了更加简洁易用的API,方便上层使用,并且对反射操作进行了一系列优化,例如缓存了类的元数据,提高了反射操作的性能。
-
类型转换模块
为简化配置文件提供了别名机制,该机制是类型转换模块的主要功能之一。类型转换模块的另一个功能是实现 JDBC 类型与 Java 类型之间的转换,该功能在为 SQL 语句绑定实参以及映射查询结果集时都会涉及。在为SQL语句绑定实参时,会将数据由 Java 类型转换 JDBC 类型;而在映射结果集时,会将数据由 JDBC 类型转换成 Java 类型。
-
日志模块
MyBatis 作为一个设计优良的框架,除了提供详细的日志输出信息,还要能够集成多种日志框架,其日志模块的一个主要功能就是集成第三方日志框架。
-
资源加载模块
资源加载模块主要是对类加载器进行封装,确定类加载器的使用顺序,并提供了加载类文件以及其他资源文件的功能。
-
解析器模块
解析器模块的主要提供了两个功能:一个功能是对 XPath 进行封装,为 MyBatis 初始化时解析 mybatis-config.xml 配置文件以及映射配置文件提供支持;另一个功能是为处理动态 SQL 语句中的占位符提供支持。
-
数据源模块
MyBatis 自身提供了相应的数据源实现,当然 MyBatis 也提供了与第三方数据源集成的接口,这些功能都位于数据源模块之中。
-
事务管理
MyBatis 对数据库中的事务进行了抽象,其自身提供了相应的事务接口和简单实现。但在大多数场景当中,Mybatis 会与 Spring 框架集成,并由 Spring 框架来管理事务。
-
缓存模块
My Batis 中提供了一级缓存和二级缓存,而这两级缓存都是依赖于基础支持层中的缓存模块实现的。这里需要注意的是, MyBatis 中自带的这两级缓存与 MyBatis 以及整个应用是运行在同一个 JVM 中的,共享同一块堆内存。如果这两级缓存中的数据量较大, 则可能影响系统中其他功能的运行,所以当需要缓存大量数据时 ,优先考虑使用 Redis、 Memcache 等缓存产品。
Binding模块
Binging模块可以在编译器帮助我们检查在映射文件中定义的SQL节点是否有出现错误
1.2 核心处理层
在核心处理层中实现了Mybatis的核心处理流程,其中包括Mybatis的初始化以及完成一次数据库操作所涉及的全部流程。
-
配置解析
在 MyBatis 初始化过程中,会加载 mybatis-config.xml 配置文件、映射配置文件以及Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到 Configuration 对象中。之后,利用该 configuration 对象创建 SqlSessionFactory 对象。待 MyBatis 初始化之后,开发人员可以通过初始化得到 SqlSessionFactory 创建 SqlSession 对象并完成数据库操作。
-
SQL解析与scripting模块
MyBatis 实现动态 SQL 语句的功能,提供了多种动态 SQL 语句对应的节点,例如,<where>节点、<if>节点、<foreach>节点等。通过这些节点的组合使用, 开发人员可以写出几乎满足所有需求的动态SQL语句。
MyBatis 中的 scripting 模块会根据用户传入的实参,解析映射文件中定义的动态 SQL 节点,并形成数据库可执行的 SQL 语句 。之后会处理 SQL 语句中的占位符,绑定用户传入的实参。 -
SQL执行
SQL 语句的执行涉及多个组件 ,其中比较重要的是 Executor、StatementHandler、ParameterHandler 和ResultSetHandler。Executor 主要负责维护一级缓存和二级缓存,并提供事务管理的相关操作 ,它会将数据库相关操作委托给 StatementHandler 完成。StatementHandler 首先通过 ParameterHandler 完成 SQL 语句的实参绑定,然后通过java.sql.Statement 对象执行 SQL 语句并得到结果集,最后通过 ResultSetHandler 完成结果集的映射,得到结果对象并返回 。下图展示了 MyBatis 执行 SQL 语句的大致过程。
-
插件
Mybatis 自身的功能虽然强大,但是并不能完美切合所有的应用场景,因此 MyBatis提供了插件接口,我们可以通过添加用户自定义插件的方式对 MyBatis 进行扩展。用户自定义插件也可以改变 Mybatis 默认行为,例如,我们可以拦截 SQL 语句并对其进行重写。由于用户自定义插件会影响 MyBatis 核心行为,在使用自定义插件之前,开发人员需要了解 MyBatis内部的原理,这样才能编写出安全、高效的插件。
1.3 接口层
接口层的核心是 SqlSession 接口,该接口中定义了 MyBatis 暴露给应用程序调用的 API ,也就是上层应用与 MyBatis 交互的桥梁。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作。
二、mybatais执行流程
下面作简要概述:
- 加载配置信息并保存到Configuration对象中
- 加载完mybatis配置信息并构造SqlSessionFactory(即会话工厂)。SqlSessionFactory拥有获取配置信息的方法。
- 由会话工厂创建sqlSession即会话,操作数据库需要通过sqlSession进行。
- mybatis底层自定义了Executor执行器接口操作数据库,Executor接口有两个实现,一个是基本执行器、一个是缓存执行器。
- Executor会将数据库相关操作委托给 StatementHandler 完成。它包装了mybatis配置信息及sql映射信息等。mapper.xml文件中一个sql对应一个MappedStatement对象,sql的id即是MappedStatement的id。
- StatementHandler 首先通过 ParameterHandler 完成 SQL 语句的实参绑定,然后通过java.sql.Statement 对象执行 SQL 语句并得到结果集。实参绑定包括对HashMap、基本类型、pojo,输入参数映射就是JDBC编程中对preparedStatement设置参数。
- 最后通过 ResultSetHandler 完成结果集的映射。映射包括HashMap、基本类型、pojo,Executor通过MappedStatement在执行sql后将输出结果映射至java对象中,输出结果映射过程相当于JDBC编程中对结果的解析处理过程。
测试代码如下:
@Test
public void getUserTest() {
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
InputStream input = getClass().getResourceAsStream("/mybatis-context-config.xml");
SqlSessionFactory devSession = builder.build(input);
System.out.println(devSession.toString());
UserInfoMapper mapper= devSession.openSession().getMapper(UserInfoMapper.class);
UserInfo userinfo = mapper.getUser(1);
System.out.println(userinfo.toString());
Assert.assertNotNull(userinfo);
}
myBatis 执行流程时序图
三、核心组件分析
1.API接口层
SqlSessionFactory
SqlSessionFactory:主要用来创建会话。
源码如下:
import java.sql.Connection;
/**
* Creates an {@link SqlSession} out of a connection or a DataSource
*
* @author Clinton Begin
*/
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
SqlSessionFactory接口的默认实现类为DefaultSqlSessionFactory,其关键部分源码如下:
/**
* @author Clinton Begin
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
/**
*默认采用SimpleExecutor类进行sql执行
*
/
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
@Override
public Configuration getConfiguration() {
return configuration;
}
/**
*最后使用DefaultSqlSession类进行执行
*
*/
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);
return new DefaultSqlSession(configuration, executor, autoCommit);
} 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();
}
}
}
DefaultSqlSessionFactory有一个属性为Configuration对象,即将会依赖于Configuration对象。DefaultSqlSessionFactory在开启回话前,就已经完成了Configuration对象的初始化。
SqlSessionFactoryBuilder
SqlSessionFactory为整个框架的入口点,其主要生成方式采用SqlSessionFactoryBuilder的build构建,SqlSessionFactoryBuilder中共有许多build重载方法,参数共有如下几种:配置文件输入流(Reader、InputStream), Properties和Environment。它的作用就是返回一个SqlSessionFactory。其源码如下:
/**
* Builds {@link SqlSession} instances.
*
* @author Clinton Begin
*/
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}
public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//注意parser.parse()将会返回一个Configuration对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
从中我们可以看到:SqlSessionFactoryBuilder默认采用的是解析xml的方式,解析xml后就可以获得对应得Configuration,然后使用默认实现SqlSessionFactory接口的DefaultSqlSessionFactory类作为SqlSessionFactory。
SqlSession
SqlSession默认实现类为DefaultSqlSession,SqlSession接口源码如下:
public interface SqlSession extends Closeable {
/**
* Retrieve a single row mapped from the statement key
* @param <T> the returned object type
* @param statement
* @return Mapped object
*/
<T> T selectOne(String statement);
/**
* Retrieve a single row mapped from the statement key and parameter.
* @param <T> the returned object type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return Mapped object
*/
<T> T selectOne(String statement, Object parameter);
/**
* Retrieve a list of mapped objects from the statement key and parameter.
* @param <E> the returned list element type
* @param statement Unique identifier matching the statement to use.
* @return List of mapped object
*/
<E> List<E> selectList(String statement);
/**
* Retrieve a list of mapped objects from the statement key and parameter.
* @param <E> the returned list element type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return List of mapped object
*/
<E> List<E> selectList(String statement, Object parameter);
/**
* Retrieve a list of mapped objects from the statement key and parameter,
* within the specified row bounds.
* @param <E> the returned list element type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @param rowBounds Bounds to limit object retrieval
* @return List of mapped object
*/
<E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
/**
* The selectMap is a special case in that it is designed to convert a list
* of results into a Map based on one of the properties in the resulting
* objects.
* Eg. Return a of Map[Integer,Author] for selectMap("selectAuthors","id")
* @param <K> the returned Map keys type
* @param <V> the returned Map values type
* @param statement Unique identifier matching the statement to use.
* @param mapKey The property to use as key for each value in the list.
* @return Map containing key pair data.
*/
<K, V> Map<K, V> selectMap(String statement, String mapKey);
/**
* The selectMap is a special case in that it is designed to convert a list
* of results into a Map based on one of the properties in the resulting
* objects.
* @param <K> the returned Map keys type
* @param <V> the returned Map values type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @param mapKey The property to use as key for each value in the list.
* @return Map containing key pair data.
*/
<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);
/**
* The selectMap is a special case in that it is designed to convert a list
* of results into a Map based on one of the properties in the resulting
* objects.
* @param <K> the returned Map keys type
* @param <V> the returned Map values type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @param mapKey The property to use as key for each value in the list.
* @param rowBounds Bounds to limit object retrieval
* @return Map containing key pair data.
*/
<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
/**
* A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.
* @param <T> the returned cursor element type.
* @param statement Unique identifier matching the statement to use.
* @return Cursor of mapped objects
*/
<T> Cursor<T> selectCursor(String statement);
/**
* A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.
* @param <T> the returned cursor element type.
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return Cursor of mapped objects
*/
<T> Cursor<T> selectCursor(String statement, Object parameter);
/**
* A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.
* @param <T> the returned cursor element type.
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @param rowBounds Bounds to limit object retrieval
* @return Cursor of mapped objects
*/
<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);
/**
* Retrieve a single row mapped from the statement key and parameter
* using a {@code ResultHandler}.
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @param handler ResultHandler that will handle each retrieved row
*/
void select(String statement, Object parameter, ResultHandler handler);
/**
* Retrieve a single row mapped from the statement
* using a {@code ResultHandler}.
* @param statement Unique identifier matching the statement to use.
* @param handler ResultHandler that will handle each retrieved row
*/
void select(String statement, ResultHandler handler);
/**
* Retrieve a single row mapped from the statement key and parameter
* using a {@code ResultHandler} and {@code RowBounds}
* @param statement Unique identifier matching the statement to use.
* @param rowBounds RowBound instance to limit the query results
* @param handler ResultHandler that will handle each retrieved row
*/
void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
/**
* Execute an insert statement.
* @param statement Unique identifier matching the statement to execute.
* @return int The number of rows affected by the insert.
*/
int insert(String statement);
/**
* Execute an insert statement with the given parameter object. Any generated
* autoincrement values or selectKey entries will modify the given parameter
* object properties. Only the number of rows affected will be returned.
* @param statement Unique identifier matching the statement to execute.
* @param parameter A parameter object to pass to the statement.
* @return int The number of rows affected by the insert.
*/
int insert(String statement, Object parameter);
/**
* Execute an update statement. The number of rows affected will be returned.
* @param statement Unique identifier matching the statement to execute.
* @return int The number of rows affected by the update.
*/
int update(String statement);
/**
* Execute an update statement. The number of rows affected will be returned.
* @param statement Unique identifier matching the statement to execute.
* @param parameter A parameter object to pass to the statement.
* @return int The number of rows affected by the update.
*/
int update(String statement, Object parameter);
/**
* Execute a delete statement. The number of rows affected will be returned.
* @param statement Unique identifier matching the statement to execute.
* @return int The number of rows affected by the delete.
*/
int delete(String statement);
/**
* Execute a delete statement. The number of rows affected will be returned.
* @param statement Unique identifier matching the statement to execute.
* @param parameter A parameter object to pass to the statement.
* @return int The number of rows affected by the delete.
*/
int delete(String statement, Object parameter);
/**
* Flushes batch statements and commits database connection.
* Note that database connection will not be committed if no updates/deletes/inserts were called.
* To force the commit call {@link SqlSession#commit(boolean)}
*/
void commit();
/**
* Flushes batch statements and commits database connection.
* @param force forces connection commit
*/
void commit(boolean force);
/**
* Discards pending batch statements and rolls database connection back.
* Note that database connection will not be rolled back if no updates/deletes/inserts were called.
* To force the rollback call {@link SqlSession#rollback(boolean)}
*/
void rollback();
/**
* Discards pending batch statements and rolls database connection back.
* Note that database connection will not be rolled back if no updates/deletes/inserts were called.
* @param force forces connection rollback
*/
void rollback(boolean force);
/**
* Flushes batch statements.
* @return BatchResult list of updated records
* @since 3.0.6
*/
List<BatchResult> flushStatements();
/**
* Closes the session
*/
@Override
void close();
/**
* Clears local session cache
*/
void clearCache();
/**
* Retrieves current configuration
* @return Configuration
*/
Configuration getConfiguration();
/**
* Retrieves a mapper.
*获取被代理的类
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
<T> T getMapper(Class<T> type);
/**
* Retrieves inner database connection
* @return Connection
*/
Connection getConnection();
}
DefaultSqlSession
在 DefaultSq!Session 中使用到了策略模式, DefaultSq!Session 扮演了 Context 的角色,而将
所有数据库相关的操作全部封装到 Executor 接口实现中,并通过 executor 字段选择不同的
Executor 实现。
其关键源码如下:
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
}
2.基础支撑层
Configuration
mybatis的所有配置信息都存储到org.apache.ibatis.session.Configuration类中,可通过xml配置或者手动实例化获取到Configuration信息。也就是Configuration管理了所有的配置信息。其关键属性如下:
/**
* @author Clinton Begin
*/
public class Configuration {
//Environment主要用于配置数据源和事务信息
protected Environment environment;
//一些默认配置
protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel = true;
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls;
protected boolean useActualParamName = true;
protected boolean returnInstanceForEmptyRow;
}
Environment主要用于配置数据源和事务信息,Environment的属性源码设置如下
/**
* @author Clinton Begin
*/
public final class Environment {
private final String id;
private final TransactionFactory transactionFactory;
private final DataSource dataSource;
}
3.数据处理层
Executor
Executor:Executor 是 MyBati s 的核心接口之一 , 其中定义了数据库操作的基本方法。在实际应用中
经常涉及的 SqISession 接口的功能,都是基于 Executor 接口实现的 。
BaseExecutor:BaseExecutor 是一个实现了 Executor 接口的抽象类 ,它实现了 Executor 接口的大部分方法,
其中就使用了模板方法模式。
SimpleExecutor:SimpleExecutor 继承了 BaseExecutor 抽象类 , 它是最简单的 Executor 接口实现。
SimpleExecutor是Mybatis执行Mapper语句时默认使用的Executor。它提供最基本的Mapper语句执行功能,没有过多的封装的。
正如前面所说, Executor 使用了模板方法模式, 一级缓存等固定不变的操作都封装到了 BaseExecutor 中 ,
在 SimpleExecutor 中就不必再关心一级缓存等操作,只需要专注实现 4 个基本方法的实现即可。
ReuseExecutor:ReuseExecutor,顾名思义,是可以重用的Executor。它重用的是Statement对象,它会在内部利用一个Map把创建的Statement都缓存起来,每次在执行一条SQL语句时,它都会去判断之前是否存在基于该SQL缓存的Statement对象,
存在而且之前缓存的Statement对象对应的Connection还没有关闭的时候就继续用之前的Statement对象,否则将创建一个新的Statement对象,并将其缓存起来。
因为每一个新的SqlSession都有一个新的Executor对象,所以我们缓存在ReuseExecutor上的Statement的作用域是同一个SqlSession。
BatchExecutor:BatchExecutor的设计主要是用于做批量更新操作的。其底层会调用Statement的executeBatch()方法实现批量操作。以下是BatchExecutor的源码。
动态代理
同动态代理模式的设计原则,mybatis也主要是两个大步骤:
1.代理对象的创建
2.使用代理对象,即实现InvocationHandler接口
mybatis代理对象的创建使用了工厂模式,使用MapperProxyFactory类进行创建。源码如下:
/**
* @author Lasse Voss
*/
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
mybatis实现InvocationHandler接口的类为MapperProxy,源码如下:
/**
* @author Clinton Begin
* @author Eduardo Macarron
*/
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
MapperMethod
执行被代理的类所要执行的方法,通过调用execute方法
MapperMethod有一个成员内部类SqlCommand和静态内部类MethodSignature
SqlCommand :MapperMethod的一个成员内部类,有两个属性:name和Type。name成员就是节点的ID,type成员表示查寻还是更新或是删除
MethodSignature:他用于说明方法的一些信息,主要有返回信息。
execute实现源码如下:
/**
* @author Clinton Begin
* @author Eduardo Macarron
* @author Lasse Voss
*/
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
/**
* 执行被代理的方法
*
*
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
}
最后到executor组件
@Override
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();
}
}
最后到SimpleExecutor的doQuery方法
@Override
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);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
StatementHandler
StatementHandler 接口是 MyBatis 的核心接口之一,它完成了 MyBatis 中最核心的工作,也
是后面要介绍的 Executor 接口实现的基础。
StatementHandler 接口中的功能很多,例如创建 Statement 对象,为 SQL 语句绑定实参,执
行 select、 insert、 update 、 delete 等多种类型的 SQL 语句,批量执行 SQL 语句,将结果集映射
成结果对象。
RoutingStatementHandler 会根据 MappedStatement 中 指定的 statementType 宇段,创建对应
的 StatementHandler 接口实现。可以把它理解为下面三个类的代理类。
PreparedStatementHandler 底层依赖于 java.sq1.PreparedStatement 对象来完成数据库的相关操
作 ,默认的实现就是PreparedStatementHandler
四、mybatis插件
Mybatis 中也提供了插件的功能,虽然叫插件,但是实际上是通过拦截器( Interceptor )实现的。在 MyBatis 的插件模块中涉及责任链模式和 JDK 动态代理。
Mybatis 允许用户使用自定义拦截器对 SQL 语句执行过程中的某一点进行拦截。默认情况下, MyBatis 允许拦截器拦截 Executor 方法、 ParameterHandler 方法、 ResultSetHandler方法以及 StatementHandler 的方法。 具体可拦截方法如下
- Executor.update()方法、 query()方法、 flushStatements()方法、 commit()方法、 rollback()方法、 getTransaction()方法、 close()方法、 isClosed()方法。
- ParameterHandler 中的 etParameterObject()方法、 setParameters()方法。
- ResultSetHandler 中的 handleReultSets()方法、 handleOutputParameters()方法。
- StatementHandler 中的 prepare()方法、 parameterize()方法、 batch()方法、 update()方法、query()方法。
Mybtis 中使用的拦截器都需要实现 Interceptor 接口。 Interceptor 接口是 Mybtis 插件模块核心。
org.apache.ibatis.plugin.Interceptor接口源码如下:
/**
* @author Clinton Begin
*/
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
五、mybatis代码自动生成
1.MybatisPlus生成插件(推荐使用)
1.插件安装与配置
2.配置数据库和生成代码
选中要生成的表,并根据具体情况修改相关配置,最后点击右下角code generatro生成代码
生成后的效果图如下:
参考:
MybatisPlus 超好用的idea代码生成插件,及使用详解_我是有多懒的博客-CSDN博客_idea mybatisplus插件
2.EasyCode插件生成代码
1.安装EasyCode
2. IDEA中开始生成代码
生成的mapper文件,自动实现批量插入,批次插入或者更新,selectlist等sql语句
参考资料
六、实战总结
思考:
1. #{}和${}的区别是什么?
#{}是预编译处理、是占位符, ${}是字符串替换、是拼接符。
- Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;相当于JDBC中的PreparedStatement。
- Mybatis在处理${}时,就是把${}替换成变量的值。调用 Statement 来赋值
简单说,#{}是经过预编译的,是安全的;${}是未经过预编译的,仅仅是替换变量的值,是非安全的,存在SQL注入。
2. 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?
Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值.
接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。
Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,
举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。
在Mybatis中,每一个<select>、<insert>、<update>、<delete>标签,都会被解析为一个MappedStatement对象。
Dao接口工作原理:
Dao接口的底层实现是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。
是否可以重载:
Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略,寻找时与入参类型及个数没有多大关系。
3. Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
4. Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
1.有哪些映射形式?
第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。
第二种是使用sql列的别名功能,将列别名书写为对象属性名,比如T_NAME AS NAME,对象属性名一般是name,小写,但是列名不区分大小写,Mybatis会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成T_NAME AS NaMe,Mybatis一样可以正常工作。
1.如何映射?
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
6. 简述Mybatis的插件运行原理,以及如何编写一个插件
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,
Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。实现Mybatis的Interceptor接口并复写intercept()方法,
编写插件:
实现 Mybatis 的 Interceptor 接口并复写 intercept()方法, 然后在给插件编写注解, 指定
要拦截哪一个接口的哪些方法即可, 在配置文件中配置编写的插件。
然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,还需要在配置文件中配置你编写的插件。
7. 一级、二级缓存
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。要开启二级缓存,你需要在你的 SQL 映射文件中添加一行:<cache/>
3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
8. Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。
在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。默认为false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,
接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
9. Mybatis映射文件中,如果A标签通过include引用了B标签的内容,请问,B标签能否定义在A标签的后面,还是说必须定义在A标签的前面?
虽然Mybatis解析Xml映射文件是按照顺序解析的,但是,被引用的B标签依然可以定义在任何地方,Mybatis都可以正确识别。
原理是,Mybatis解析A标签,发现A标签引用了B标签,但是B标签尚未解析到,尚不存在,此时,Mybatis会将A标签标记为未解析状态,然后继续解析余下的标签,包含B标签,待所有标签解析完毕,Mybatis会重新解析那些被标记为未解析的标签,此时再解析A标签时,B标签已经存在,A标签也就可以正常解析完成了。
10. 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系?
Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。
在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。
<resultMap>标签会被解析为ResultMap对象,其每个子元素会被解析为ResultMapping对象。
每一个<select>、<insert>、<update>、<delete>标签均会被解析为MappedStatement对象,标签内的sql会被解析为BoundSql对象。
11.mybatis与hibernate区别?
1.mybatis支持很多的动态语句,编写sql更加灵活。但同时也是一个缺点就是需要自己手写很多sql。hibernate封装了很多的sql,适用于简单的业务操作
2.hibernate数据库移植性远大于mybatis。
3.hibernate有更好的二级缓存机制,可以使用第三方缓存。mybatis二级缓存机制不佳
12.mybatis的优缺点
优点:
1、基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签, 支持编写动态 SQL 语句, 并可重用。
2、与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;
3、很好的与各种数据库兼容( 因为 MyBatis 使用 JDBC 来连接数据库,所以只要JDBC 支持的数据库MyBatis 都支持)。
4、能够与 Spring 很好的集成;
5、提供映射标签, 支持对象与数据库的 ORM 字段关系映射; 提供对象关系映射标签, 支持对象关系组件维护。
缺点:
1、SQL 语句的编写工作量较大, 尤其当字段多、关联表多时, 对开发人员编写SQL 语句的功底有一定要求。
2、SQL 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库。
七、mybatis动态sql
1.批量操作
1. 批量更新
1)mysql
<update id="updateStoreTask" parameterType="Object">
<foreach collection="storeTaskList" item="item" index="index" separator=";">
update t_store_task set
store_sale_task=#{item.storeSaleTask}
where task_id=#{taskId} AND store_no=#{item.storeNo}
</foreach>
</update>
注意:
1.在配置时必须加上allowMultiQueries=true如:
url=“jdbc:mysql://localhost:3306/testDatabase?allowMultiQueries=true”
allowMultiQueries=true时,可以允许一次执行多条sql(通过分号分割)
2.最后编译的结果为
Preparing: update t_store_task set store_sale_task=? where task_id=? AND store_no=? ; update t_store_task set store_sale_task=? where task_id=? AND store_no=?
- oracle
<!--oracle-->
<update id="updateStoreTask" parameterType="Object">
<foreach collection="storeTaskList" item="item" index="index" open="begin" close=";end;" separator=";">
update t_store_task set
store_sale_task=#{item.storeSaleTask}
where task_id=#{taskId} AND store_no=#{item.storeNo}
</foreach>
</update>
@see https://www.cnblogs.com/feixian/p/5960111.html
2. 批量插入
1. mysql
<insert id="batchAdd" parameterType="Object">
insert into
t_store_task(task_id,store_no,store_name,store_sale_task)
values
<foreach collection="storeTaskList" item="item" index="index" separator=",">
(#{taskId},#{item.storeNo},#{item.storeName},#{item.storeSaleTask})
</foreach>
</insert>
最后编译的结果为
Preparing: insert into t_store_task(task_id,store_no,store_name,store_sale_task) values (?,?,?,?) , (?,?,?,?)
2.批量插入并返回主键
<insert id="batchAddReturnPK" parameterType="java.util.List" keyProperty="id" useGeneratedKeys="true">
insert into study_stage
(study_plan_id, stage_name, study_proposal,create_user_id, update_user_id, version)
values
<foreach collection="list" item="item" index="index" separator=",">
(
#{item.studyPlanId}, #{item.stageName}, #{item.studyProposal},#{item.createUserId}, #{item.updateUserId},#{item.version}
)
</foreach>
</insert>
对应的mapper为:
/**
* 批量插入并返回主键
* @param list
*/
void batchAddReturnPK(List<StudyStage> list);
注:
使用时 mapper中的 collection 参数必须为 list,即外部的mapper接口中只接受两种情况:
1、不Param注解,默认值list
2、使用param注解,但注解的value也必须为list
3.mybatis的版本必须在3.3.1以上。官方在这个版本中加入了批量新增返回主键id的功能
4.更新语句或者SaveOrUpdate语句都不支持返回主键,如果有插入或者更新的需求,建议分开进行,对性能也不会有多大影响。
参考文档:https://www.cnblogs.com/abel-he/p/mybatis.html
3.大数据量批量插入mybatis如何优化操作?
- SqlServer 对语句的条数和参数的数量都有限制,分别是 1000 和 2100。
- Mysql 对语句的长度有限制,默认是 4M。
- Mybatis 对动态语句没有数量上的限制。
1.使用labmbda表达式
思路:分批插入,并使用labmbda表达式skip和limit特性。
源码如下:
@Test
public void insert() {
List<TestVO> list = new ArrayList<>();
for (int i = 0; i < 200000; i++) {
list.add(new TestVO(i + "," + i, i + "," + i, i + "," + i, i + "," + i, i + "," + i));
}
int index = list.size() / 10000;
for (int i=0;i<= index;i++){
//stream流表达式,skip表示跳过前i*10000条记录,limit表示读取当前流的前10000条记录
testMapper.testBatchInsert(list.stream().skip(i*10000).limit(10000).collect(Collectors.toList()));
}
}
2.需要自己把集合拆分成较小的集合,可以用guava的List.partition()。
List<List<AuthAppOrg>> partition = Lists.partition(list, 1000);
partition.parallelStream().forEach(item -> {
List<String> ids = item.stream().map(IdEntity::getId).collect(Collectors.toList());
authAppOrgMapper.deleteBatchIds(ids);
//super.removeByIds(ids);
});
3. 批量筛选
in条可以实现通过id批量查询,批量删除,批量更新
动态 SQL 的另外一个常用的必要操作是需要对一个集合进行遍历,通常是在构建 IN 条件语句的时候。
<if test="categoryList != null and categoryList.size()!=0">
and produt_pl IN
<foreach collection="categoryList" item="item" index="index" open="(" close=")" separator=",">
#{item.categoryNo}
</foreach>
</if>
编译结果:
and produt_pl IN ( ? , ? )
注意:如果不加categoryList.size()!=0,当list不为null且size为0会报错,需要在在业务中提前判断categoryList.size()是否为0
如果是数组则用length,如:
<if test="driverNos != null and driverNos.length>0">
and tt.driver_no in
<foreach collection="driverNos" item="item" index="index" open="(" close=")" separator=",">
#{item}
</foreach>
</if>
2.插入不成功执行更新
1.oracle
<!--插入不成功,则执行更新-->
<insert id="batchSaveOrUpdate" parameterType="Object">
MERGE INTO
t_store_task T1
USING (
<foreach collection="storeTaskList" item="item" index="index" separator="union">
SELECT #{taskId} task_id,
#{item.storeNo} store_no,
#{item.storeName} store_name,
<choose>
<when test="item.storeSaleTask!=null">
#{item.storeSaleTask} store_sale_task
</when>
<otherwise>
0 store_sale_task
</otherwise>
</choose>
FROM DUAL
</foreach>
) T
ON (T1.task_id = T.task_id AND T1.store_no = T.store_no)
WHEN MATCHED THEN
UPDATE SET T1.store_sale_task=T.store_sale_task
WHEN NOT MATCHED THEN
INSERT
(task_id,store_no,store_name,store_sale_task)
VALUES
(T.task_id,T.store_no,T.store_name,T.store_sale_task)
</insert>
@see http://blog.csdn.net/gjldwz/article/details/17414155
2.mysql
<insert id="bachSaveOrUpdate" parameterType="Object">
insert into
t_goods_info(id,goods_no,category_no,category_name,brand,goods_name,scutcheon_price,transaction_price,promotion_cost,brokerage,promotion_level,promotion_time,selling_points,remark,is_img,is_video,status,create_time)
values
<foreach collection="goodsInfoList" item="item" index="index" separator=",">
(#{item.id},#{item.goodsNo},#{item.categoryNo},#{item.categoryName},#{item.brand},#{item.goodsName},#{item.scutcheonPrice},#{item.transactionPrice},#{item.promotionCost},#{item.brokerage},#{item.promotionLevel},#{item.promotionTime},#{item.sellingPoints},#{item.remark},#{item.isImg},#{item.isVideo},#{item.status},now())
</foreach>
ON DUPLICATE KEY UPDATE
category_name = VALUES(category_name),
brand = VALUES(brand),
goods_name = VALUES(goods_name),
scutcheon_price = VALUES(scutcheon_price),
transaction_price = VALUES(transaction_price),
promotion_cost = VALUES(promotion_cost),
brokerage = VALUES(brokerage),
promotion_level = VALUES(promotion_level),
promotion_time = VALUES(promotion_time),
selling_points = VALUES(selling_points),
remark = VALUES(remark)
</insert>
@see 批量插入记录,遇到重复记录则为自动更新https://blog.csdn.net/rj042/article/details/50560220
非批量
<insert id="saveOrUpdate" parameterType="object">
insert into student_sigup
(`student_id`,`category_id`,`year`,`sigup_type`,`province_id`,`operator_id`)
values(#{studentId}, #{categoryId}, #{year},#{sigupType}, #{provinceId},#{operatorId})
ON DUPLICATE KEY UPDATE
<if test="sigupType != null">
sigup_type=VALUES(sigup_type),
</if>
<if test="provinceId != null">
province_id=VALUES(province_id),
</if>
update_date=now()
</insert>
3.《if test=“”》标签
1.《if test=“”》标签为0的坑
现象:
<if test="orderStatus != null and orderStatus !=''"> and t.is_ship=#{orderStatus} </if>
当状态值设置为0时,操作完了,数据库没反应,没有设置为0
把状态用1和2表示,不使用0,一切正常,问题消失了。
原理:
MyBatis的表达式是用OGNL处理的。OGNL表达式的规则如下
根据这一条If the object is a Number, its double-precision floating-point value is compared with zero; non-zero is treated as true, zero as false
所以:state!=' ' 传入0时,表达式的值为false;所以不执行。
解决办法,把这个删掉就可以了。
<if test="state != null">state = #{state }</if>
2.《if test=“”》标签为字符的坑
最近在项目使用mybatis中碰到个问题
当传入的type的值为y的时候,if判断内的sql也不会执行
mybatis是使用的OGNL表达式来进行解析的,在OGNL的表达式中,'y'会被解析成字符,因为java是强类型的,char 和 一个string 会导致不等。所以if标签中的sql不会被解析。具体的请参照 OGNL 表达式的语法。
到这里,上面的问题终于解决了,只需要把代码修改成:
4.《<choose》《/when》标签
choose标签是按顺序判断其内部when标签中的test条件出否成立,如果有一个成立,则 choose 结束。当 choose 中所有 when 的条件都不满则时,则执行 otherwise 中的sql。类似于Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。
判断相等条件demo:
<if test="surveyStatus != null">
<choose>
<when test="surveyStatus==1">
<![CDATA[
AND now() < start_date
]]>
</when>
<when test="surveyStatus==3">
<![CDATA[
AND now() > stop_date
]]>
</when>
<when test="surveyStatus==2">
<![CDATA[
AND now() >= start_date and now() <= stop_date
]]>
</when>
</choose>
</if>
而如果使用if-test标签则 不会跳出判断语句
5.Mybatis 处于大于,小于等特殊符号
<![CDATA[
AND t2.shift_start_time <= #{shiftStartTime}
AND t2.shift_end_time >= #{shiftEndTime})
]]>
6.Mybatis 模糊匹配
<if test="searchConditon != null and searchConditon != '' ">
AND `position` LIKE CONCAT(CONCAT('%', #{searchConditon}), '%')
</if>
7.Mybatis 一对多映射
实体:
public class Task {
private static final long serialVersionUID = 1L;
private Integer id;// 主键
private String branchNo;// 分部代码
private String branchName;// 分部名称
private String taskNo;// 任务代码
private String taskName;// 任务名称
private Double branchTotal;// 任务总计
private String taskType;// 任务类型;阶段性,月度
private String startDate;// 开始日期
private String endDate;// 结束日期
private List<StoreTask> storeTaskList;
public List<StoreTask> getStoreTaskList() {
return storeTaskList;
}
public void setStoreTaskList(List<StoreTask> storeTaskList) {
this.storeTaskList = storeTaskList;
}
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
public String getBranchNo() {
return this.branchNo;
}
public void setBranchNo(String branchNo) {
this.branchNo = branchNo;
}
public String getBranchName() {
return this.branchName;
}
public void setBranchName(String branchName) {
this.branchName = branchName;
}
public String getTaskNo() {
return this.taskNo;
}
public void setTaskNo(String taskNo) {
this.taskNo = taskNo;
}
public String getTaskName() {
return this.taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
public Double getBranchTotal() {
return this.branchTotal;
}
public void setBranchTotal(Double branchTotal) {
this.branchTotal = branchTotal;
}
public String getTaskType() {
return this.taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
public String getStartDate() {
return this.startDate;
}
public void setStartDate(String startDate) {
this.startDate = startDate;
}
public String getEndDate() {
return this.endDate;
}
public void setEndDate(String endDate) {
this.endDate = endDate;
}
maper.xml
<resultMap id="taskStoreResultMap" type="entity.Task">
<result column="branch_no" property="branchNo" />
<result column="branch_name" property="branchName" />
<result column="task_no" property="taskNo" />
<result column="task_name" property="taskName" />
<result column="branch_total" property="branchTotal" />
<collection property="storeTaskList" ofType="entity.StoreTask">
<result property="storeNo" column="store_no"/>
<result property="storeName" column="store_name"/>
<result property="storeSaleTask" column="store_sale_task"/>
</collection>
</resultMap>
<select id="queryByBranchNoAndTaskNo" resultMap="taskStoreResultMap"
parameterType="Object">
SELECT
t.branch_no,
t.branch_name,
t.task_no,
t.task_name,
t.branch_total,
st.store_no,
st.store_name,
st.store_sale_task
FROM
`t_task` t
LEFT JOIN t_store_task st ON t.id = st.task_id
WHERE
branch_no = 'HN01'
AND task_no = '2';
</select>
参考资料
1.MyBatis框架的学习(二)——MyBatis架构与入门MyBatis快速入门第二讲——MyBatis的快速入门_李阿昀的博客-CSDN博客
2.Mybatis源码分析(一)- Configuration配置文件详解 https://blog.csdn.net/einarzhang/article/details/53022820
3.MyBatis 源码分析系列 MyBatis 源码分析 - 随笔分类 - Aomi - 博客园
4.书籍《mybatis技术内幕》徐郡明
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)