MyBatis基础
原理
使用端
- 引入框架依赖
- 提供两部分配置信息:数据库配置信息 和 sql 配置信息
- 数据库配置信息:mybatis-config.xml。不仅仅是存放数据库配置信息,还指定了 sql 配置信息文件路径
- sql 配置信息:mapper.xml(对应 dao 层的 xml)
框架底层
准备工作(项目启动时完成)
- 第一步:读取配置信息(因为 mybatis-config.xml 还指定了 mapper.xml 路径,所以一次读取所有的配置文件)得到一个输入流
- 第二步:创建 Configuration
- 包含所有 mybatis 的配置,比如数据库,sql配置等,还会维护 mppedStatements,这是个 map,key:mapper全限定名+方法名(比如 com.study.UserMapper.selectById),value:MppedStatement
- 第三步:创建 SqlSession
- 根据上一步得到的 Configuration 创建,方法:
SqlSessionFactory build(Configuration config)
- 根据上一步得到的 Configuration 创建,方法:
- 第四步:为 mapper 生成代理对象并保存
- 通过 JDK 代理生成 mapper 接口的对象,保存在 MapperRegistry.knownMappers 中(mybatis-config.xml 中通过 mappers 标签指定所有 mapper.xml,每个 mapper.xml 有 namespace 属性指定 mapper 接口全限定名)
执行 mapper 方法
- 第一步:根据 SqlSession 获取 Mapper 接口代理对象
- 调用 SqlSession.getMapper(Mapper.class)。其实获取的就是 MapperRegistry.knownMappers 中的对象,没获取到会抛出异常
- 第二步:执行 mapper 方法
- 执行的方法分为两种情况,一个是 Object 类的方法就直接执行,比如 toString、equals 等
- 如果是不是 Objcet 类的方法,会继续往下走
- 第三步:主要是代理对象的方法和 MppedStatement 关系
- 方法名和 mapper 接口全限定名 是 MppedStatement 的 key,而 MppedStatement 里面有代理对象方法 MapperMethod ,这个方法里面有 sql 信息和 返回信息
源码
- 创建 SqlSessionFactory:SqlSessionFactoryBuilder.build(InputStream inputStream)
// 方法1,使用配置文件的流创建创建 SqlSessionFactory public SqlSessionFactory build(InputStream inputStream) { // 调用方法 2 return build(inputStream, null, null); } // 方法2 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 解析配置文件 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 调用方法3。parser.parse() 返回 Configuration(mybatis-config.xml里面的每个标签和值设置到 Configuration 对象) 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. } } } // 方法3,最后是这里返回 SqlSessionFactory(如果是springboot,跳过方法1和方法2,直接调用这里,原因也很简单,因为 springboot 压根就没有 mybatis 的配置文件嘛) public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
- 创建 SqlSession 流程:DefaultSqlSessionFactory.openSession()
// 自动提交 public SqlSession openSession() { // 参数就是解析配置文件后得到的 Configuration 对象里的 defaultExecutorType 属性 return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } // 不会自动提交 public SqlSession openSession(boolean autoCommit) { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit); } /** + ExecutorType 指定Executor的类型,分为三种:SIMPLE, REUSE, BATCH,默认使用的是SIMPLE + TransactionIsolationLevel 指定事务隔离级别,使用null,则表示使用数据库默认的事务隔离界别 + autoCommit 是否自动提交,传过来的参数为false,表示不自动提交 */ 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); // 创建Executor,即执行器。它是真正用来Java和数据库交互操作的类 final Executor executor = configuration.newExecutor(tx, execType); // 创建 DefaultSqlSession 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(); } }
- 创建执行器:Configuration.newExecutor(Transaction transaction, ExecutorType executorType)
// 创建执行器(这个方法属于 Configuration 类,所以这个类里面能拿到所有的配置信息,包括缓存、驼峰、事务隔离等等) public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { // ExecutorType 默认是 SIMPLE,所以默认的执行器是 SimpleExecutor executor = new SimpleExecutor(this, transaction); } // 是否开启缓存(二级缓存) if (cacheEnabled) { // 如果开启了,使用装饰器模式添加二级缓存功能 executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
应用
基于 sqlSession
// 常用方法
<T> T selectOne(String statement, Object parameter)
<E> List<E> selectList(String statement, Object parameter)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)
void commit()
void rollback()
示例
//加载核⼼配置⽂件
InputStream resourceAsStream = Resources.getResourceAsStream("myabtis-config.xml");
//获得sqlSession⼯⼚对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//获得sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//sqlSession 的第一个参数是 MppedStatement ID 即 接口全限定名+方法名
List<User> userList = sqlSession.selectList("com.study.mapper.userMapper.findAll");
//打印结果
System.out.println(userList);
//释放资源
sqlSession.close();
基于 mapper
通过实现类实现
// mapper 接口
public interface UserDao {
List<User> findAll() throws IOException;
}
// mapper 实现类
public class UserDaoImpl implements UserDao {
public List<User> findAll() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("myabtis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> userList = sqlSession.selectList("userMapper.findAll");
sqlSession.close();
return userList;
}
}
// 测试
@Test
public void testTraditionDao() throws IOException {
UserDao userDao = new UserDaoImpl();
List<User> all = userDao.findAll();
System.out.println(all);
}
基于代理(主流方式)
- Mapper.xml ⽂件中的 namespace 与 mapper 接⼝的全限定名相同
- Mapper 接⼝⽅法名和 Mapper.xml 中定义的每个 statement 的 id 相同
- Mapper 接⼝⽅法的输⼊参数类型和 mapper.xml 中定义的每个sql的parameterType的类型相同
- Mapper 接⼝⽅法的输出参数类型和 mapper.xml 中定义的每个 sql 的 resultType 的类型相同
// mapper 接口
public interface UserDao {
List<User> findById(int id) throws IOException;
}
<!-- mapper.xml -->
<mapper namespace="com.stydu.mapper.UserMapper">
<select id="findById" parameterType="int" resoult="com.stydu.entity.User">
SELECT * FROM T_USER WHERE ID = #{id}
</select>
</mapper>
// 测试
@Test
public void testProxyDao() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获得MyBatis框架⽣成的 UserMapper 接⼝的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.findById(1);
System.out.println(user);
sqlSession.close();
}
动态 sql
where 条件
有的时候 where 1=1 会影像性能,所以最好使用
<select id="findByCondition" parameterType="user" resultType="user">
select * from User
<where>
<if test="id!=0">
and id=#{id}
</if>
<if test="username!=null">
and username=#{username}
</if>
</where>
</select>
循环
… … …
// 获得 MyBatis 框架⽣成的 UserMapper 接⼝的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
int[] ids = new int[]{2,5};
List<User> userList = userMapper.findByIdsAndName(ids, "Milk");
System.out.println(userList);
… … …
<select id="findByIds" resultType="user">
select * from User
<where>
<!-- collection 的值和 mapper 接口指定的参数名相同 -->
<foreach collection="ids" open="id in(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
<if test="name != '' and name != null">
and name = #{name}
</if>
</select>
片段抽取
<!--抽取sql⽚段简化编写-->
<sql id="selectUser"> select * from User</sql>
<select id="findById" parameterType="int" resultType="user">
<include refid="selectUser" /> where id=#{id}
</select>
级联查询
一对一
一个订单对应一个用户
// 订单
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单从属于哪⼀个客户
private User user;
}
}
// 用户
public class User {
private int id;
private String username;
private String password;
private Date birthday;
}
<mapper namespace="com.lagou.mapper.OrderMapper">
<resultMap id="orderMap" type="com.lagou.domain.Order">
<!-- 主表,订单表 -->
<result property="id" column="id"></result>
<result property="ordertime" column="ordertime"></result>
<result property="total" column="total"></result>
<!-- 从表,用户表 -->
<association property="user" javaType="com.lagou.domain.User">
<!-- 如果两个表的主键都叫 ID,要指定别名 -->
<result column="uid" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
</association>
</resultMap>
<select id="findAll" resultMap="orderMap">
<!-- select * from t_order o, t_user u where o.uid=u.id -->
<!-- 上面使用交叉连接,没有处理别名,我比较喜欢左连接 -->
select o.*,u.id as uid, u.username, u.password, u.birthday from t_order o left join t_user u on o.uid = u.id
</select>
</mapper>
一对多
// 订单表
public class Order {
private int id;
private Date ordertime;
private double total;
}
// 用户表
public class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前⽤户具备哪些订单
private List<Order> orderList;
}
<mapper namespace="com.lagou.mapper.UserMapper">
<!-- 主表,用户 -->
<resultMap id="userMap" type="com.lagou.domain.User">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<!-- 从表,订单 -->
<collection property="orderList" ofType="com.lagou.domain.Order">
<result column="oid" property="id"></result>
<result column="ordertime" property="ordertime"></result>
<result column="total" property="total"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
select u.*,o.id as oid, o.ordertime, o.total from t_user left join t_order on u.id = o.uid
</select>
</mapper>
多对多
同一对多
缓存
一级缓存
是 SqlSession 级别的,对于查询,先查询缓存,如果缓存未查到就查数据库
默认开启,如果需要关闭,有两种方式,一种是单独指定某个查询方法,第二种是全局设定
增、删、改会清空缓存
<!-- flushCache = true,关闭一级缓存,当前方法生效 -->
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap" flushCache="true">
select
<include refid="Base_Column_List" />
from cbondissuer
where OBJECT_ID = #{objectId,jdbcType=VARCHAR}
</select>
<!-- 默认是SESSION,也就是开启一级缓存 -->
<setting name="localCacheScope" value="STATEMENT"/>
- Executor 里维护了一个 map,具体是 BaseExecutor.localCache
- Exexutor.query 时会先查询 localCache,没有再查数据库
二级缓存
1,是 mapper.xml namespace 级别的,多个 SqlSession 都能查询到某个 mapper.xml 2,namespace 的缓存
3,当查询时:二级缓存 -> 一级缓存 -> 数据库
4,默认关闭,需手动开启,在 mybatis-config.xml 和 mapper.xml 中都要配置
5,增、删、改会默认清空缓存,查询默认使用缓存(可以配置,useCache 和 flushCachs)
6,如果开启二级缓存,所有的数据库实体必须实现 Serializable 接口
<!-- mybatis-config.xml 中配置打开二级缓存 -->
<!--开启⼆级缓存,父节点是 settings 标签 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- mapper.xml 中也需要配置 -->
<!-- 都是用默认配置的话,简单写个 cache 标签即可,父节点是 mapper 标签(和 select result 同级) -->
<cache/>
- select 标签
- flushCache 默认为 false,表示任何时候语句被调用,都不会去清空本地缓存和二级缓存
- useCache 默认为 true,表示会将本条语句的结果进行二级缓存
- insert、update、delete 标签
- flushCache 默认为 true,表示任何时候语句被调用,都会导致本地缓存和二级缓存被清空
- useCache 属性在该情况下没有