这一遍看 Mybatis 的原因是怀念一下去年的 10 月 24 号我写自己第一个项目时使用全配置文件版本的 MyBatis, 那时我们三个人刚刚大二, 说实话, 当时还是觉得 MyBatis 挺难玩的, 但是今年再看最新版的 Mybatis3.5.3, 还是挺有感觉的 Mybatis 的官网一级棒...
Mybatis 的核心组件及其生命周期
SqlSessionFactoryBuider:
作用: 构建器, 根据配置信息生成 SqlSessionFactory
生命周期: 这个类可以被实例化, 使用和丢弃, 一旦创建了 SqlSessionFactory, 就不再需要它了. 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量). 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例, 但是最好还是不要让其一直存在, 以保证所有的 xml 解析资源可以被释放给更重要的事情.
SqlSessionFactory
作用: 生成 sqlSession
生命周期: SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在, 没有任何理由丢弃它或重新创建另一个实例. 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次, 多次重建 SqlSessionFactory 被视为一种代码 "bad smell". 因此 SqlSessionFactory 的最佳作用域是应用作用域. 有很多方法可以做到, 最简单的就是使用单例模式或者静态单例模式
SqlSession
作用: 它表示一次 sql 的会话, 即可以去执行 sql 返回结果, 也可以获取为 mapper 生成的代理对象 , 支持事物, 通过 commit,rollback 方法提交或者回滚事物
生命周期: 每个线程都应该有它自己的 SqlSession 实例. SqlSession 的实例不是线程安全的, 因此是不能被共享的, 所以它的最佳的作用域是请求或方法作用域. 绝对不能将 SqlSession 实例的引用放在一个类的静态域, 甚至一个类的实例变量也不行. 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中, 比如 Servlet 框架中的 HttpSession. 如果你现在正在使用一种 Web 框架, 要考虑 SqlSession 放在一个和 HTTP 请求对象相似的作用域中. 换句话说, 每次收到的 HTTP 请求, 就可以打开一个 SqlSession, 返回一个响应, 就关闭它. 这个关闭操作是很重要的, 你应该把这个关闭操作放到 finally 块中以确保每次都能执行关闭. 下面的示例就是一个确保 SqlSession 关闭的标准模式:
SqlMapper
作用: : MyBatis 的映射器, 现在大多数都使用 java 接口, 早前使用配置文件来描述 sql 查询结果和 java 对象之间的映射规则 定义参数类型, 描述缓存, 描述 SQL 语句 , 定义查询结果和 POJO 的映射关系
生命周期: 最好把映射器放在方法作用域内
基于 xml 版本的环境搭建测试
基于 xml 版本, 搭建 mybatis 开发环境中, 存在一个主配置文件, 和多个子配置文件, 主配置文件中配置数据库相关的信息, 而子配置文件中配置的是单个 Dao 接口层中的抽象方法对应的 sql 语句
点击看官方文档 xml 全套配置信息
主配置文件如下
需要注意的地方, 下面的 < mapper > 标签中 resource 属性存放的是从配置文件的路径, 但是从配置文件的目录信息得和 src 中相应的接口位于相同的目录
<?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">
- <!--mybatis 的主配置文件 -->
- <configuration>
- <!-- 配置环境 -->
- <environments default="mysql">
- <!-- 配置 mysql 的环境 -->
- <environment id="mysql">
- <!-- 配置事务的类型 -->
- <transactionManager type="JDBC"/>
- <!-- 配置数据源 -->
- <!--dataSource 存在三个, 其中的 POOLED 池化的连接池 -->
- <dataSource type="POOLED">
- <property name="driver" value="com.mysql.jdbc.Driver"/>
- <property name="url" value="jdbc:mysql://localhost:3306/trymybatis"/>
- <property name="username" value="root"/>
- <property name="password" value="root"/>
- </dataSource>
- </environment>
- </environments>
- <!-- 指定映射配置文件的位置, 也就是针对每个 Dao 的配置文件的位置 -->
- <!-- 下面指定的 xml 配置文件的路径, 需要和 src 下 IUserDao 接口的目录保持一致 -->
- <mappers>
- <mapper resource="com/changwu/dao/IUserDao.xml"/>
- </mappers>
- </configuration>
从配置文件
需要注意的地方: 命名空间是全类名, id 是方法名, 返回值是全类名
还有一点就是, 单个 mapper 标签中, namespace 和 id 都不能少, 两者合在一起才能确定一个全局唯一的方法, 至于为什么我们配置一个接口就 ok, 而不用添加配置文件, 那是因为 Mybatis 会使用代理技术, 为接口生成代理对象, 让程序员使用代理对象
<?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">
- <!--namespace 是全类名 -->
- <mapper namespace="com.changwu.dao.IUserDao">
- <!--
- id 为方法名
- resultType 为返回值个体的封装类
- -->
- <select id="findAll" resultType="com.changwu.pojo.User">
- select * from user
- </select>
- </mapper>
其他数据源的配置方式
把如下的配置放在 resourse 目录下面
- jdbc.driver=com.MySQL.jdbc.Driver
- jdbc.url=jdbc:MySQL://localhost:3306/trymybatis
- jdbc.username=root
- jdbc.password=root
然后将主配置文件改成下面这样
当然也可以在 < properties > 标签中使用 url 但是需要遵循 url 协议的规范
<?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">
- <!--mybatis 的主配置文件 -->
- <configuration>
- <properties resource="jdbcConfig.properties"> </properties>
- <!-- 配置环境 -->
- <environments default="mysql">
- <!-- 配置 mysql 的环境 -->
- <environment id="mysql">
- <!-- 配置事务的类型 -->
- <transactionManager type="JDBC"/>
- <!-- 配置数据源 -->
- <!--dataSource 存在三个, 其中的 POOLED 池化的连接池 -->
- <dataSource type="POOLED">
- <property name="driver" value="${jdbc.driver}"/>
- <property name="url" value="${jdbc.url}"/>
- <property name="username" value="${jdbc.username}"/>
- <property name="password" value="${jdbc.password}"/>
- </dataSource>
- </environment>
- </environments>
- <!-- 指定映射配置文件的位置, 也就是针对每个 Dao 的配置文件的位置 -->
- <!-- 下面指定的 xml 配置文件的路径, 需要和 src 下 IUserDao 接口的目录保持一致 -->
- <mappers>
- <mapper class="com.changwu.dao.IUserDao"/>
- </mappers>
- </configuration>
编码测试类
- @Test
- public void text01() {
- try {
- // 1. 读取配置文件
- InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
- // 2. 创建 SqlSessionFactory 工厂
- SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
- // 3. 创建 sqlSession
- SqlSession sqlSession = factory.openSession();
- // 4. SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法
- // 使用正确描述每个语句的参数和返回值的接口(比如 BlogMapper.class),
- // 你现在不仅可以执行更清晰和类型安全的代码, 而且还不用担心易错的字符串字面值以及强制类型转换
- IUserDao userDao = sqlSession.getMapper(IUserDao.class);
- // 5. 执行方法
- List<User> all = userDao.findAll();
- for (User user : all) {
- System.out.println(user.getUsername());
- }
- // 6, 释放资源
- sqlSession.close();
- resourceAsStream.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
基于注解版本的环境搭建测试
因为现在依然是 MyBatis 孤军深入, 没有和 Spring,Springboot 等框架进行整合, 因此上面说的那个主配置文件无论如果都不能缺失, 不像 SpringBoot 那样一个 @MapperScan("XXX")就完成扫描整合这么给力
基于注解的开发模式, 我们可以轻易进一步去除单个 dao 层的接口对应的 xml 配置文件, 取代之的是注解, 三步:
第一步: 删除原来的子配置文件的目录
第二步: 在 dao 层接口使用注解开发
- @Select("select * from user")
- List<User> findAll();
第三步: 修改部分主配置文件
- <mappers>
- <mapper class="com.changwu.dao.IUserDao"/>
- </mappers>
常用的注解
- @Results
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.METHOD)
- public @interface Results {
- // 因为当前的 Results 注解中存在实例的描述, 使用 id 标识当前的 map, 实现给 @resultMap 的复用
- String id() default "";
- Result[] value() default {};
- }
- @Result
继续看看这个 @Result 注解, 如下: 这个注解拥有 xml 中的 resultMap 中大部分的属性
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target({})
- public @interface Result {
- boolean id() default false;
- // 表中的列名
- String column() default "";
- // java 实体类中的属性名
- String property() default "";
- // 实体类型
- Class<?> javaType() default void.class;
- JdbcType jdbcType() default JdbcType.UNDEFINED;
- Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;
- // 实体之间的关系为 1 对 1
- One one() default @One;
- // 实体之间的关系为 1 对多
- Many many() default @Many;
- }
- @One
跟进 @One 注解, 他是对 select 属性的封装, FetchType 是一个枚举, 三种值, 分别是 LAZY, EAGER, DEFAULT
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target({})
- public @interface One {
- String select() default "";
- FetchType fetchType() default FetchType.DEFAULT;
- }
- @Many
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target({})
- public @interface Many {
- String select() default "";
- FetchType fetchType() default FetchType.DEFAULT;
- }
- @ResultMap
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.METHOD)
- public @interface ResultMap {
- String[] value();
- }
类型别名
原来在 xml 版本配置 mapper 时, 会使用 parameterType 属性指定程序员提供的类的全类名, 但是这个全类名真的太长了, 于是 MyBatis 官方提供了给全类名取别名的标签, 在 Mybatis 的主配置文件中添加如下的配置, 如下:
- <typeAliases>
- <typeAlias alias="user" type="com.changwu.pojo.User"/>
- </typeAliases>
添加了全类名的配置之后, 我们的在 mapper 中就可以使用别名了, 如下: 并且在 Windows 系统下不区分大小写
- <update id="updateUser" parameterType="UsEr">
- update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id = #{id}"
- </update>
但是像上面这样, 为每一个 POJO 都添加上别名的配置, 确实显得有点麻烦, 于是可以像下面这样, 为一整个包下面的类名配置别名, 别名就是类名不区分大小写的格式
- <typeAliases>
- <package alias="user" type="com.changwu.pojo"/>
- </typeAliases>
公共 sql 的抽取
一处抽取, 多处使用
- <sql id="findUsers">
- select * form user;
- </sql>
- <select id="findAll" resultType="com.changwu.pojo.User">
- <include refid="findUsers"></include>
- </select>
优先级
MyBatis 支持的 3 种配置方式, 编码 > properties 配置文件 > property 元素标签, 优先级如下:
在 properties 配置文件中指定的配置文件的属性首先被读取
根据配置文件中的 resources 属性读取类路径下的属性文件, 或者根据 url 属性读取属性文件并会覆盖同名属性
读取作为方法参数传递的属性, 并覆盖已经读取的同名属性
不要混合使用 xml 版和注解版两种开发模式, 否则 Mybatis 启动不了
MyBytais 中的参数类型的封装
在使用注解版做开发时, 我们会在每个 mapper 中标记好入参的类型
简单类型
MyBattis 的参数传递是支持简单类型的, 比如下面这种
- <delete id="deleteUserById" parameterType="java.lang.Integer">
- delete from user where id = #{id}
- </delete>
传递 pojo 对象
看下面的代码和配置, 在编写 sql 时, 我们直接指定参数的类型的 Pojo 对象
- @Update({
- "update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id = #{id}"
- })
- void updateUser(User user);
还有这种配置
- <update id="updateUser" parameterType="com.changwu.pojo.User">
- update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id = #{id}"
- </update>
那么, myBatis 如何解析我们传递的 pojo 对象呢? 答案是使用 ojnl(Object graphic navigation Language) 对象图导航语言, 实际是底层是通过对象的方法来获取数据, 但是在写法上却省略了 getXXX
比如: 我们想获取 username, 按理说这样写 user.getUserName() 但是在 ojnl 表达式来说表达成 user.username, 于是我们就可以在 sql 中直接写上 pojo 中字段的属性名, MyBatis 会自动完成从对象中, 取值解析
注意点就是说, 得 sql 中属性的顺序和 pojo 中属性的生命顺序保持一致, 否则存进去的就是乱序的数值
传递 pojo 包装后的对象
开发中可能会有各种各样的查询条件, 其中, 很多时候用来查询的条件不是简单的数据类型, 而是一类对象, 举个例子: 如下
根据另一个封装类去查询用户列表, 其中的 QueryVo 并不是持久化在数据库中的对象, 而是某几个字段封装类, 于是我们像下面这样传递值
- @Select("select * from user where username = #{user.username}")
- List<User> findUserByQueryVo(QueryVo vo);
xml 版本
- <select id="findByQueryVo" paramterType="com.changwu.vo.QueryVo" resultType="com.changwo.pojo.User">
- select * from user where username like #{user.username}
- </select>
注意点: 传递 pojo 的包装类是有限制的, 在下面取值时, 强制程序员不能把名字写错
user == vo 对象中的属性名 user
username == vo 对象中的属性 user 中的属性名 username
MyBytais 中的结果类型的封装
基于 xml 的 resultMap
前面的实验中, 我们的 pojo 字段名和数据表中列表保持百分百一致, 所以我们在 resultType 标签中使用 com.changwu.pojo.User 接收返回的数据时才没出任何差错, 但是一般在现实的开发中, 同时使用数据库的列名的命名风格和 java 的驼峰命名法, 然而, 当我们的 pojo 的属性名个 sql 中的列表不一致时, Mybatis 是不能完成两者的赋值的
解决方法 1: 取别名
注解版本的, 默认支持驼峰命名法, 意思和忽略大小写擦不多, 但是如果两者名字忽略大小写之后还不一样就真的得配置取别名了
- @Select("select * from user")
- @Select("select id as userId from user")
- <select id="findAll" resultType="com.changwu.pojo.User">
- select id as userId from user;
- </select>
解决方法 2: 使用配置 resultMap 如下:
id 为当前 resultMap 的唯一身份标识
type 表示查询的实体类是哪个实体类
property 为 java 中的实体类属性名
column 为数据库中列名
在 select 标签中去除掉原来的 resultType, 取而代之的是 resultMap
- <resultMap id="userMap" type="com.changwu.pojo.User">
- <id property="userId" column="id"></id>
- <result property="userName" column="username"></result>
- </resultMap>
- <select id="findAll" resultMap="userMap">
- select id as userId from user;
- </select>
解决方法 3: 开启驼峰命名配置
<?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="mapUnderscoreToCamelCase" value="true"/>
- <setting name="useGeneratedKeys" value="true"/>
- </settings>
- </configuration>
但是如果两个字段的差异已经不是驼峰命名法可以解决的了, 就只能去配置别名了
基于注解实现 resultMap
当实体类中的属性和表中的字段命名出现严重不一致时, 我们使用通过注解解决此类问题
同样 property 中是 java 对象中的属性, column 为表中的列名
通过 @Results 中的 id 属性值, 使其他方法可以通过 @ResultMap 复用已经存在的映射关系
- @Select("select * from user")
- @Results(id = "userMap",value = {
- @Result(id = true,property = "",column =""),
- @Result(id = true,property = "",column =""),
- @Result(id = true,property = "",column =""),
- @Result(id = true,property = "",column =""),
- })
- List<User> findAll();
- @Select("select * from user where id = #{id}")
- @ResultMap(value = {"userMap"})
- User findById(Integer id);
MyBatis 的数据连接池
如何配置
在如下 Mybatis 主配置文件中的 < dataSource type="POOLED">
<?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">
- <!--mybatis 的主配置文件 -->
- <configuration>
- <properties resource="jdbcConfig.properties"> </properties>
- <!-- 配置环境 -->
- <environments default="mysql">
- <!-- 配置 mysql 的环境 -->
- <environment id="mysql">
- <!-- 配置事务的类型 -->
- <transactionManager type="JDBC"/>
- <!-- 配置数据源 -->
- <!--dataSource 存在三个, 其中的 POOLED 池化的连接池 -->
- <dataSource type="POOLED">
- <property name="driver" value="${jdbc.driver}"/>
- <property name="url" value="${jdbc.url}"/>
- <property name="username" value="${jdbc.username}"/>
- <property name="password" value="${jdbc.password}"/>
- </dataSource>
- </environment>
- </environments>
- <!-- 指定映射配置文件的位置, 也就是针对每个 Dao 的配置文件的位置 -->
- <!-- 下面指定的 xml 配置文件的路径, 需要和 src 下 IUserDao 接口的目录保持一致 -->
- <mappers>
- <mapper class="com.changwu.dao.IUserDao"/>
- </mappers>
- </configuration>
- POOLED
采用传统的 javax.sql.DataSource 规范中的连接池, 这种数据源的实现利用 "池" 的概念将 JDBC 连接对象组织起来, 避免了创建新的连接实例时所必需的初始化和认证时间. 这是一种使得并发 Web 应用快速响应请求的流行处理方式.
特点: 使用完了的连接会被回收, 而不是被销毁
其他相应的属性
poolMaximumActiveConnections : 任意时间正在使用的连接数量, 默认为 10
poolMaximumIdleConnections : 任意事件可能存在的空闲连接数
poolMaximumCheckoutTime : 在被强制返回之前, 池中连接被检出 (checked out) 时间, 默认值: 20000 毫秒(即 20 秒)
poolTimeToWait: 默认是 20 秒, 如果花费了 20 秒还没有获取到连接, 就打印日志然后重新尝试获取
poolMaximumLocalBadConnectionTolerance : 就是如果当前的线程从连接池中获取到了一个坏的连接, 数据源会允许他重新获取一次, 但是重新尝试的次数不能超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和. 默认值: 3 (新增于 3.4.5)
poolPingQuery: 用来检验连接是否正常工作并准备接受请求. 默认是 "NO PING QUERY SET", 这会导致多数数据库驱动失败时带有一个恰当的错误消息
poolPingEnabled: 是否启用侦测查询. 若开启, 需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句), 默认值: false.
poolPingConnectionsNotUsedFor : 配置 poolPingQuery 的频率. 可以被设置为和数据库连接超时时间一样, 来避免不必要的侦测, 默认值: 0(即所有连接每一时刻都被侦测 - 当然仅当 poolPingEnabled 为 true 时适用).
它的实现类是 PooledDataSource, 看下它的继承体系图如下, 它实现 javax.sql 的接口规范
我们看下它的获取连接的实现代码如下: 可以看到, 从他里面获取新的连接, 不是无脑 new, 而是受到最大连接数, 空闲连接数, 当前活跃数, 工作连接数等因素的限制
- private PooledConnection popConnection(String username, String password) throws SQLException {
- boolean countedWait = false;
- PooledConnection conn = null;
- long t = System.currentTimeMillis();
- int localBadConnectionCount = 0;
- while (conn == null) {
- // 通过同步代码块保证了线程的安全性, 因为现实环境中, 多用户并发请求获取连接
- synchronized (state) {
- // 如果空闲的连接数不为空, 就使用从空闲池中往外拿连接
- if (!state.idleConnections.isEmpty()) {
- // Pool has available connection
- conn = state.idleConnections.remove(0);
- if (log.isDebugEnabled()) {
- log.debug("Checked out connection" + conn.getRealHashCode() + "from pool.");
- }
- } else {
- // 没有空闲
- // Pool does not have available connection
- if (state.activeConnections.size() <poolMaximumActiveConnections) {
- // 活动的连接池的最大数量 比 预先设置的最大连接数小, 就创建新的连接
- // Can create new connection
- conn = new PooledConnection(dataSource.getConnection(), this);
- if (log.isDebugEnabled()) {
- log.debug("Created connection" + conn.getRealHashCode() + ".");
- }
- } else {
- // 判断最先进入 活跃池中的连接, 设置新的参数然后返回出去
- // Cannot create new connection
- PooledConnection oldestActiveConnection = state.activeConnections.get(0);
- long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
- if (longestCheckoutTime> poolMaximumCheckoutTime) {
- // Can claim overdue connection
- state.claimedOverdueConnectionCount++;
- state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
- state.accumulatedCheckoutTime += longestCheckoutTime;
- state.activeConnections.remove(oldestActiveConnection);
- if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
- try {
- oldestActiveConnection.getRealConnection().rollback();
- } catch (SQLException e) {
- log.debug("Bad connection. Could not roll back");
- }
- }
- conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
- conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
- conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
- oldestActiveConnection.invalidate();
- if (log.isDebugEnabled()) {
- log.debug("Claimed overdue connection" + conn.getRealHashCode() + ".");
- }
- } else {
- // Must wait
- try {
- if (!countedWait) {
- state.hadToWaitCount++;
- countedWait = true;
- }
- if (log.isDebugEnabled()) {
- log.debug("Waiting as long as" + poolTimeToWait + "milliseconds for connection.");
- }
- long wt = System.currentTimeMillis();
- state.wait(poolTimeToWait);
- state.accumulatedWaitTime += System.currentTimeMillis() - wt;
- } catch (InterruptedException e) {
- break;
- }
- }
- }
- }
- if (conn != null) {
- // ping to server and check the connection is valid or not
- if (conn.isValid()) {
- if (!conn.getRealConnection().getAutoCommit()) {
- conn.getRealConnection().rollback();
- }
- conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
- conn.setCheckoutTimestamp(System.currentTimeMillis());
- conn.setLastUsedTimestamp(System.currentTimeMillis());
- state.activeConnections.add(conn);
- state.requestCount++;
- state.accumulatedRequestTime += System.currentTimeMillis() - t;
- } else {
- if (log.isDebugEnabled()) {
- log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
- }
- state.badConnectionCount++;
- localBadConnectionCount++;
- conn = null;
- if (localBadConnectionCount> (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
- if (log.isDebugEnabled()) {
- log.debug("PooledDataSource: Could not get a good connection to the database.");
- }
- throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
- }
- }
- }
- }
- }
- return conn;
- }
- UNPOOLED
这个数据源的实现只是每次被请求时打开和关闭连接. 虽然有点慢, 但对于在数据库连接可用性方面没有太高要求的简单应用程序来说, 是一个很好的选择. 不同的数据库在性能方面的表现也是不一样的, 对于某些数据库来说, 使用连接池并不重要, 这个配置就很适合这种情形. UNPOOLED 类型的数据源具有以下属性.
它存在如下的配置信息
driver - 这是 JDBC 驱动的 Java 类的完全限定名(并不是 JDBC 驱动中可能包含的数据源类).
url - 这是数据库的 JDBC URL 地址.
username - 登录数据库的用户名.
password - 登录数据库的密码.
defaultTransactionIsolationLevel - 默认的连接事务隔离级别.
defaultNetworkTimeout - The default network timeout value in milliseconds to wait for the database operation to complete. See the API documentation of java.sql.Connection#setNetworkTimeout() for details.
作为可选项, 你也可以传递属性给数据库驱动. 只需在属性名加上 "driver." 前缀即可, 例如:
driver.encoding=UTF8
这将通过 DriverManager.getConnection(url,driverProperties) 方法传递值为 UTF8 的 encoding 属性给数据库驱动.
它的实现类是 UnpooledDataSource, 看下它的继承体系图如下, 它实现 javax.sql 的接口规范
我们看下它对获取连接的实现代码如下: 每一次获取连接都使用 jdk 底层的加载驱动, 创建新的连接给用户使用
- private Connection doGetConnection(Properties properties) throws SQLException {
- initializeDriver();
- Connection connection = DriverManager.getConnection(url, properties);
- configureConnection(connection);
- return connection;
- }
- private synchronized void initializeDriver() throws SQLException {
- if (!registeredDrivers.containsKey(driver)) {
- Class<?> driverType;
- try {
- if (driverClassLoader != null) {
- driverType = Class.forName(driver, true, driverClassLoader);
- } else {
- driverType = Resources.classForName(driver);
- }
- // DriverManager requires the driver to be loaded via the system ClassLoader.
- // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
- Driver driverInstance = (Driver)driverType.newInstance();
- DriverManager.registerDriver(new DriverProxy(driverInstance));
- registeredDrivers.put(driver, driverInstance);
- } catch (Exception e) {
- throw new SQLException("Error setting driver on UnpooledDataSource. Cause:" + e);
- }
- }
- }
- JNDI
作为了解吧, 这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用, 容器可以集中或在外部配置数据源, 然后放置一个 JNDI 上下文的引用.
MyBatis 中的事务管理器
MyBatis 中存在两种事务管理器如下:
JDBC
xml 配置
- <transactionManager type="JDBC">
- <property name="closeConnection" value="false"/>
- </transactionManager>
这个配置就是直接使用了 JDBC 的提交和回滚设置, 它依赖于从数据源得到的连接来管理事务作用域.
相关编码的实现: 它通过 sqlSession 对象的 commit 方法, 和 rollback 方法实现事务的提交和回滚
设置自动提交, 使用 openSession()重载的方法
- // 1. 读取配置文件
- InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
- // 2. 创建 SqlSessionFactory 工厂
- SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
- // 3. 创建 sqlSession
- SqlSession sqlSession = factory.openSession(true);
- MANAGED
这个配置几乎没做什么. 它从来不提交或回滚一个连接, 而是让容器来管理事务的整个生命周期, 默认情况下它会关闭连接, 然而一些容器并不希望这样, 因此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为
- <transactionManager type="MANAGED">
- <property name="closeConnection" value="false"/>
- </transactionManager>
如果你正在使用 Spring + MyBatis, 则没有必要配置事务管理器, 因为 Spring 模块会使用自带的管理器来覆盖前面的配置
动态 SQL
MyBatis 的动态 sql 为我们提供了什么功能呢? 举一个相似的场景, 就是用户提交了 username password 两个字段的信息到后端, 后端进行下一步校验, 然后后端的程序员可能就的通过自己拼接 sql 来完成这个功能 类似这样 select * from user where username = + username + and password = +password , 一个两个没事, 这是一个痛苦的事, 例如拼接时要确保不能忘记添加必要的空格, 还要注意去掉列表最后一个列名的逗号
动态 sql 解决了这个问题
虽然在以前使用动态 SQL 并非一件易事, 但正是 MyBatis 提供了可以被用在任意 SQL 映射语句中的强大的动态 SQL 语言得以改进这种情形.
if
最常用的一种是, 将对象中满足 if 条件的字段当成 sql 中 where 的条件
举个例子, 当用户名不为空时, 按照用户名查找
- <select id="findUserByCondition" resultType="com.changwu.pojo.User" parameterType="com.changwu.pojo.User">
- select * from user where 1=1
- <if test="userName != null">
- and username = #{userName}
- </if>
- </select>
- choose (when, otherwise)
choose 类似 java 中的 switch case 语句, 像下面这样, 命中了某一种情况后不再匹配其他的情况, 都没有命中的话执行默认的代码块
- Integer i =1;
- switch (i) {
- case 1:
- //do
- break;
- case 2:
- //do
- break;
- default:
- //do
- }
示例: 从 user 表中检索, 当 userName 不为空时, 仅仅使用 userName 当成条件去匹配, 如果 userName 为空, 则检查第二个条件是否满足, 如果第二个条件满足了, 则用第二个条件当成结果拼接到 sql 中, 所有条件都没有就拼接 < otherwise > 标签中的语句
- <select id="findUserByConditionLike"
- resultType="com.changwu.pojo.User"
- parameterType="com.changwu.vo.QueryVo">
- select * from user
- <where>
- <choose>
- <when test="userName != null">
- and username like #{userName}
- </when>
- <when test="user != null and user.sex != null">
- and sex = #{user.sex}
- </when>
- <otherwise>
- and count = 1
- </otherwise>
- </choose>
- </where>
- </select>
- trim (where, set)
神奇的 AND title like #{title} , 看下面的例子
- <select id="findActiveBlogLike"
- resultType="Blog">
- SELECT * FROM BLOG
- WHERE
- <if test="state != null">
- state = #{state}
- </if>
- <if test="title != null">
- AND title like #{title}
- </if>
- <if test="author != null and author.name != null">
- AND author_name like #{author.name}
- </if>
- </select>
如果所有的 if 条件全都不成立, 那么最终拼接的 sql 是这样的
SELECT * FROM BLOG WHERE
如果第一个条件不成立, 而第二个条件成立, 拼接成的 sql 是这样的
SELECT * FROM BLOG WHERE AND title like #{title}
以上的两个结果都将导致 java 程序运行失败, Mybatis 推出 < where > 标签解决了这个问题, 像下面这样
被标签包围的 if 条件, 只有至少有一个 if 成立了, 才会在 sql 中拼接上 where 关键字, 如果仅仅只有一个 if 成立了, 这个 if 还带有 and or 的关键字样, where 会自动踢除他们
- <select id="findActiveBlogLike"
- resultType="Blog">
- SELECT * FROM BLOG
- <where>
- <if test="state != null">
- state = #{state}
- </if>
- <if test="title != null">
- AND title like #{title}
- </if>
- <if test="author != null and author.name != null">
- AND author_name like #{author.name}
- </if>
- </where>
- </select>
- foreach
forEach 的功能非常强大! 它允许程序员指定一个集合, 然后通过 foreach 标签遍历这个集合从而完成 in 语句的拼接
注意点 1: collection 代表将要遍历的集合, 下面我给他取名 ids, 这个名字不是乱取的, 对应着我的 "com.changwu.vo.QueryVo" 这个 vo 中的一个属性名
注意点 2:#{id}里面的名和 item 保持一致
- <select id="selectUserInIds" resultType="com.changwu.pojo.User" parameterType="com.changwu.vo.QueryVo">
- select * from user where id in
- <foreach item="id" collection="ids" open="(" separator="," close=")">
- #{id}
- </foreach>
- </select>
玩转 MyBatis 多表关系
xml 版: 一对一级联
实验场景: 比如说学生和学号是百分百的一对一的关系, 但是我直接用一个不太恰当的例子, 就是强制规定 用户和账户的是一对一的关系, 即一个用户只能存在一个账户(这个例子很牵强...)
这时, 我们想实现这样的级联效果: 在查询账户的同时, 级联查询出这个账户所属的用户信息
第一点: 就是数据库中的表怎么设计, 在账户表中添加一列当成外键, 关联用户表中的用户 id
第二点: 我们想在查询账户的同时级联查询出用户的信息, 说白了就是让 Mybatis 帮我们将属性封装进我们使用 resultType 标签指定的返回值类型的对象中, 于是我们就得像下面这样写 账户类, 在账户类中添加用户信息引用
- public class Account {
- private Integer id;
- private Integer uid;
- private Integer money;
- private User user;
- }
第三点: sql 语句怎么写?
关注点就是下面的 resultMap 标签中的 < association > 标签, 通过这个标签告诉 Mybatis 如何将查询出来的结果封装进 Account 中的 User 字段
可以看到我在 association 标签中将 user 所有的属性全都配置进去了, 其实这是没必要的, 因为我的 sql 语句并没有返回全部的结果
association 中存在一个 column 属性, 这个属性存放就是在 account 表中的外键的列名 , javaType 表示是告诉 MyBatis, 这个封装类的类型
- <resultMap id="accountUserMap" type="com.changwu.pojo.Account">
- <id property="id" column="id"/>
- <result property="uid" column="uid"/>
- <result property="money" column="money"/>
- <association property="user" column="uid" `javaType`="com.changwu.pojo.User">
- <id property="id" column="id"/>
- <result property="userName" column="userName"/>
- <result property="birthday" column="birthday"/>
- <result property="sex" column="sex"/>
- <result property="address" column="address"/>
- </association>
- </resultMap>
- <!-- 查询所有的账户, 同时包含用户名和地址信息 -->
- <select id="findAllAccount" resultMap="accountUserMap">
- select a.*,u.username,u.address from account a,user u where a.uid=u.id
- </select>
注解版: 一对一级联
配置查询账户时, 级联查询出账户所属的用户, 如果说, 账户实体和数据库中表的字段命名不同, 需要用到下面的 @Result()注解进行纠正, 当然虽然我下面写了四个 @Result, 除了第一个配置 id, 中间两个的 property 和 column 值是一样的, 所以根本没有写的必要
有必要的是一对一的关系需要使用 @Result()配置, 同样 column 为 Account 表中的关联 user 表中的外键列名, 强制不能写错, 具体是一对一, 但是一对多的关系通过 one 和 manay 控制, 通过 fetchType 控制是及早求值还是懒加载
- @Select("select * from account")
- @Results(id = "account",value = {
- // 先描述自己的信息, 然后描述一对一的信息
- @Result(id = true, property = "id",column = "id"),
- @Result(property = "uid",column = "uid"),
- @Result(property = "money",column = "money"),
- @Result(property = "user",column = "uid",
- one=@One(select = "com.changwu.dao.IUserDao.findById",fetchType = FetchType.EAGER))
- })
- List<Account> findAll();
xml 版: 一对多级联
实验场景: 查询用户的信息的同时级联查询出用户所有的账户的信息
在 java 中上面的描述转换成代码的意思是, User 类中要添加一个集合, 存放查询出来的账户的信息, 我们进一步通过配置告诉 Mybatis 将查询出的属性封装进这个 list 中
- public class User {
- private Integer id;
- private String userName;
- private Date birthday;
- private String sex;
- private String address;
- // 在主表中唯一从表的集合
- private List<Account> accounts;
像下面这样配置
注意点就是在一对多的配置中我们使用 collection 标签, 接着使用属性 ofType 标识 一的一方维护的集合中元素的类型
像这种 property 类型的属性全都是 java 类中的属性名, 写错了 MyBatis 会报错
column 属性: 按理说是数据库中列名, 如果不一样的话, 不至于报错, 但是数据一定封装不上, 但是有时候 也可能是在 sql 语句中为原列名取的别名的名称
- <!-- todo 一对多的配置 -->
- <resultMap id="findAllUserAndUserAccount" type="com.changwu.pojo.User">
- <id property="id" column="id"/>
- <result property="userName" column="userName"/>
- <result property="birthday" column="birthday"/>
- <result property="sex" column="sex"/>
- <result property="address" column="address"/>
- <!-- 一对多的配置 -->
- <!--ofType 是一的一方维护的集合中元素的类型 -->
- <collection property="accounts" ofType="com.changwu.pojo.Account">
- <id property="id" column="aid"/>
- <result property="uid" column="uid"/>
- <result property="money" column="money"/>
- </collection>
- </resultMap>
- <select id="findAllUserAndUserAccount" resultMap="findAllUserAndUserAccount">
- select * from user u left outer join account a on u.id=a.uid
- </select>
注解版: 一对多级联
和 1 对 1 的配置很像
- @Select("select * from user")
- @Results(id="UserAccount",value = {
- @Result(id = true ,property = "id",column = "id"),
- @Result(property = "accounts",column = "id",
- many = @Many(select = "com.changwu.dao.IAccountDao.findAccountByUid",fetchType = FetchType.LAZY))
- })
- List<User> findAll();
多对多级联
实验场景: 典型的用户和角色之间的关系
多对多的配置其实和一对多一样, 比如想在查询出用户信息时级联查询出这个用户拥有的角色的信息
于是第一步:
我们在 User 类中添加属性 private List<Role> roles;
第二步: 写 xml 中的 sql mapper 配置
下面这个 column 属性配置的 rid 其实就是在使用我们 sql 中为数据库中的某列取的别名
如果查询的结果中出现了两个相同的列名, 但是值不同, 代表的意义也不同, 最好就给其中一个取别名
- <!--todo 多对多 role user-->
- <resultMap id="roleUserMap" type="com.changwu.pojo.Role">
- <!--todo 这里的 colum 就是不原生的数据库的列名, 而是取的别名 -->
- <id property="id" column="rid"/>
- <result property="roleName" column="role_name"/>
- <result property="roleDesc" column="role_desc"/>
- <collection property="users" ofType="com.changwu.pojo.User">
- <id property="id" column="id"/>
- <result property="userName" column="userName"/>
- <result property="birthday" column="birthday"/>
- <result property="sex" column="sex"/>
- <result property="address" column="address"/>
- </collection>
- </resultMap>
- <select id="findAll" resultMap="roleUserMap">
- select u.*,r.ID as rid,r.ROLE_NAME,r.ROLE_DESC from role r
- left join user_role ur on r.ID=ur.RID
- left join user u on ur.UID = u.id
- </select>
MyBatis 的延迟加载
association 一对一的延迟加载
即用户和账户的关系是一对一的关系, 我们希望这样, 当用户仅仅查询账户信息时, Mybatis 仅仅执行查询账户信息的语句, 但是当用户使用这个账户关联的对象时, 再让 MyBatis 将账户对象中的用户对象的引用时, 触发懒加载, 让 mybatis 再去查询数据库
像下面这样配置 xml 文件, 它其实是对原生的一对一的级联查询的升级, 将 association 标签内部的通过 result 的属性描述全部去掉了, 因为目标是懒加载, 加上这些描述也用不到了
取而代之的是一个新星 select, 它指向了 IUserDao 中的根据 id 查询用户的方法 findUserById
还有一个注意点就是, association 中的 column 属性不能去掉, 而且必须写成数据库中 Account 表中存放关联 User 的外键的那个列名, 通过它指定当触发延迟加载时, 使用哪个字段给 findById()方法使用
- <!--todo 延迟加载 -->
- <resultMap id="findAllAccountLazy" type="com.changwu.pojo.Account">
- <id property="id" column="id"/>
- <result property="uid" column="uid"/>
- <result property="money" column="money"/>
- <!-- select 指定的内容, 可以查询出单个用户的唯一方法标识 -->
- <!-- 这里的 column 属性, 指定的是 select 中指定的 fingById 所需要的 id 值 -->
- <association property="user" column="uid" javaType="com.changwu.pojo.User" select="com.changwu.dao.IUserDao.findById">
- </association>
- </resultMap>
- <select id="findAll" resultType="int" resultMap="findAllAccountLazy">
- select * from account
- </select>
下面是 User 中 findById 的配置, sql 中的 #{}中的内容是可以随便写的
- <!--todo 疑问下面这 id 可不可以乱写 -->
- <select id="findById" parameterType="int" resultMap="UserMap">
- select * from user where id = #{123id}
- </select>
实验成功的结果就是, 当我们使用 Account 的 fingAll 方法时, 如果不继续 getUser(), 结果控制台打印单条 sql, 一旦使用 getUser(), 控制台会继续打印多条新的 sql
collection 实现一对多的延迟加载
一个用户存在多个账户, 我们希望如果仅仅是查询用户信息则延迟加载用户账户的信息, 使用用户信息时, 才再次执行新的 sql 加载用户的信息
实现的思路和上面的相似, 注意 collection 标签中的 column 的值, 已经 select 标签中 findAccountByUid 的实现
- <!-- todo 一对多的配置 延迟加载 -->
- <resultMap id="findAllUserAndUserAccount" type="com.changwu.pojo.User">
- <id property="id" column="id"/>
- <result property="userName" column="userName"/>
- <result property="birthday" column="birthday"/>
- <result property="sex" column="sex"/>
- <result property="address" column="address"/>
- <!-- 一对多的配置 -->
- <!--ofType 是一的一方维护的集合中元素的类型 -->
- <collection property="accounts" ofType="com.changwu.pojo.Account"
- column="id" select="com.changwu.dao.IAccountDao.findAccountByUid">
- </collection>
- </resultMap>
- <select id="findAllUserAndUserAccount" resultMap="findAllUserAndUserAccount">
- select * from user
- </select>
缓存
什么是缓存?
缓存是指将查询的数据暂存在内存中, 当下次再次查询时, 直接从内存中获取, 实现减少和数据库交互的次数, 提高执行效率
适用于缓存的数据: 经常被查询, 不经常被修改, 而且对此类数据的一致性没有很严格的要求, 与之相反的数据比如, 银行的汇率, 商品的库存中数据一致性要求极其严格的数据就不适合使用缓存机制
Mybatis 中的一级缓存
一级缓存是存在于 MyBatis 的 SqlSession 中的数据, 当用户执行一次查询, 查询的结果就会被缓存在 SqlSession 中一份, 当用户再次查询时, 先检查 sqlSession 中是否存在相应的数据, 如果存在的话不再重新查询数据库, 而是取出缓存中的数据给用户
所以, 当 sqlSession 对象消失时, 一级缓存就不复存在
一级缓存是默认存在的
像下面这个测试, 全程使用同一个 sqlSession, 那么获取出来的 user 也会是同一个, 控制台仅仅输入一条 sql, 打印结果也为 true
- @Test
- public void testFirstCache(){
- IUserDao userDao = this.sqlSession.getMapper(IUserDao.class);
- User user1 = userDao.findUserByIdFirstCache(42);
- User user2 = userDao.findUserByIdFirstCache(42);
- System.out.println(user1==user2);
- }
但是像下面这样, 一级缓存将会消失
- public void testFirstCache(){
- IUserDao userDao = this.sqlSession.getMapper(IUserDao.class);
- User user1 = userDao.findUserByIdFirstCache(42);
- this.sqlSession.close();
- this.sqlSession = this.factory.openSession();
- userDao = this.sqlSession.getMapper(IUserDao.class);
- User user2 = userDao.findUserByIdFirstCache(42);
- System.out.println(user1==user2);
- }
同样适用 sqlSession 的 clearCache()也可以实现缓存的清空
为了安全, MyBatis 的一级缓存在 sqlSession 出现 修改, 添加, 删除, commit(),close()等方法时, 就会被清空
MyBatis 中的二级缓存
二级缓存指的是 MyBatis 中的 SqlSessionFactory 对象的缓存, 由同一个 SqlSessionFactory 对象创建的 SqlSession 会共享这块二级缓存
使用: 在 MyBatis 主配置文件中开始支持缓存的配置, 默认是开启的状态
<setting name="cacheEnabled" value="true"></setting>
在从配置文件中开启缓存的配置
- <!-- 为当前的 mapper 开启二级缓存的支持 -->
- <cache/>
第三步: 在 select 标签中添加 userCache 属性
- <!-- 测试一级缓存 -->
- <select id="findUserByIdFirstCache" parameterType="int" resultMap="UserMap" useCache="true">
- select * from user where id = #{id}
- </select>
测试:
按照上面的配置后, 编写下面的测试方法, 测试二级缓存的存在就得关闭一级缓存, 在下面的测试用例中同时开启两个 sqlSession, 第一个 sqlSession 查询完结果后随即关闭, 接着开启第二个 sqlSession, 获取 mapper 继续查询, 但是整个流程查询的 sql 仅仅会执行一次, 原因就是存在二级缓存, 为什么最后的输出结果 user!=user2 呢? 因为属于 SqlSessionFactory 的二级缓存, 存放的并不是对象, 而是键值对形式存在的数据, 使用这部分缓存时, MyBatis 会自动为我们创新的对象, 然后将这部分数据封装进去, 返回这个新对象
- @Test
- public void testFirstCache(){
- SqlSession sqlSession1 = this.factory.openSession();
- IUserDao userDao = sqlSession1.getMapper(IUserDao.class);
- User user1 = userDao.findUserByIdFirstCache(42);
- System.out.println(user1);
- sqlSession1.close();
- SqlSession sqlSession2 = this.factory.openSession();
- userDao = sqlSession2.getMapper(IUserDao.class);
- User user2 = userDao.findUserByIdFirstCache(42);
- System.out.println(user2);
- sqlSession2.close();
- System.out.println(user1==user2);
- }
注解版开启二级缓存
在我们的 dao 层上添加如下注解即可
@CacheNamespace(blocking = true)
来源: https://www.cnblogs.com/ZhuChangwu/p/11734347.html