Java——MyBatis

1. 简介及引入

普通原生JDBC的问题:

  1. 每次查询都要开关数据库连接,性能不高(解决方案:连接池)
  2. SQL语句、Statement、ResultSet硬编码在类中,通用性不高(解决方案:配置到XML中、查询结果自动映射为Java对象)

MyBatis可以帮我们封装输入参数、输出结果。

官方文档:MyBatis
Maven引入依赖:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.13</version>
</dependency>

<!-- 导入Log4J方便调试 -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2. MyBatis框架概览

  • SqlMapConfig.xml:MyBatis的全局配置文件,因为是我们手动加载的,可以任意命名
  • mapper.xml:映射文件,命名方式为XXXMapper.xml
  • SqlSessionFactory:用来创建SqlSession
  • SqlSession:用来发出SQL语句
  • Executor:SqlSession内部通过Executor操作数据库。
  • mapped statement:封装SQL语句、输入参数、输出结果

使用步骤如下:

第一步创建MyBatis的全局配置XML,指定Mapper:

<?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>
  <!-- 环境列表,及默认环境。整合Spring后将不使用environments -->
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <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>
  <!-- 声明映射文件,以classpath为根路径,使用/代替. -->
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

第二步配置映射文件Mapper.xml,每一个语句的定位由mapper的namespace和语句的id唯一确定:

<?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="org.example.UserMapper">
  <!-- select语句。id指定sql语句封装成的MappedStatement的id,resultType指定输出数据类型 -->
  <select id="findUserById" resultType="com.example.domain.User">
    SELECT * FROM users WHERE id = #{id}
  </select>
</mapper>

第三步读取MyBatis配置文件创建SqlSessionFactory。SqlSessionFactory使用单例模式进行访问,避免重复创建,提高效率:

public static void main(String[] args) {
	private static SqlSessionFactory factory;
    static {
		Reader resource = null;
		try {
			// 以classpath为根路径,包名分隔符使用/代替.
			resource = Resources.getResourceAsReader("/package/to/mybatis.xml");
			factory = new SqlSessionFactoryBuilder().build(resource);
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (resource != null) {
				try {
					resource.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

第四步使用SqlSessionFactory创建SqlSession进行查询,使用selectOne()selectList()insert()update()delete()进行对应的增删改查操作,接收参数为第二步中的语句坐标,以及查询参数。MyBatis根据传入的语句坐标执行具体语句。

public void selectUserById() {
    SqlSession session = sqlSessionFactory.openSession();
    try {
    	// 使用mapper的namespace + statement的id
	    User user = (User)session.selectOne("com.example.mybatis.UserMapper.selectUserById", 101);
    } finally {
    	session.close();
    }
}

SqlSession具有一些属性,同时是线程不安全的,因此最好放到方法体当中创建为局部变量,就可以避免多线程操作冲突。

IDEA中需要将XML等资源文件加入到classpath中,才能打包到最后的包中。
加入classpath的方式有两种,第一种是放置到resources类型的目录下
在这里插入图片描述
第二种是编译时将指定格式的资源文件也编译进去

<!-- pom.xml -->
<build>
  <resources>
    <resource>
      <directory>src/main/java</directory>
      <includes>
        <include>**/*.xml</include>
      </includes>
    </resource>
  </resources>
</build>

3. Mapper声明及使用

Mapper中的SQL语句中的参数可以使用两种符号

  • #{}:对应SQL语句中的占位符,例如,会自动对输入参数添加引号'',基本数据类型的参数可以使用任意变量名,POJO类型需要使用属性名。
  • ${}:直接拼接输入的参数到SQL中,所以需要手动添加'',也可以添加匹配符%,基本数据类型的参数名必须为value,POJO类型与#{}一样使用属性名。缺点是可能会被SQL注入。

3.1 纯XML方式

Mapper声明,此时的namespace只起到区分Mapper的作用:

<mapper namespace="com.example.mapper">
    <select id="findUserById" resultType="com.example.mybatis.domain.User">
        SELECT * FROM users WHERE id = #{value}
    </select>
</mapper>

Mapper使用,需要通过namespace+id引用具体Mapper:

SqlSession session = factory.openSession();
User user = session.selectOne("com.example.mapper.findUserById", 3);

3.2 XML+接口方式

首先需要声明一个接口作为Mapper接口,方法名为具体的查询方法:

package com.example.mapper;
public Interface UserMapper {
    User findUserById(int id);
}

然后声明Mapper XML,此时namespace需要为Mapper接口的完整类名,id为mapper接口中的对应方法:

<mapper namespace="com.example.mybatis.mapper.UserMapper">
    <select id="findUserById" resultType="com.example.mybatis.domain.Commodity">
        SELECT * FROM commodity WHERE id = #{value}
    </select>
</mapper>

然后获取Mapper接口类实例进行查询,MyBatis会根据接口的完整路径在XML中查找相对应的mapper语句,然后帮我们进行接口实现,调用接口的具体方法就可以实现查询了:

UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.findUserById(6);

3. 全局配置

全局配置包含的字标签内容有:

  • properties(属性)
  • settings(全局配置)
  • typeAliases(别名)
  • typeHandlers(类型处理器)
  • objectFactory(对象工厂)
  • plugins(插件)
  • environments(环境)
  • mappers(映射器)

3.1 配置方式

全局配置可以使用纯XML方式,也可以使用XML+properties的方式。

3.1.2 纯XML方式

如果有&需要使用&amp;

<dataSource type="POOLED">
    <property name="driver" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306?serverTimezone=UTC&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
    <property name="username" value="root"/>
    <property name="password" value="123456"/>
</dataSource>

3.1.3 XML结合properties

可以在XML中声明引用的外部properties,同样是以classpath为根路径,最好加上一层变量封装,避免跟操作系统自带变量重名,例如使用jdbc.username代替username

<!-- 指定使用的资源文件 -->
<properties resource="mybatis-config.properties" />
<environments>
	<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>
</environments>
// mybatis-config.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=

如果使用中文查询无法正常查询出结果集时,需要设置连接的URL添加characterEncoding=UTF-8参数,同时还需要配置MySQL的服务端字符集character-set-server为uft8

// my.ini
[mysqld]
character-set-server=utf8

参考:关于mybatis 中文条件查询没结果的问题

也可以在创建SqlSessionFactory时传入读取的props

is = Resources.getResourceAsStream("mybatis-config.xml");
Properties props = Resources.getResourceAsProperties("mybatis-config.properties");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is, props);

3.2 全局配置项

全局配置项为<configuration>标签中的可以设置的事项

3.2.1 properties

可以通过resource属性指定要加载的属性文件,或者通过内部的property属性指定具体属性值:

<configuration>
    <properties resource="">
        <property name="" value=""/>
    </properties>
</configuration>

3.2.2 settings

MyBatis的配置:

<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="autoMappingBehavior" value="FULL"/>
    </settings>
</configuration>

其中一些重要的配置:

  • mapUnderscoreToCamelCase:自动映射的过程中把数据库字段名中的_转换为Java变量通用的驼峰式命名法,例如user_id可以映射为userId,默认为false
  • autoMappingBehavior:自动映射行为,默认为PARTIAL,即只自动映射单层ResultMap(包含ResultType),不自动映射多层ResultMap。设置为FULL启用多层ResultMap映射。

3.2.3 typeAliases

Mapper XML中需要指定parameterType和resultType的具体类型,为了方便具体类型的引用,MyBatis内置了对于基础数据类型及包装类型的别名,例如_int对应int类型,int对应Integer类型。其他自定义的POJO就需要我们在MyBatis全局配置文件中设置别名了,例如:

<typeAliases>
  <!-- 单个别名 -->
  <typeAlias alias="Author" type="domain.blog.User"/>
  <!-- 批量设置别名,声明包名,则包下的类自动赋予与类名相同的别名,首字母大小写都可以-->
  <package name="com.example"/>
</typeAliases>

Mapper中就可以直接使用:

<select resultType="User" />

3.2.4 TypeHandlers(类型处理器)

MyBatis中通过TypeHandlers完成从Java类型转换为JDBC类型,MyBatis内置提供了很多的TypeHandlers。

3.2.5 Mapper

加载Mapper的两种方式:

  • 单个加载:
    • resource属性加载以classpath为根路径的mapper XML
      <mapper resource="mapper/UserMapper.xml"/>
      
    • url属性加载绝对物理路径的mapper XML
      <mapper url="file:///xxxxxx\xxx\xxx\UserMapper.xml" />
      
    • class属性加载mapper XML对应的Mapper接口,XML与接口类需要同名且同包
      <mapper class="com.example.UserMapper"/>
      
  • 批量加载某个包
    <!-- 批量加载,在通过Mapper接口加载的前提下,声明Mapper接口的包名,则会自动加载该包名下的Mapper接口及Mapper XML -->
    <package name="com.example"/>
    

4. 查询示例

4.1 查询多条记录

  <select id="selectBlogByName" parameterType="java.lang.String" resultType="Blog"> <!-- ${}必须使用value作为参数名 -->
  	select * from Blog where name = '%${value}%'
  </select>
Blog blog = (Blog)session.selectList("org.mybatis.example.BlogMapper.selectBlog", 101);

查询单条时使用selectOne()或者selectList()都可以,但是查询多条时必须使用selectList(),否则抛出TooManyResultsException

4.2 插入记录

<!-- 输入对象为POJO时,#{}必须以其属性名命名 -->
<insert id="insertUser" parameterType="xx.xx.User">
	insert into user(id,name) values (#{id}, ${name})
</insert>
sqlSession.insert("insertUser", new User());
// 需要提交才会实际进行修改
sqlSession.commit();
sqlSession.close();

获取自增主键自动生成的主键的方式有两种

  • 设置useGeneratedKeys=true,并且设置keyProperty为自增主键的字段名

    <insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user (name) values ('root')
    </insert>
    

    生成的主键值就会赋值给传入的POJO对象中。

  • 使用MySQL的last_insert_id()函数,会将获取后的主键值设置到传入的POJO对象中:

    <insert id="insertUser" parameterType="xx.xx.User">
    	<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
    		SELECT LAST_INSERT_ID()
    	</selectKey>
    	insert into user(name) values (${name})
    </insert>
    

非自增主键可以首先通过MySQL的uuid()函数生成35位字符串,作为主键的值,然后insert语句执行后,取出id值:

<insert id="insertUser" parameterType="xx.xx.User">
	<selectKey keyProperty="id" order="BEFORE" resultType="java.lang.String">
		SELECT UUID()
	</selectKey>
	insert into user(id,name) values (#{id}, ${name})
</insert>

4.3 删除记录

<delete id="deleteUser" parameterType="java.lang.Integer">
	delete from user where id=#{id}
</delete>

4.3 更新记录

<update id="updateUser" parameterType="xx.xx.User">
	update user set username=#{username} where id=#{id}
</update>

5. DAO

5.1 原生DAO

原始DAO即自己创建DAO接口和具体实现类,例如:

public interface UserDao {
    User findUserById(int id) throws Exception;
	int insertUser(User user) throws Exception;
	boolean deleteUser(User user) throws Exception;
}

public class UserDaoImpl implements UserDao {
    private SqlSessionFactory factory;
    public UserDaoImpl(SqlSessionFactory factory) {
    	this.factory = factory;
    }
    User findUserById(int id) {
    	SqlSession session = this.factory.openSession();
    	// statement的id, 传入参数
    	session.selectOne("xxx", 2);
    	session.close();
    }
    // ...
}

原始DAO的缺点:

  1. 每个方法内存在一些重复代码
  2. statement的id硬编码在代码中
  3. 传入参数为Object,编译时无法发现错误

5.2 Mapper代理接口

鉴于原始DAO的缺点,MyBatis提供了Mapper接口的机制。Mapper接口相当于DAO接口,只要遵循了Mapper的规范,MyBatis就可以为我们创建实现类。

开发规范:

  1. Mapper XML文件的namespace为Mapper接口的完整类名
  2. Mapper接口中的方法名与Mapper XML中的statement的id一致,方法参数类型与parameterType一致,返回值类型与resultType一致。

实现原理:通过类名、方法名、方法参数、方法返回值通过反射调用对应的Mapper接口,生成实现类作为DAOImpl供我们调用。

Mapper方法内部根据返回参数是否是集合对象来决定调用selectOne()selectList()

限于Mapper XML中的parameterType只有一个,可以使用POJO类型实现多个参数的传递。

6. 输入映射

通过parameterType指定输入参数的类型,可以是基础类型、HashMap、POJO类型

6.1 hashmap

以某个键值对的key和value为参数值:

<select id="selectUser" parameterType="hashmap">
    select * from user where name = #{some_key} and age = #{some_value}
</select>

6.2 POJO

可以将多个查询参数包装为自定义的POJO类型,然后Mapper中调用POJO类型的属性。

public class UserQueryVo{
	private User user;
	private Order order;
}
  <select id="selectUser" parameterType="com.example.UserQueryVo"> <!-- ${}必须使用value作为参数名 -->
  	select * from user where name LIKE '%${user.name}%' AND id = #{order.id}
  </select>
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setName("小王");
Order order = new Order();
order.setId(4);
UserQueryVo query = new UserQueryVo(user, order);
mapper.selectUser(query);

7. 输出映射

输出映射分为resultType和resultMap。其实resultType也是通过resultMap实现的。

解决查询结果集字段名与POJO对象属性名不一致的方法有多种:

  • 可以给结果集字段名取别名,与POJO属性名一致,然后指定resultType为对应POJO即可。这种方式是结果集去适应POJO。
  • 也可以自定义一个新的POJO,属性名与查询的结果集字段名一一对应,通过指定resultType即可。这种方式是POJO去适应结果集
  • 定义一个resultMap,其中结果集字段名与POJO属性名一致的可以自动映射,不一致的可以通过<id><result>标签进行映射。这种方式是以resultMap为桥梁连接结果集和POJO。

7.1 resultType

resultType可以为简单数据类型、POJO、hashmap。hashmap以字段名为key,字段值为value。

与parameterType一样,值都是集合中的类型或单个POJO的类型,不是集合类型。

使用resultType进行输出映射时,只有查询出的字段名与POJO中的属性名一致,该字段值才会赋给对应属性。

如果查询出的字段名与POJO中的属性完全不一样,则不会创建POJO对象。只要有一个一样,就会创建POJO。

7.2 resultMap

解决resultType中结果字段名称和POJO属性名称不一致、或者POJO中包含了其他POJO,导致无法直接映射的问题。

<mapper>
	<!-- type是最终映射的Java类型,可以为aliases。id为resultMap的id -->
	<resultMap type="user" id="userResultMap">
		<!-- id标签为结果集中的唯一标识的字段。column为结果字段名,property为POJO的属性名 -->
		<id column="id_" property="id" />
		<!-- result标签为普通字段 -->
		<result column="name_" property="name"/>
	</resultMap>
	<!-- resultMap中以已定义的resultMap的id为值,如果是其他Mapper中定义的resultMap,需要在前面加上namespace -->
	<select id="findUserById" resultMap="userResultMap">
		<!-- 给字段取别名 -->
	    select id id_, name name_ from user where id = #{value}
	</select>
<mapper>

POJO复杂嵌套的情况时使用association(一对一)、collection(一对多),其实这两个也是一种resultMap。

8. 动态SQL

通过表达式动态拼装SQL语句。

IF判断

<select id="findUsers">
	select * from user
	<!-- where标签可以自动去除内部生成表达式的第一个and -->
	<where>
		<if test="userCustom != null">
			<if test="userCustom.name != ''">
				and name = #{userCustom.name}
			</if>
			<if test="userCustom.mobile != ''">
				and mobile = #{userCustom.mobile}
			</if>
		</if>
	</where>
</select>

SQL片段

定义可复用的SQL语句片段,一般基于单表进行定义,也尽量不要包含where标签,可以更好地复用。

<sql id="query_user_where">
	<if test="userCustom != null">
		<if test="userCustom.name != ''">
			and name = #{userCustom.name}
		</if>
		<if test="userCustom.mobile != ''">
			and mobile = #{userCustom.mobile}
		</if>
	</if>
</sql>

<select id="findUsers">
	select * from user
	<where>
		<!-- 引用sql片段,如果定义在其他Mapper中,需要在前面加上namespace -->
		<include refid="query_user_where"/>
	</where>
</select>

foreach

如果传入参数是数组或List时,可以使用foreach标签循环输入的参数:(collection和item不用加#{},里面的参数需要加#{})

<select id="findUsers" parameterType="int">
	select * from user
	<where>
		<!-- 输入参数中是否包括ids属性 -->
		<if test="ids != null">
			<!-- collection是要遍历的集合,item是每一项的变量名,open是遍历前添加的字符串,close是遍历后添加的字符串,separator是两次遍历间的字符串 -->
			<!-- 组合成类似 AND (1 OR 2 OR 3) -->
			<foreach collection="ids" item="id" open="AND (" close=")" separator="OR">
				id = #{id}
			</foreach>
			<!-- 组合成类似 IN (1, 2, 3) -->
			<foreach collection="ids" item="id" open="IN (" close=")" separator=",">
			    #{id}
			</foreach>
		</if>
	</where>
</select>

9. 多表查询

一对一查询

一对一查询的例子,比如订单对用户是一对一(一个订单对应一个用户)

  • 使用resultType的话,直接继承自Order(或User类),并把复制User(或Order)中的属性到继承的子类中,使得POJO的属性与查询结果字段能一一对应即可。

  • 使用resultMap的话,直接改写Order类,添加一个成员变量User,然后配置resultMap如下:

    <resultMap id="ordersUserResultMap" type="com.example.Order">
    	<!-- 首先配置Order类原先的数据。 -->
    	<!-- id标签为唯一标识Order实体的字段,column为字段名,property为Order中的对应属性 -->
    	<id column="id" property="id"/>
    	<!-- result标签为普通字段 -->
    	<result column="user_id" property="userId" />
    	<result column="orderno" property="orderno" />
    	<!-- 然后使用association把查询到的其他字段配置到相关联的单个对象中。property为要映射到的Order中的关联属性名,javaType为其类型 -->
    	<association property="user" javaType="com.example.User">
    		<!-- 然后映射结果字段到关联对象属性中 -->
    		<!-- 首先要注意id标签指明查询结果中唯一标识关联对象的字段user_id,对应Order中属性user中的id属性 -->
    		<id column="user_id" property="id"/>
    		<!-- 然后是普通字段 -->
    		<result column="name" property="name"/>
    	</association>
    </resultMap>
    <select resultMap="ordersUserResultMap">
    	select * from orders, user where orders.user_id = user.id
    </select>
    

使用resultType和resultMap的对比:

  • resultType相对比较简单,只要根据查询结果自定义新POJO即可。
  • resultMap可以实现复杂数据的映射,例如集合对象。
  • resultMap可以实现延迟加载。

一对多查询

一对多查询,例如根据订单查询订单详情。因为一对多查询时,POJO中需要使用一个集合类型的属性来保存另一个表中的多条数据,无法实现结果字段到POJO属性的直接一一对应,需要使用resultMap来实现。

<resultMap id="OrderAndOrderDetailMap" type="com.tim.domain.Order">
    <id column="id" property="id"/>
    <result column="orderno" property="orderNo"/>
    <!-- collection是映射成集合,ofType指定集合所包含的类型。 -->
    <collection property="orderDetails" ofType="com.tim.domain.OrderDetail">
    	<!-- 同样的column是查询结果中的字段名,如果有多个同名,查询时需要取别名。property是集合所包含类型里的属性 -->
        <id column="ordersdetail_id" property="id"/>
        <result column="goods_id" property="goodsId"/>
        <result column="goods_num" property="goodsNum"/>
    </collection>
</resultMap>

<!-- 一对多,实际查询结果有多条,MyBatis会自动将同个Order.id的记录合并为一条,把多条记录里的OrderDetail根据Collection标签映射成集合。 -->
<select id="findOrderAndOrderDetail" resultMap="OrderAndOrderDetailMap" parameterType="int">
    select
      orders.id,
      user_id,
           orderno,
           ordersdetail.id ordersdetail_id,
           goods_id,
           goods_num
    from orders, ordersdetail where orders.id = ordersdetail.orders_id and orders.id = #{value}
</select>

collection和association可以互相嵌套,例如User(一)——Order(多)——(OrderDetail)(多),一名用户可以有多个订单,User中就包含List<Order>,同时单个订单中又有多个订单明细,Order包含List<OrderDetail>,resultMap的结构示例如下:

<resultMap>
	<id/>
	<result/>
	<collection>
		<collection></collection>
	</collection>
</resultMap>

使用resultType也可以实现,只不过要自己手动添加内嵌POJO的各个字段,同时因为会查出多条记录,要自己合并同个Order.id的记录。

根据实际需求选择resultType或者resultMap。如果最终结果是个列表,列表所含类型中没有POJO属性,则可以使用resultType。如果最终结果类型中包含了POJO属性,则使用resultMap的association(一对一)或collection(一对多)。

resultMap间可以相互继承。

延迟加载

resultMap中的association和collection支持延迟加载。

首先延迟加载要开启全局配置:

<configuration>
	<settings>
		<setting name="lazyLoadingEnabled" value="true"/>
		<setting name="aggressiveLazyLoading" value="false"/>
	</settings>
</configuration>

假设需要获取订单对应的用户信息:

    <resultMap id="OrderAndUserMap" type="com.tim.domain.Order">
        <id column="id" property="id"/>
        <result column="orderno" property="orderNo"/>
        <!-- association或collection,select表示查询懒加载数据所使用的select标签id, column表示查询结果中用来查询懒加载数据的字段名 -->
        <association property="user" javaType="com.tim.domain.User" select="findUserById" column="user_id"/>
    </resultMap>
    <!--主句中进行简单的单表查询,不进行关联查询或子查询 -->
    <select id="findOrdersAndUser" resultMap="OrderAndUserMap" parameterType="int">
        select *
        from orders
        where id = #{value};
    </select>
    <!-- 懒加载使用的select -->
    <select id="findUserById" resultType="User">
        select * from users where id = #{id}
    </select>
final Order order = sqlSession.selectOne("com.tim.mapper.UserMapper.findOrdersAndUser", 1);
// 调用关联查询的属性时才会发出查询
final User user = order.getUser();

10. 查询缓存

一级缓存

一级缓存是SqlSession级别的缓存。SqlSession对象中具有一个HashMap来存储缓存数据。不同SqlSession的缓存数据是不一样。

MyBatis会将SQL语句和对应的查询结果封装为key-value,当再次发起同样的请求时直接返回结果,提高响应速度,降低数据库压力。

如果对数据库进行了insert、delete、update时,一级缓存会清空,避免脏读。

二级缓存

二级缓存是Mapper级别的缓存。多个SqlSession操作同个Mapper的select语句,会保存到该Mapper下的HashMap,二级缓存是跨SqlSession的。注意实际上是按照namespace进行区分的,不同namespace的Mapper具有不同的二级缓存,相同namespace的Mapper的二级缓存是相同的。

与一级缓存一样,如果进行了增删改,则会清空二级缓存。

二级缓存的开启步骤:

  1. 开启全局配置
    <setting name="cacheEnabled" value="true" />
    
  2. Mapper
    <mapper namespace="">
    	<!-- 开启本namespace下的二级缓存 -->
    	<cache />
    </mapper>
    

二级缓存需要让对应的POJO类实现序列化接口,因为二级缓存可能在内存中,也可能在硬盘中。

SqlSessionclose()会把数据写到二级缓存中。

statement级别的禁用本句二级缓存:

<select useCache="false"></select>

MyBatis整合ehcache

ehcache是一个分布式缓存。分布式缓存包括redis、memcached、ehcache。使用分布式缓存,可以对缓存数据集中管理。

MyBatis中有个Cache接口,用来实现二级缓存功能。MyBatis中默认的实现类是PerpectualCache

整合ehcache,首先要导入相关库,然后在Mapper中指定cache:

<dependency>
  <groupId>org.mybatis.caches</groupId>
  <artifactId>mybatis-ehcache</artifactId>
  <version>1.1.0</version>
</dependency>
<mapper>
	<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
</mapper>

接着创建配置文件ehcache.xml放置到classpath中(可选)。

二级缓存的应用场景

对于访问较多,同时实时性要求不是很高的请求,或者耗时较高的统计分析等场景,可以使用二级缓存。此时设置一个刷新间隔flushInterval,到期后自动清空缓存,再进行查询。

二级缓存的局限性

MyBatis的二级缓存是Mapper级别的,当进行了某个语句的增删改时,其他查询的缓存都会被清空,导致需要重新查询。

11. Spring整合MyBatis

11.1 整合步骤

整体思路是通过Spring单例管理SqlSessionFactory,具体首先要在MyBatis库的基础上导入Spring及其他相关库:

<!-- Spring相关库 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>4.1.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.1.4.RELEASE</version>
</dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>4.1.4.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
	<version>4.1.4.RELEASE</version>
</dependency>
<!--mybatis-spring整合包 -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.0</version>
</dependency>
<!-- 线程池使用DBCP -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.5.0</version>
</dependency>

然后要在Spring中配置DPCP的dataSource:

<context:property-placeholder location="classpath:db.properties"/>
<bean id="datasource" class="org.apache.tomcat.dbcp.dbcp2.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数  -->
    <property name="acquireIncrement" value="5"/>
    <!-- 初始连接池大小 -->
    <property name="initialPoolSize" value="10"/>
    <!-- 连接池中连接最小个数 -->
    <property name="minPoolSize" value="5"/>
    <!-- 连接池中连接最大个数 -->
    <property name="maxPoolSize" value="20"/>
</bean>

注意,Spring中读取Properties文件,默认情况下系统变量会覆盖我们自定义的同名变量,例如username,导致数据库连接参数与实际配置参数不同,此时可以修改我们配置的变量名,或者加一层命名空间jdbc.username。或者设置system-properties-modeFALLBACKNEVER,参考Access denied for user ‘Administrator’@‘localhost’ (using password: YES)

11.2 Spring结合DAO

11.2.1 原生DAO

DAO实现类可以继承SqlSessionDaoSupport类,该类提供了SqlSessionFactory的注入,可以通过调用this.getSession()获取SqlSession,获取的SqlSession由Spring进行管理,无需手工调用sqlSession.close()关闭。

DAO的bean配置:

<context:property-placeholder location="classpath:db.properties"/>
<bean id="datasource" class="org.apache.tomcat.dbcp.dbcp2.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="datasource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>

<bean id="userDao" class="com.example.dao.UserDaoImpl">
	<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {

	@Override
	public User findUserById(int id) {
		final SqlSession sqlSession = this.getSqlSession();
		final User user = sqlSession.selectOne("org.tim.UserMapper.findUserById", 2);
		return user;
	}
}

11.2.2 Mapper代理

在原来Mapper代理的前提条件设置下,可以进行单个Mapper配置

<context:property-placeholder location="classpath:db.properties"/>
<bean id="datasource" class="org.apache.tomcat.dbcp.dbcp2.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="datasource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>

<!-- 单个Mapper -->
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
	<property name="mapperInterface" value="com.example.mapper.UserMapper"/>
	<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

<!-- 批量扫描包里的Mapper接口,自动创建代理对象,并在容器中注册。生成的bean id为Mapper接口的名字首字母小写 -->
<!-- XxxMapper.java和XxxMapper.xml需要同名同包。配置该项后MyBatis的package标签可以不用配置了。 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<!-- 多个包使用半角逗号分隔 -->
	<property name="basePackage" value="com.example.mapper"/>
	<!-- 注意此处使用sqlSessionFactoryBeanName,因为要等待datasource加载properties -->
	<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

或批量扫描包里的Mapper接口,自动创建代理对象,并在容器中注册。生成的bean id为Mapper接口的名字首字母小写

<context:property-placeholder location="classpath:db.properties"/>
<bean id="datasource" class="org.apache.tomcat.dbcp.dbcp2.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="datasource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>

<!-- XxxMapper.java和XxxMapper.xml需要同名同包。配置该项后MyBatis的package标签可以不用配置了。 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<!-- 多个包使用半角逗号分隔 -->
	<property name="basePackage" value="com.example.mapper"/>
	<!-- 注意此处使用sqlSessionFactoryBeanName,才不会在datasource前设置SqlSessionFactory -->
	<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

12. 逆向工程

MyBatis提供根据单表自动生成Mapper.java、Mapper.xml、VO等等。

mybatis-generator

13. 注解开发

首先还是需要配置sqlMapConfig.xml,因为注解开发消除了mapper.xml,所以只需要指定扫描包即可:

<?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>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://localhost/test?serverTimezone=UTC&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
                <property name="username" value="root"/>
                <property name="password" value=""/>
            </dataSource>
        </environment>
    </environments>
    <!-- 指定扫描包 -->
    <mappers>
        <package name="com.tim.dao"/>
    </mappers>
</configuration>

Dao:

public interface BrandDao {
	@Select("SELECT * FROM tdb_brands")
	List<Brand> findAllBrands();

	@Insert("INSERT INTO tdb_brands (brand_name) VALUES (#{brand_name})")
	void saveBrand(Brand brand);
	
	@UPDATE("UPDATE tdb_brands SET brand_name = #{brand_name}) WHERE brand_id = #{brand_id}")
	void updateBrand(Brand brand);
	
	@Delete("DELETE FROM tdb_brands WHERE id = #{id}")
	void deleteBrandById(int id);
}

有以下四个注解:

  • @Select:代替<select>
  • @Update:代替<update>
  • @Insert:代替<insert>
  • @Delete:代替<delete>

这四个注解的value为具体的SQL语句,占位符有两种

  • #{目标变量名}:对于基本数据类型,不管目标变量名为何,直接使用方法变量作为值,如果是字符串类型自动添加引号。对于POJO类型,则是会调用方法变量中目标变量名对应的getter方法。
  • ${目标变量名}:对于基本数据类型,需要使用value作为目标变量名,否则识别为POJO。对于POJO类型,会调用方法变量中目标变量名对应的getter方法。不是采用PreparedStatement的占位符方式,而是直接修改Statement,直接替换内容,所以如果是字符串不会自动添加引号。

13.1 一对一查询

以查询商品及所对应品牌的场景为例:

品牌表格:

CREATE TABLE `tdb_brands` (
  `brand_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `brand_name` varchar(40) NOT NULL DEFAULT '' COMMENT '所属品牌'
  PRIMARY KEY (`brand_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

品牌POJO:

public class Brand {
	private int brand_id;
	private String brand_name;
	// 省略getter setter
}

商品表格:

CREATE TABLE `tdb_goods` (
  `goods_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `goods_name` varchar(150) NOT NULL COMMENT '商品名称',
  `cate_id` smallint(5) unsigned NOT NULL,
  `brand_id` smallint(5) unsigned NOT NULL,
  `goods_price` decimal(15,3) unsigned NOT NULL DEFAULT '0.000',
  PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 

商品POJO:

public class Goods {
	private int goods_id;
	private String goods_name;
	private int cate_id;
	private int brand_id;
	private float goods_price;
	// 省略getter setter
}

提供一个商品+品牌的封装类:

public class GoodsWithBrand extends Goods {
    private Brand brand;
    // 省略getter setter
}

品牌DAO:

public interface BrandDao {
	@Select("SELECT * FROM tdb_brands WHERE brand_id = #{id}")
	Brand findBrandById(int id);
}

重点是商品DAO:

public interface GoodsDao {
	@Select("SELECT * FROM tdb_goods WHERE goods_id = #{id}")
	@Results({
			@Result(property = "goods_id", column = "goods_id", id = true),
			@Result(property = "goods_name", column = "goods_name"),
			@Result(property = "cate_id", column = "cate_id"),
			@Result(property = "brand_id", column = "brand_id"),
			@Result(property = "goods_price", column = "goods_price"),
			@Result(property = "brand", column = "brand_id", javaType = Brand.class,
					one = @One(select = "com.tim.dao.BrandDao.findBrandById",
							fetchType = FetchType.DEFAULT))
	})
	GoodsWithBrand selectGoodsWithBrandById(int id);
}

其中商品的查询SQL仍然使用@Select,对于品牌的一对一映射,需要配置@Results标签,里面接收一个@Result数组,@Result中的property对应结果POJO的字段,column对应查询结果的字段,使用id表示当前字段为主键。

对于结果POJO中的内嵌POJO,column指明商品表中的外键字段名,添加javaType属性指明具体类,添加one属性表示一对一关系,里面的select属性接收一个DAO方法的完整方法名,MyBatis会以外键字段的值作为参数调用这个select语句,查出嵌套的对象。另外还可以指定一个fetchType表示加载模式,DEFAULT为EAGER,即立即加载。LAZY为懒加载。

因为可以自动映射,所以可以省略非关键字段如下。因为column中的brand_id在最后映射给了内嵌POJO,如果省略了@Result(property = "brand_id", column = "brand_id"),则POJO中的brand_id就会为0。

public interface GoodsDao {
	@Select("SELECT * FROM tdb_goods WHERE goods_id = #{id}")
	@Results({
			@Result(property = "goods_id", column = "goods_id", id = true),
			@Result(property = "brand_id", column = "brand_id"),
			@Result(property = "brand", column = "brand_id", javaType = Brand.class,
					one = @One(select = "com.tim.dao.BrandDao.findBrandById",
							fetchType = FetchType.DEFAULT))
	})
	GoodsWithBrand selectGoodsWithBrandById(int id);
}

13.2 一对多查询

品牌表格:

CREATE TABLE `tdb_brands` (
  `brand_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `brand_name` varchar(40) NOT NULL DEFAULT '' COMMENT '所属品牌'
  PRIMARY KEY (`brand_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

品牌POJO:

public class Brand {
	private int brand_id;
	private String brand_name;
	// 省略getter setter
}

商品表格:

CREATE TABLE `tdb_goods` (
  `goods_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `goods_name` varchar(150) NOT NULL COMMENT '商品名称',
  `cate_id` smallint(5) unsigned NOT NULL,
  `brand_id` smallint(5) unsigned NOT NULL,
  `goods_price` decimal(15,3) unsigned NOT NULL DEFAULT '0.000',
  PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 

商品POJO:

public class Goods {
	private int goods_id;
	private String goods_name;
	private int cate_id;
	private int brand_id;
	private float goods_price;
	// 省略getter setter
}

提供一个品牌+商品的封装类:

public class BrandWithGoods extends Brand {
	private List<Goods> goods;
    // 省略getter setter
}

商品DAO:

public interface GoodsDao {
	@Select("SELECT * FROM tdb_goods WHERE brand_id = #{id}")
	List<Goods> selectGoodsByBrandId(int id);
}

重点是品牌DAO:

public interface BrandDao {
	@Select("SELECT * FROM tdb_brands WHERE brand_id = #{id}")
	@Results({
			@Result(property = "brand_id", column = "brand_id", id = true),
			@Result(property = "goods", column = "brand_id", javaType = List.class,
			many = @Many(select="com.tim.dao.GoodsDao.selectGoodsByBrandId"))
	})
	BrandWithGoods selectBrandWithGoodsById(int id);
}

与一对一不同的是,one改成了manyjavaType改成了List.class,实质上思想还是根据一个字段值调用另一个查询方法,把结果映射到对应的字段上。

14. 以Map为输入参数

void updateTypes(@Param("types") Map types);

collection以types.key为遍历目标,也可以遍历types.value。遍历key取value时,使用#{types[${key}]}q为value值。

<update id="updateTypes" parameterType="map">
  <foreach collection="types.keys" item="key" index="index">
    update shoes_info set content = #{types[${key}]} where type = #{key};
  </foreach>
</update>

15. 以Map为输出参数

@MapKey("date")
Map<String, CustomObject> query();
<select id="query" resultType="com.example.CustomObject">
    select val1, val2 from my_table
</select>

select语句指定resultType或resultMap,接口层添加@MapKey注解,value的值为CustomObject的一个属性,以这个属性为key,把返回的数据列表转为map。

16. Spring + MyBatis + Druid 批量执行语句

首先jdbc的url需要加上allowMultiQueries=true参数。

然后druid如果配置了wall-filter,需要配置multiStatementAllownoneBaseStatementAllow参数:

<bean id="wallFilter" class="com.alibaba.druid.wall.WallFilter">
    <property name="dbType" value="mysql"/>
    <property name="config" ref="wallConfig"/>
</bean>
<bean id="wallConfig" class="com.alibaba.druid.wall.WallConfig">
    <property name="multiStatementAllow" value="true"/>
    <property name="noneBaseStatementAllow" value="true"/>
</bean>

DruidDataSource再设置proxyFilters为我们自定义的Filter:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${db.driverClassName}"/>
    <property name="url" value="${db.url}"/>
    <property name="username" value="${db.username}"/>
    <property name="password" value="${db.password}"/>
    <property name="initialSize" value="3"/>
    <property name="minIdle" value="3"/>
    <property name="maxActive" value="20"/>
    <property name="maxWait" value="60000"/>
    <!--        <property name="filters" value="stat,wall"/>-->
    <property name="proxyFilters">
        <list>
            <ref bean="statFilter"/>
            <ref bean="wallFilter"/>
        </list>
    </property>
</bean>

参考:使用Druid批量更新报错解决方案

17. JSON字段自动转换

参考:Mybatis优雅存取json字段的解决方案 - TypeHandler

步骤

  1. 创建类继承自BaseTypeHandler,指定泛型类,实现字段转换的逻辑。
  2. 把实体类中对应成员变量的类型从String改为JSON对应的实体类型。
  3. 设置mybatis的type-handler的扫描包或具体类,例如yybatisSqlSessionFactoryBean.setTypeHandlersPackage("")yybatisSqlSessionFactoryBean.setTypeHandlers()
public class ListTypeHandler extends BaseTypeHandler<List<Test>> {

    private static final Logger LOGGER = LoggerFactory.getLogger(ListTypeHandler.class);
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final TypeReference<List<T>> TYPE_REFERENCE = new TypeReference<List<T>>() {
    };

    /**
     * 根据参数设置字段值
     */
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int columnIndex, List<Test> list, JdbcType jdbcType) throws SQLException {
        if (CollectionUtils.isEmpty(list)) {
            preparedStatement.setString(columnIndex, "[]");
        } else {
            try {
                String json = OBJECT_MAPPER.writeValueAsString(list);
                preparedStatement.setString(columnIndex, json);
            } catch (JsonProcessingException e) {
                LOGGER.error("序列化失败", e);
                throw new ServiceException("序列化失败");
            }
        }
    }

    /**
     * 根据字段名获取实体值
     */
    @Override
    public List<Test> getNullableResult(ResultSet resultSet, String columnLabel) throws SQLException {
        String json = resultSet.getString(columnLabel);
        if (StringUtils.isBlank(json)) {
            return new ArrayList<>();
        }
        try {
            return OBJECT_MAPPER.readValue(json, TYPE_REFERENCE);
        } catch (IOException e) {
            LOGGER.error("反序列化失败", e);
            throw new ServiceException("反序列化失败");
        }
    }

    /**
     * 根据字段下标获取实体值
     */
    @Override
    public List<Test> getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
        String json = resultSet.getString(columnIndex);
        try {
            return OBJECT_MAPPER.readValue(json, TYPE_REFERENCE);
        } catch (IOException e) {
            LOGGER.error("反序列化失败", e);
            throw new ServiceException("反序列化失败");
        }
    }

    /**
     * 根据字段下标获取实体值
     */
    @Override
    public List<Test> getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
        String json = callableStatement.getString(columnIndex);
        try {
            return OBJECT_MAPPER.readValue(json, TYPE_REFERENCE);
        } catch (IOException e) {
            LOGGER.error("反序列化失败", e);
            throw new ServiceException("反序列化失败");
        }
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值