Mybatis应用分析与最佳实践
1. 为什么使用Mybatis
JDBC连接数据库需要五步:
首先在pom.xml中引入MySQL驱动的依赖
第一步:Class.forName注册驱动
第二步:获取一个Connection对象
第三步:创建一个Statement对象
第四步:execute()方法执行SQL。返回一个ResultSet结果集
第五步:通过ResultSet获取数据,给POJO的属性赋值
最后关闭数据库相关的资源,包括ResultSet,Statement,Connection。
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("");
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SQL");
while(rs.next()) {
int id = rs.getInt("b_id");
}
缺点:
- 代码重复
- 资源管理
- 结果集处理
- SQL耦合
DbUtils和Spring JDBC
Apache 2003年发布Commons DbUtils工具类,优化对数据库的操作
Spring对原生的JDBC进行封装
- 代码重复:spring提供模板方法JDBCTemp,里面封装了各种各样的execute、query、update方法.
JDBCTemp:它是JDBC的核心包的中间类。简化了JDBC的使用,可以避免常见异常。封装了JDBC的核心流程,是线程安全的。 - 对于结果集的处理,SpringJDBC提供了RowMapper接口,可以把结果集转换成Java对象,它作为JDBCTemplate的参数使用。例如要把查询的对象换成Attachement对象,可以针对一个Attachement创建一个RowMapper对象:
class AttachmentRowRoMapper implements RowMapper{
@Override
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
Attachment attachment = new Attachment();
attachment.setAttachmentId(rs.getLong("is"));
return attachment;
}
}
如果项目的表比较多的时候,创建一个支持泛型的BaseRowMapper实现RowMapper接口,通过反射的方法自动获取所有的属性,把表字段赋值到属性。
class BaseRowMapper<T> implements RowMapper<T> {
private Class<?> targetClazz;
private HashMap<String, Field> fieldMap;
public BaseRowMapper(Class<?> targetClazz) {
this.targetClazz = targetClazz;
fieldMap = new HashMap<>();
Field[] fields = this.targetClazz.getDeclaredFields();
for (Field field : fields) {
fieldMap.put(field.getName(), field);
}
}
@Override
public T mapRow(ResultSet rs, int rowNum) throws SQLException {
T obj = null;
try {
obj = (T) this.targetClazz.newInstance();
ResultSetMetaData metaData = rs.getMetaData();
int columnLength = metaData.getColumnCount();
String columnName = null;
for (int i = 0; i < columnLength; i++) {
columnName = metaData.getColumnName(i);
Class fieldClazz = fieldMap.get(camel(columnName)).getType();
Field field = fieldMap.get(camel(columnName));
field.setAccessible(true);
if (fieldClazz == int.class || fieldClazz == Integer.class) {
field.set(obj,rs.getInt(columnName));
} else if (fieldClazz == boolean.class || fieldClazz == Boolean.class){
}
field.setAccessible(false);
}
} catch (Exception e) {
}
return obj;
}
//驼峰转换,将下划线转变成驼峰
private String camel(String str){
Pattern pattern = Pattern.compile("_(\\w)");
Matcher matcher = pattern.matcher(str);
StringBuffer sb = new StringBuffer(str);
if (matcher.find()) {
sb = new StringBuffer();
matcher.appendReplacement(sb,matcher.group(1).toUpperCase());
matcher.appendTail(sb);
} else {
return sb.toString();
}
return camel(sb.toString());
}
}
JDBC测试:
list= jdbcTemplate.query("select* from tbl_emp", new BaseRowMapper(Employee.class));
DbUtils和Spring Jdbc,这两个对JDBC做了轻量级封装的框架(工具类),解决了:
- 对操作数据的增删改查的方法进行封装。
- 无论是QueryRunner(DbUtils核心方法)还是JDBCTemplate,都可以传入一个数据源初始化,也就是资源管理这一部分的事情,可以交给专门的数据源组件去做,不用手段创建和关闭。
- 可以帮组我们映射结果集,无论是映射成List、Map还是POJO。
这两个工具存在一些不足:
- SQL语句都是写在代码里的,存在硬编码问题。
- 参数只能按照固定位置的顺序传入,它是通过占位符去替换的,不能传入对象和Map,不能自动映射。
- 在方法里面,可以把结果集映射成实体类,但是不能直接把实体类映射成数据录的记录(没有自动生产SQL的功能)。
- 查询没有缓存的功能,性能不够好。
Hibernate
ORM的全拼:Object Relational Mapping,也就是对象与关系的映射。对象是程序里的对象,关系是它与数据库里面的数据关系。
Configuration configuration = new Configuration();
configuration.configure();
//创建Session工厂
SessionFactory factory = configuration.buildSessionFactory();
//创建Session
Session session = factory.openSession();
//获取事务对象
Transaction transaction = session.getTransaction();
//开启事务
transaction.begin();
//数据操作
session.save("");
//提交事务
transaction.commit();
//关闭Session
session.close();
Hibernate特性
- 根据数据库方言自动生成SQL,移植性好;
- 自动管理连接资源(支持数据源);
- 实现了对象和关系型数据的完全映射,操作对象就像操作数据库一样;
- 提供了缓存功能机制。
但是Hibernate在业务复杂的项目中使用存在一些问题:
- 使用get(), update(), save()对象的这种方法,实际操作的所有字段,没有办法指定部分字段(不灵活)
- 自动生成SQL的方式,如果要基于SQL去做一些优化的话,是非常困难的
- 不支持动态SQL,比如分表中的表明、条件、参数变化,无法根据条件自动生成SQL。
MyBatis
“半自动化”的ORM框架,不会自动生成全部的SQL语句,主要解决的是SQL和对象的映射关系。SQL和代码是分离的。
2. MyBatis实际使用案例
编程式使用
MyBatis有两种配置文件:全局式配置文件和映射器配置文件
创建一个全局配置文件,这里面是对MyBatis的核心行为的控制,例如:myBatis-config.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>
<properties resource="db.properties"></properties>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
<!--
<setting name="localCacheScope" value="STATEMENT"/>
-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<typeAliases>
<typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
</typeAliases>
<!-- <typeHandlers>
<typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
</typeHandlers>-->
<!-- 对象工厂 -->
<!-- <objectFactory type="com.gupaoedu.objectfactory.GPObjectFactory">
<property name="gupao" value="666"/>
</objectFactory>-->
<!-- <plugins>
<plugin interceptor="com.gupaoedu.interceptor.SQLInterceptor">
<property name="gupao" value="betterme" />
</plugin>
<plugin interceptor="com.gupaoedu.interceptor.MyPageInterceptor">
</plugin>
</plugins>-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
<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>
<mappers>
<mapper resource="BlogMapper.xml"/>
<mapper resource="BlogMapperExt.xml"/>
</mappers>
</configuration>
db.properties:数据源一般交给spring管理
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true
jdbc.username=root
jdbc.password=123456
映射器配置文件:Mapper.xml
<?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">
<mapper namespace="com.gupaoedu.mapper.BlogMapper">
<!-- 声明这个namespace使用二级缓存 -->
<!-- <cache/>-->
<!-- 使用Redis作为二级缓存 -->
<!--
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
-->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
<resultMap id="BaseResultMap" type="blog">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<!--
<result column="name" property="name" jdbcType="VARCHAR" typeHandler="com.gupaoedu.type.MyTypeHandler"/>
-->
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="author_id" property="authorId" jdbcType="INTEGER"/>
</resultMap>
<!-- 根据文章查询作者,一对一查询的结果,嵌套查询 -->
<resultMap id="BlogWithAuthorResultMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<!-- 联合查询,将author的属性映射到ResultMap -->
<association property="author" javaType="com.gupaoedu.domain.Author">
<id column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
</association>
</resultMap>
<!-- 另一种联合查询(一对一)的实现,但是这种方式有“N+1”的问题 -->
<resultMap id="BlogWithAuthorQueryMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.gupaoedu.domain.Author"
column="author_id" select="selectAuthor"/> <!-- selectAuthor 定义在下面-->
</resultMap>
<!-- 查询文章带评论的结果(一对多) -->
<resultMap id="BlogWithCommentMap" type="com.gupaoedu.domain.associate.BlogAndComment" extends="BaseResultMap" >
<collection property="comment" ofType="com.gupaoedu.domain.Comment">
<id column="comment_id" property="commentId" />
<result column="content" property="content" />
</collection>
</resultMap>
<!-- 按作者查询文章评论的结果(多对多) -->
<resultMap id="AuthorWithBlogMap" type="com.gupaoedu.domain.associate.AuthorAndBlog" >
<id column="author_id" property="authorId" jdbcType="INTEGER"/>
<result column="author_name" property="authorName" jdbcType="VARCHAR"/>
<collection property="blog" ofType="com.gupaoedu.domain.associate.BlogAndComment">
<id column="bid" property="bid" />
<result column="name" property="name" />
<result column="author_id" property="authorId" />
<collection property="comment" ofType="com.gupaoedu.domain.Comment">
<id column="comment_id" property="commentId" />
<result column="content" property="content" />
</collection>
</collection>
</resultMap>
<!-- ===============以上是resultMap定义================= -->
<select id="selectBlogById" resultMap="BaseResultMap" statementType="PREPARED" >
select * from blog where bid = #{bid}
</select>
<!-- $只能用在自定义类型和map上 -->
<select id="selectBlogByBean" parameterType="blog" resultType="blog" >
select bid, name, author_id authorId from blog where name = '${name}'
</select>
<select id="selectBlogList" resultMap="BaseResultMap" >
select bid, name, author_id authorId from blog
</select>
<!-- 动态SQL where 和 if -->
<select id="selectBlogListIf" parameterType="blog" resultMap="BaseResultMap" >
select bid, name, author_id authorId from blog
<where>
<if test="bid != null">
AND bid = #{bid}
</if>
<if test="name != null and name != ''">
AND name LIKE '%${name}%'
</if>
<if test="authorId != null">
AND author_id = #{authorId}
</if>
</where>
</select>
<!-- 动态SQL choose -->
<select id="selectBlogListChoose" parameterType="blog" resultMap="BaseResultMap" >
select bid, name, author_id authorId from blog
<where>
<choose>
<when test="bid !=null">
bid = #{bid, jdbcType=INTEGER}
</when>
<when test="name != null and name != ''">
AND name LIKE CONCAT(CONCAT('%', #{name, jdbcType=VARCHAR}),'%')
</when>
<when test="authorId != null ">
AND author_id = #{authorId, jdbcType=INTEGER}
</when>
<otherwise>
</otherwise>
</choose>
</where>
</select>
<!-- 动态SQL set -->
<update id="updateByPrimaryKey" parameterType="blog">
update blog
<set>
<if test="name != null">
name = #{name,jdbcType=VARCHAR},
</if>
<if test="authorId != null">
author_id = #{authorId,jdbcType=CHAR},
</if>
</set>
where bid = #{bid,jdbcType=INTEGER}
</update>
<!-- 动态SQL trim -->
<insert id="insertBlog" parameterType="blog">
insert into blog
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="bid != null">
bid,
</if>
<if test="name != null">
name,
</if>
<if test="authorId != null">
author_id,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="bid != null">
#{bid,jdbcType=INTEGER},
</if>
<if test="name != null">
#{name,jdbcType=VARCHAR},
<!-- #{name,jdbcType=VARCHAR,typeHandler=com.gupaoedu.type.MyTypeHandler}, -->
</if>
<if test="authorId != null">
#{authorId,jdbcType=INTEGER},
</if>
</trim>
</insert>
<!-- foreach 动态SQL 批量插入 -->
<insert id="insertBlogList" parameterType="java.util.List">
insert into blog (bid, name, author_id)
values
<foreach collection="list" item="blogs" index="index" separator=",">
( #{blogs.bid},#{blogs.name},#{blogs.authorId} )
</foreach>
</insert>
<!-- foreach 动态SQL 批量删除 -->
<delete id="deleteByList" parameterType="java.util.List">
delete from blog where bid in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.bid,jdbcType=INTEGER}
</foreach>
</delete>
<!-- foreach 动态SQL 批量更新-->
<update id="updateBlogList">
update blog set
name =
<foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
when #{blogs.bid} then #{blogs.name}
</foreach>
,author_id =
<foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
when #{blogs.bid} then #{blogs.authorId}
</foreach>
where bid in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.bid,jdbcType=INTEGER}
</foreach>
</update>
<!-- 根据文章查询作者,一对一,嵌套结果,无N+1问题 -->
<select id="selectBlogWithAuthorResult" resultMap="BlogWithAuthorResultMap" >
select b.bid, b.name, b.author_id, a.author_id , a.author_name
from blog b
left join author a
on b.author_id=a.author_id
where b.bid = #{bid, jdbcType=INTEGER}
</select>
<!-- 根据文章查询作者,一对一,嵌套查询,存在N+1问题,可通过开启延迟加载解决 -->
<select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" >
select b.bid, b.name, b.author_id, a.author_id , a.author_name
from blog b
left join author a
on b.author_id=a.author_id
where b.bid = #{bid, jdbcType=INTEGER}
</select>
<!-- 嵌套查询 -->
<select id="selectAuthor" parameterType="int" resultType="com.gupaoedu.domain.Author">
select author_id authorId, author_name authorName
from author where author_id = #{authorId}
</select>
<!-- 根据文章查询评论,一对多 -->
<select id="selectBlogWithCommentById" resultMap="BlogWithCommentMap" >
select b.bid, b.name, b.author_id authorId, c.comment_id commentId, c.content
from blog b, comment c
where b.bid = c.bid
and b.bid = #{bid}
</select>
<!-- 根据作者文章评论,多对多 -->
<select id="selectAuthorWithBlog" resultMap="AuthorWithBlogMap" >
select b.bid, b.name, a.author_id authorId, a.author_name authorName, c.comment_id commentId, c.content
from blog b, author a, comment c
where b.author_id = a.author_id and b.bid = c.bid
</select>
<!-- 手动实现翻页,没有对应方法,取消注释会报错 -->
<!-- <select id="selectBlogPage" parameterType="map" resultMap="BaseResultMap">
select * from blog limit #{curIndex} , #{pageSize}
</select>-->
<!-- 自动生成的Example -->
<sql id="Base_Column_List">
bid, name, author_id
</sql>
<sql id="Example_Where_Clause">
<where>
<foreach collection="oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
<select id="selectByExample" parameterType="com.gupaoedu.domain.BlogExample" resultMap="BaseResultMap">
select
<if test="distinct">
distinct
</if>
'true' as QUERYID,
<include refid="Base_Column_List" />
from blog
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
</mapper>
MyBatis的目的是简化JDBC的操作,它提供了一个可以执行CRUD的对象:SqlSession接口。可以把它理解为跟数据库的一个连接,或者一次会话。
SqlSession的创建:
因为数据源、Mybatis核心行为的控制(例如是否开启缓存)都是在全局配置文件中,所以必须根据全局配置文件创建,这里不是直接new出来的,而是通过一个工厂类创建的。
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
Blog blog = (Blog) session.selectOne("com.gupaoedu.mapper.BlogMapper.selectBlogById", 1);
System.out.println(blog);
} finally {
session.close();
}
最后通过SqlSession接口上的方法传入Statement Id来执行Mapper映射器中的SQL。这样的调用方式解决了重复代码、资源管理、SQL耦合、结果集映射这几个问题。
不过仍然存在一些问题:
1. Statement Id是硬编码,维护不方便。
2. 不能在编译时进行类型检查,如果namespace或者Statement Id出错,只在运行时报错。
优化:定义一个Mapper接口的方式,这个接口全路径必须跟Mapper.xml里面的namespace对应起开,方法也要跟Statement Id 一一对应。
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try{
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogByName("MySQL从入门到改行");
} finally{
session.close;
}
MyBatis的核心特性(Mybatis可以解决哪些主要问题):
- 使用连接池对连接进行管理
- SQL和代码分离,集中管理
- 结果集映射
- 参数映射和动态SQL
- 重复SQL的提取
- 缓存管理
- 插件机制
核心对象的生命周期
在编程式的demo中,我们发现Mybatis的几个核心对象:SQLSessionFactoryBuiler、SQLSessionFactory、SqlSession、Mapper。在一些分布式的应用里面,多线程高并发的场景中如果要写出高效的代码,必须了解这4个对象的生命周期。
1. SqlSessionFactoryBuiler
SqlSessionFactoryBuiler是用来创建SqlSessionFactory的。而SqlSessionFactory只需要一个,所有只要创建了一个SqlSessionFactory,它的使命就完成了,也就没有存在的意义,所以它的生命周期只存在于方法的内部。
2. SqlSessFactory(单例)
SqlSessionFactory是用来创建Session的,每次应用程序访问数据库都需要创建一个会话。因为我们一直友会话的需要,所以SqlSessionFactory应该存在于整个生命周期中(作用域是应用作用域)。创建SqlSession只需要一个实例来做这件事就行了。否则会产生混乱和浪费资源。所以采用单例模式。
3. SqlSession
SqlSession是一个会话,不是线程安全的,不能在线程间共享。所以在请求开始的时候创建一个SqlSession对象,在请求结束或方法执行完毕的时候要及时关闭它。
4. Mapper
Mapper(实际上是一个代理对象)是从SqlSession中获取的。它的做用是发送SQL来操作数据库的数据,它应该在一个SqlSession事务方法之内。
3.核心配置解读
中文官网:https://mybatis.org/mybatis-3/zh/index.html
全局配置文件mybatis-config.xml
configuration
configuration是整个配置文件的根标签,实际上也对应着MyBatis里面最重要的配置类Configuration。
properties
第一个一级标签,用来配置参数信息,比如最常见的数据库连接信息。可以用resource引用应用里面的相对路径,也可以用url指定本地服务器或者网络的绝对路径
settings
settings里面是MyBatis的一些核心配置。
typeAliases
typeAlias是类型的别名,只要用来简化类名全路径的拼写。例如
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
</typeAliases>
MyBatis里面有系统预定好的类型别名,在TypeAliasRegistry中。
typeHandlers
由于Java类型和数据库的JDBC类型不是一一对应的(比如String与varchar、char、text),所以我们把java对象转换为数据库的值,把数据库的值转换成Java对象,需要经过一定的转换这两个方向的转换需要用到TypeHandler。
MyBatis内置了很多的TypeHandler(type包下)它们全部注册在TypeHandlerRegistry中,它们都继承了抽象类BaseTypeHandler,泛型就是要处理的Java数据类型。
自定义TypeHandler时候,需要继承BaseTypeHandler。有4个抽象方法必须实现。
从Java类型到JDBC类型
setNonNullParameter:设置非空参数
从JDBC类型到Java类型
getNullableResult:获取空结果集(根据列名),一般调用这个
getNullableResult:获取空结果集(根据下标值)
getNullableResult:存储过程用的
举例:数据库保存使用逗号分类的字符串:例如“1,2,4”,而返回给程序的时候是整形数组{1,2,4}。
第一步:自定义typeHandler
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Integer[] parameter, JdbcType jdbcType) throws SQLException {
// 设置 Integer[] 类型的参数的时候调用,Java类型到JDBC类型
// 注意只有在字段上添加typeHandler属性才会生效
StringJoiner sj = new StringJoiner(",");
for (int j = 0; j < parameter.length; j++) {
sj.add(parameter[i].toString());
}
ps.setString(i,sj.toString());
}
@Override
public Integer[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
String str = rs.getString(columnName);
String[] parameter = str.split(",");
Integer [] param = new Integer[parameter.length];
for (int i = 0; i < parameter.length; i++) {
param[i] = Integer.parseInt(parameter[i]);
}
return param;
}
第二步:注册TypeHandler
<typeHandlers>
<typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
</typeHandlers>
第三步:配置Mapper.xml
<resultMap id="BaseResultMap" type="blog">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR" typeHandler="com.gupaoedu.type.MyTypeHandler"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="author_id" property="authorId" jdbcType="INTEGER"/>
</resultMap>
在插入时实现自定义TypeHandler
<insert id="insert" parameterType="com.lhl.mybatis.beans.Test" useGeneratedKeys="true" keyProperty="id">
insert into test(id, nums, name)
values (#{id,jdbcType=INTEGER},#{nums,jdbcType=INTEGER},#{name,jdbcType=VARCHAR,javaType=java.lang.String})
</insert>
等价于
<insert id="insert" parameterType="com.lhl.mybatis.beans.Test" useGeneratedKeys="true" keyProperty="id">
insert into test(id, nums, name)
values (#{id,jdbcType=INTEGER},#{nums,jdbcType=INTEGER},#{name,typeHandler=com.lhl.mybatis.typehandler.CustomTypeHandler})
</insert>
objectFactory
当我们把数据库返回的结果集转换成实体类的时候,需要创建对象的实例。由于不知道处理的类型是什么,有那些属性,所以不能使用new的方式去创建,只能通过反射来创建。
在MyBatis中,它提供了一个工厂类接口,叫做ObjectFactory,专门用来创建对象的实例
plugins
插件是MyBatis的一个很强大的机制,跟许多其他的框架一样,MyBatis预留了插件的接口,让Mybatis更容易扩展。
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor(update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler(getParameterObject, setParamters)
- ResultSetHandler(handleResultSets, handleOutPutParameters)
- StatementHandler(prepare, parameterize, batch, update, query)
SqlSession是对外提供的接口。而SQLSession的CRUD的方法都是由Executor完成的
Execute是真正的执行器角色,也是实际的SQL逻辑执行的开始。而MyBatis中又把SQL的执行按照过程细分为三个对象:parameterHandler处理参数,StatementHandler执行SQL,ResultSetHandler处理结果集。
environments、environment
environments标签用来管理数据库的环境,比如开发环境、测试环境、生产环境的数据库。
一个environment标签就是一个数据源,代表一个数据库,这里有两个关键的标签,一个是事务管理器,一个是数据源
transactionManager
JDBC:使用Connection对象的commit(), rollback(), close()管理事务。
MANAGED:把事务交给容器管理,比如JBOSS,Weblogic。
如果是Spring+Mybatis,没有必要配置,因为在applicationContext.xml里面配置数据源和事务,覆盖MyBatis的配置
dataSource
数据源,在Java里面,它是对数据库连接的一个抽象。
一般的数据源都会包括连接池管理的功能,所以dataSource又叫做连接池,或带连接池功能的数据源。
一般连接池都会有初始连接数、最大连接数、回收时间等参数,提供提前创建、资源重用、数量控制、超时管理等功能。
MyBatis自带两种数据源:UNPOOLED和POOLED。也可以配置成其他数据库,比如C3P0,、Hikari等。
mappers
配置的是映射器,也就是Mapper.xml的路径。这里配置的目的是让MyBatis在启动的时候去扫描这些映射器,创建映射关系。
4种指定Mapper文件的方式:
- 使用相对于类路径的资源引用(resource)
<mappers>
<mapper resource="BlogMapper.xml"/>
</mappers>
- 使用完全限定资源定位符(绝对路径)
<mappers>
<mapper url="file:src/main/resources/BlogMapper.xml"/>
</mappers>
- 使用映射器接口实现类的完全限定类名
<mappers>
<mapper class="com.gupaoedu.mapper.BlogMapper"/>
</mappers>
- 将包内的映射器接口实现全部注册为映射器(最常用)
<mappers>
<mapper class="com.gupaoedu.mapper"/>
</mappers>
映射器配置文件 Mapper.xml
映射器最主要的配置了SQL语句,也解决了参数映射和结果集映射的问题一共八个标签
cache
给定命名空间的缓存配置(是否开启二级缓存)
cache-ref
其他命名空间的缓存配置的引用。
resultMap
用来描述如何从数据库结果集来加载对象
sql
可被其他语句引用的可重用语句块
CRUD标签
insert、update、delete、select
4. MyBatis最佳实践
动态SQL
MyBatis动态SQL的动态标签主要有4类:if,choose(when, otherwise),trim(where, set),foreach
if——需要判断的时候,条件写在test中
<!-- 动态SQL where 和 if -->
<select id="selectBlogListIf" parameterType="blog" resultMap="BaseResultMap" >
select bid, name, author_id authorId from blog
<where>
<if test="bid != null">
AND bid = #{bid}
</if>
<if test="name != null and name != ''">
AND name LIKE '%${name}%'
</if>
<if test="authorId != null">
AND author_id = #{authorId}
</if>
</where>
</select>
choose(when, otherwise)——需要选择一个条件的时候
<!-- 动态SQL choose -->
<select id="selectBlogListChoose" parameterType="blog" resultMap="BaseResultMap" >
select bid, name, author_id authorId from blog
<where>
<choose>
<when test="bid !=null">
bid = #{bid, jdbcType=INTEGER}
</when>
<when test="name != null and name != ''">
AND name LIKE CONCAT(CONCAT('%', #{name, jdbcType=VARCHAR}),'%')
</when>
<when test="authorId != null ">
AND author_id = #{authorId, jdbcType=INTEGER}
</when>
<otherwise>
</otherwise>
</choose>
</where>
</select>
trim(where, set)——需要去掉where、and、逗号之类的符号的时候
<!-- 动态SQL trim -->
<insert id="insertBlog" parameterType="blog">
insert into blog
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="bid != null">
bid,
</if>
<if test="name != null">
name,
</if>
<if test="authorId != null">
author_id,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="bid != null">
#{bid,jdbcType=INTEGER},
</if>
<if test="name != null">
#{name,jdbcType=VARCHAR},
<!-- #{name,jdbcType=VARCHAR,typeHandler=com.gupaoedu.type.MyTypeHandler}, -->
</if>
<if test="authorId != null">
#{authorId,jdbcType=INTEGER},
</if>
</trim>
</insert>
foreach——需要遍历集合的时候
<!-- foreach 动态SQL 批量插入 -->
<insert id="insertBlogList" parameterType="java.util.List">
insert into blog (bid, name, author_id)
values
<foreach collection="list" item="blogs" index="index" separator=",">
( #{blogs.bid},#{blogs.name},#{blogs.authorId} )
</foreach>
</insert>
<!-- foreach 动态SQL 批量删除 -->
<delete id="deleteByList" parameterType="java.util.List">
delete from blog where bid in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.bid,jdbcType=INTEGER}
</foreach>
</delete>
<!-- foreach 动态SQL 批量更新-->
<update id="updateBlogList">
update blog set
name =
<foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
when #{blogs.bid} then #{blogs.name}
</foreach>
,author_id =
<foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
when #{blogs.bid} then #{blogs.authorId}
</foreach>
where bid in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.bid,jdbcType=INTEGER}
</foreach>
</update>
Batch Execute
MyBatis的动态标签批量操作存在一定的缺点,比如数据量特别大的时候,拼接出的SQL语句过大。
MySQL的服务端对于接受的数据包有大小限制,max_allowed_packet默认是4M,需要修改默认配置或者手动地控制条数,才可以解决。
在全局配置文件中,可以配置默认的Executor的类型(默认是SIMPLE)。还有一种BatchExecutor。
<setting name="defaultExecutorType" value="BATCH" />
也可以在创建会话的时候指定执行器的类型
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
BaseExecutor的三个子类:BatchExecutor、ResultExecutor、SimpleExecutor
三种类型执行器的区别(通过doUpdate()方法对比)
- SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。(默认)
- ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement,而是放置在Map内。供下一次使用。简而言之,就是重复使用statement对象。
- BatchExecutor:执行update(没有select,JDBC批量处理不支持select),将所有的sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
executeUpdate()是一个语句访问一次数据库,executeBatch()是一批语句访问一次数据库(具体一批发送多少条SQL跟服务端的max_allowed_packet有关)。
BatchExecutor底层是对JDBC ps.addBatch()和ps.executeBatch()的封装。
嵌套(关联)查询/ N+1 / 延迟加载
association:是用于一对一和多对一
collectin:用于一对多
一对一的关联查询的两种配置方式:
在Blog(文章)里面包好一个Author(作者)对象。
- 嵌套结果
<!-- 根据文章查询作者,一对一查询的结果,嵌套查询 -->
<resultMap id="BlogWithAuthorResultMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<!-- 联合查询,将author的属性映射到ResultMap -->
<association property="author" javaType="com.gupaoedu.domain.Author">
<id column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
</association>
</resultMap>
<!-- 根据文章查询作者,一对一,嵌套结果,无N+1问题 -->
<select id="selectBlogWithAuthorResult" resultMap="BlogWithAuthorResultMap" >
select b.bid, b.name, b.author_id, a.author_id , a.author_name
from blog b
left join author a
on b.author_id=a.author_id
where b.bid = #{bid, jdbcType=INTEGER}
</select>
- 嵌套查询
<!-- 另一种联合查询(一对一)的实现,但是这种方式有“N+1”的问题 -->
<resultMap id="BlogWithAuthorQueryMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.gupaoedu.domain.Author"
column="author_id" select="selectAuthor"/> <!-- selectAuthor 定义在下面-->
</resultMap>
<!-- 根据文章查询作者,一对一,嵌套查询,存在N+1问题,可通过开启延迟加载解决 -->
<select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" >
select b.bid, b.name, b.author_id, a.author_id , a.author_name
from blog b
left join author a
on b.author_id=a.author_id
where b.bid = #{bid, jdbcType=INTEGER}
</select>
<!-- 嵌套查询 -->
<select id="selectAuthor" parameterType="int" resultType="com.gupaoedu.domain.Author">
select author_id authorId, author_name authorName
from author where author_id = #{authorId}
</select>
嵌套查询是分两次查询的,当我们查询了Blog信息后,会再发送一条SQL语句到数据库查询作者信息。 存在N+1问题。
在MyBatis中可以通过开启延迟加载的开关来解决这个问题。
在Settings标签里面可以配置
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
开启延时加载后,调用blog.getAuthor()以及默认的equals、clone、hashCode、toString 方法时才会发起第二次查询。
触发延迟加载的方法可以通过lazyLoadTriggerMethods标签配置。如果开启了aggressiveLazyLoading=true,除了关联查询的方法和系统方法,其他方法也会触发第二次查询,比如blog.getName()。
翻页
翻页分为逻辑翻页和物理翻页
逻辑翻页(假翻页):把所有的数据查出来,在内存中删选数据。
物料翻页(真翻页):MySQL使用limit语句。Oracle使用rownum语句。SQLServer使用top语句。
通用Mapper
MyBatis常见问题
用注解还是xml配置
常用注解:@Insert、@Select、@Update、@Delete、@Param、@Results、@Result
注解的缺点是SQL无法集中管理,复杂的SQL很难配置,所有在业务复杂的项目中只使用XML配置的形式,业务简单的项目中可以使用注解和XML混合的形式。
Mapper接口无法注入或Invalid bound statument (not found)
遇到这中情况需要检查一下地方:
1. 扫面配置,xml文件和Mapper接口有没有被扫描到
2. namespace的值可接口全类名一致。
3. 检查对应的sql语句ID是否存在。
怎么获取插入的最新自动生成的ID
在MySQL的插入数据使用自增ID的场景,在添加的时候配置useGeneratedKeys=“true” keyProperty=“id”
例如:
<insert id="insertOnePlayer" parameterType="Player" useGeneratedKeys="true" keyProperty="id">
insert into tb_player (id, playName, playNo,team, height)
values (
#{id,jdbcType=INTEGER},
#{playName,jdbcType=VARCHAR},
#{playNo,jdbcType=INTEGER},
#{team,jdbcType=VARCHAR},
#{height,jdbcType=DECIMAL}
)
</insert>
在insert成功后,MyBatis会将插入的值自动绑定到插入的对象的Id属性中,我们用getId就能获取最新的Id。
blogService.addBlog(blog);
System.out.printIn(blog.getId());
什么时候用#{}, 什么时候用个${}
在Mapper.xml里面配置传入参数,有两种写法:#{}、${}。作为OGNL表达式都可以实现参数的替换。这两种方式的区别在哪里,首选要清楚PrepareStatement和Statement的区别。
- 两个都是接口,PrepareStatement是继承自Statement的。
- Statement处理静态的SQL。PrepareStatement主要用于执行带参数的语句。
- PrepareStatement的addBatch()方法一次性发送多个查询给数据库。
- PS相似SQL只编译一次(对语句进行了缓存,相当于一个函数),例如语句相同参数不同,可以减少编译次数。
- PS可以防止SQL注入。
MyBatis任意语句的默认值:PREPARED
这两个符号的解析方式是不一样的
“#”会解析为PrepareStatement的参数标记符,参数部分用“?”代替,传入的参数会经过类型检查和安全检查。
“$”只会做字符串的替换
#和$的区别:
1. 是否防止SQL注入:美元符号方式不会对符号进行转义,不能防止SQL注入
2. 性能:¥方式没有预编译,不会缓存。
结论:
1. 能用#的地方都用#
2. 常量的替换,例如排序条件中的字段明后才能,不用加单引号,可以使用¥
如何实现模糊查询like
- 字符串拼接,在Java代码中拼接%%,直接like。因为没有预编译,存在SQL注入风险,不推荐使用。
- CONCAT(推荐)
<when test="empName != null amd empName != "">
AND e.emp_name LIKE CONCAT(CONCAT('%', #{emp_name, jdbcType=VARCHAR}),'%')
</when>
- bind标签
<select id="getEmpList_bind" resultType="empResultMap" parameterType="Employee">
<bind name="pattern1" value="'%' + empName + '%'" />
SELECT * from tbl_emp
<where>
<if test="empId != null">
emp_id = #{empId, jdbcType= INTEGER}
</if>
</where>
ORDER BY emp_id
</select>