Skip to content

《面渣逆袭》MyBatis 篇 · 第 1/3 章。原版 PDF(下载 / 打印)

1. 说说什么是MyBatis?

先吹一下

Mybatis 是一个半 ORM (对象关系映射)框架,它内部封装了 JDBC ,开发时只需要关注 SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql ,可以严格控制 sql 执行性能,灵活度高。

MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

再说一下缺点

SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL 语句的功底有一定要求

SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库

ORM 是什么?

配图

ORM (ObjectRelationalMapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java 对象(POJO )的映射关系的技术。简单来说,ORM 是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。

为什么说Mybatis 是半自动ORM 映射工具?它与全自动的区别在哪里?

Hibernate 属于全自动ORM 映射工具,使用Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。

而Mybatis 在查询关联对象或关联集合对象时,需要手动编写SQL 来完成,所以,被称之为半自动

ORM 映射工具。

JDBC 编程有哪些不足之处,MyBatis 是如何解决的?

配图

  1. 数据连接创建、释放频繁造成系统资源浪费从而影响系统性能,在mybatis-config.xml 中配置

数据链接池,使用连接池统一管理数据库连接。

  1. s ql 语句写在代码中造成代码不易维护,将s ql 语句配置在XXXXmapper.xml 文件中与java 代码

分离。

  1. 向s ql 语句传参数麻烦,因为s ql 语句的where 条件不一定,可能多也可能少,占位符需要和参

数一一对应。Mybatis 自动将java 对象映射至s ql 语句。

  1. 对结果集解析麻烦,s ql 变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装

成pojo 对象解析比较方便。Mybatis 自动将s ql 执行结果映射至java 对象。

2. Hibernate 和 MyBatis 有什么区别?

相同点

都是对jdbc 的封装,都是应用于持久层的框架。

配图

不同点

  1. 映射关系

MyBatis 是一个半自动映射的框架,配置Java 对象与s ql 语句执行结果的对应关系,多表关联关系配置简单

Hibernate 是一个全表映射的框架,配置Java 对象与数据库表的对应关系,多表关联关系配置复杂

  1. SQL 优化和移植性

Hibernate 对SQL 语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL (HibernateQueryLanguage)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL 语句优化困难。

MyBatis 需要手动编写 SQL ,支持动态 SQL 、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL 语句操作数据库,不支持数据库无关性,但s ql 语句优化容易。

  1. MyBatis 和Hibernate 的适用场景不同

配图

Hibernate 是标准的ORM 框架,SQL 编写量较少,但不够灵活,适合于需求相对稳定,中小型的软件项目,比如:办公自动化系统

MyBatis 是半ORM 框架,需要编写较多SQL ,但是比较灵活,适合于需求变化频繁,快速迭代的项目,比如:电商网站

3. MyBatis使用过程?生命周期?

MyBatis 基本使用的过程大概可以分为这么几步:

配图

  1. 创建SqlSessionFactory

可以从配置或者直接编码来创建SqlSessionFactory

java
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  1. 通过SqlSessionFactory 创建SqlSession

SqlSession (会话)可以理解为程序和数据库之间的桥梁

SqlSession session = sqlSessionFactory.openSession();
  1. 通过sqlsession 执行数据库操作

可以通过 SqlSession 实例来直接执行已映射的 SQL 语句:

Blog blog = (Blog)session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);

更常用的方式是先获取Mapper( 映射) ,然后再执行SQL 语句:

BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
4)调用session.commit()提交事务

如果是更新、删除语句,我们还需要提交一下事务。

5)调用session.close()关闭会话

最后一定要记得关闭会话。

MyBatis 生命周期?

上面提到了几个MyBatis 的组件,一般说的MyBatis 生命周期就是这些组件的生命周期。

SqlSessionFactoryBuilder一旦创建了 SqlSessionFactory ,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的生命周期只存在于方法的内部。

SqlSessionFactory

SqlSessionFactory 是用来创建SqlSession 的,相当于一个数据库连接池,每次创建

SqlSessionFactory 都会使用数据库资源,多次创建和销毁是对资源的浪费。所以SqlSessionFactory是应用级的生命周期,而且应该是单例的。

SqlSession

SqlSession 相当于JDBC 中的Connection ,SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的生命周期是一次请求或一个方法。

Mapper映射器是一些绑定映射语句的交叉。映射器交叉的实例是从 SqlSession 中获得的,它的生命周期在

sqlsession 事务方法之内,一般会控制在方法级。

配图

当然,万物皆可集成Spring ,MyBatis 通常也是和Spring 集成使用,Spring 可以帮助我们创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到我们的 bean 中,我们不需要关心它们的创建过程和生命周期,那就是另外的故事了。

配图

4. 在mapper中如何传递多个参数?

配图

方法1 :顺序传参法

java
public User selectUser(String name, int deptId);

<selectid="selectUser"resultMap="UserResultMap">

select*fromuser

    where user_name = #{0} and dept_id = #{1}

</select>

**#{}**里面的数字代表传入参数的顺序。

这种方法不建议使用,s ql 层表达不直观,且一旦顺序调整容易出错。

方法2 :@ Param 注解传参法

java
public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);

<selectid="selectUser"resultMap="UserResultMap">

select*fromuser

    where user_name = #{userName} and dept_id = #{deptId}

</select>

**#{}**里面的名称对应的是注解@ Param 括号里面修饰的名称。

这种方法在参数不多的情况还是比较直观的,(推荐使用)。

方法3 :M ap 传参法

java
public User selectUser(Map<String, Object> params);

<selectid="selectUser"parameterType="java.util.Map"resultMap="UserResultMap">

select*fromuser

    where user_name = #{userName} and dept_id = #{deptId}

</select>

**#{}**里面的名称对应的是M ap 里面的k ey 名称。

这种方法适合传递多个参数,且参数易变能灵活传递的情况。

方法4 :JavaBean 传参法

java
public User selectUser(User user);

<selectid="selectUser"parameterType="com.jourwon.pojo.User"

resultMap="UserResultMap">

select*fromuser

    where user_name = #{userName} and dept_id = #{deptId}

</select>

**#{}**里面的名称对应的是User 类里面的成员属性。

这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。(推荐使用)。

5. 实体类属性名和表中字段名不一样 ,怎么办?

第1 种: 通过在查询的SQL 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。

<selectid="getOrder"parameterType="int"resultType="com.jourwon.pojo.Order">

selectorder_idid,order_noorderno,order_pricepriceformorderswhereorder_id=#

{id};

</select>第2 种: 通过resultMap 中的< result> 来映射字段名和实体类属性名的一一对应的关系。

<selectid="getOrder"parameterType="int"resultMap="orderResultMap">

sql
  select * from orders where order_id=#{id}

</select><resultMaptype="com.jourwon.pojo.Order"id="orderResultMap"><! – 用id属性来映射主键字段– ><idproperty="id"column="order_id"><! – 用result 属性来映射非主键字段,property 为实体类属性名,column 为数据库表中的属性– ><resultproperty="orderno"column="order_no"/><resultproperty="price"column="order_price"/></reslutMap>

6. Mybatis是否可以映射Enum枚举类?

Mybatis 当然可以映射枚举类,不单可以映射枚举类,Mybatis 可以映射任何对象到表的一列上。

映射方式为自定义一个TypeHandler,实现TypeHandler的setParameter()和getResult()接又方

法。

TypeHandler 有两个作用,一是完成从javaType 至jdbcType 的转换,二是完成jdbcType 至

javaType的转换,体现为setParameter()和getResult()两个方法,分别代表设置sql问号占位符参

数和获取列查询结果。

7. {}和${}的区别?

配图

**#{}是占位符,预编译处理;${}**是拼接符,字符串替换,没有预编译处理。

Mybatis 在处理**#{}时,#{}传入参数是以字符串传入,会将SQL 中的#{}**替换为? 号,调用

PreparedStatement 的s et 方法来赋值。

**#{}可以有效的防止SQL 注入,提高系统安全性;${}**不能防止SQL 注入

**#{}的变量替换是在DBMS 中;${}**的变量替换是在 DBMS 外

8. 模糊查询like语句该怎么写?

配图

1 ’%${question}%’ 可能引起SQL 注入,不推荐2**"%"#{question}"%"注意:因为#{ … }**解析成s ql 语句时候,会在变量外侧自动加单引号’ ' ,所以这里 % 需要使用双引号" " ,不能使用单引号 ’ ' ,不然会查不到任何结果。

3CONCAT(’%’,#{question},’%’) 使用CONCAT()函数,(推荐

配图

4 使用bind 标签(不推荐)

<selectid="listUserLikeUsername"resultType="com.jourwon.pojo.User">  <bindname="pattern"value="'%'+username+'%'"/>  selectid,sex,age,username,passwordfrompersonwhereusernameLIKE#

{pattern}

</select>

9. Mybatis能执行一对一、一对多的关联查询吗?

当然可以,不止支持一对一、一对多的关联查询,还支持多对多、多对一的关联查询。

配图

一对一< association>

比如订单和支付是一对一的关系,这种关联的实现:

实体类:

java
public class Order {
    private Integer orderId;
    private String orderDesc;

/

  • 支付对象*/
java
    private Pay pay;
    //……
}

结果映射<!-- 订单resultMap--><resultMapid="peopleResultMap"type="cn.fighter3.entity.Order"><idproperty="orderId"column="order_id"/><resultproperty="orderDesc"column="order_desc"/><!-- 一对一结果映射- -><associationproperty="pay"javaType="cn.fighter3.entity.Pay"><idcolumn="payId"property="pay_id"/><resultcolumn="account"property="account"/></association></resultMap>查询就是普通的关联查<selectid="getTeacher"resultMap="getTeacherMap"parameterType="int">

select*fromordero

leftjoinpaypono.order_id=p.order_id

    where o.order_id=#{orderId}

</select>

一对多< collection>

比如商品分类和商品,是一对多的关系。

实体类

java
public class Category {
    private int categoryId;
    private String categoryName;

/

  • 商品列表

/

    List<Product> products;
    //……
}

结果映射<resultMaptype="Category"id="categoryBean"><idcolumn="categoryId"property="category_id"/><resultcolumn="categoryName"property="category_name"/><!-- 一对多的关系 --><!--property: 指的是集合属性的值, ofType :指的是集合中元素的类型 --><collectionproperty="products"ofType="Product"><idcolumn="product_id"property="productId"/><resultcolumn="productName"property="productName"/><resultcolumn="price"property="price"/></collection></resultMap>查询查询就是一个普通的关联查询<!-- 关联查询分类和产品表 --><selectid="listCategory"resultMap="categoryBean">

selectc.*,p.*fromcategory_cleftjoinproduct_ponc.id=p.cid </select>那么多对一、多对多怎么实现呢?还是利用\ <association> 和\ <collection> ,篇幅所限,这里就不展开了。

10. Mybatis是否支持延迟加载?原理?

Mybatis 支持association 关联对象和collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在Mybatis 配置文件中,可以配置是否启用延迟加载

lazyLoadingEnabled=true|false 。

它的原理是,使用CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如

调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保

存好的查询关联B 对象的s ql ,把B 查询上来,然后调用a .setB(b) ,于是a 的对象b 属性就有值了,接着完成a .getB().getName() 方法的调用。这就是延迟加载的基本原理。

当然了,不光是Mybatis ,几乎所有的包括Hibernate ,支持延迟加载的原理都是一样的。

11. 如何获取生成的主键?

新增标签中添加:keyProperty="ID" 即可<insertid="insert"useGeneratedKeys="true"keyProperty="userId">

insertintouser(

    user_name, user_password, create_time)
    values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP})

</insert>这时候就可以完成回填主键

mapper.insert(user);
user.getId;

12. MyBatis支持动态SQL吗?

MyBatis 中有一些支持动态SQL 的标签,它们的原理是使用OGNL 从SQL 参数对象中计算表达式的值,根据表达式的值动态拼接SQL ,以此来完成动态SQL 的功能。

配图

if根据条件来组成where 子句<selectid="findActiveBlogWithTitleLike"

resultType="Blog">

SELECT*FROMBLOG

WHEREstate= ‘ ACTIVE ’<iftest="title!=null">

  AND title like #{title}

</if></select>

choose (when, otherwise)

这个和Java 中的 switch 语句有点像<selectid="findActiveBlogLike"

resultType="Blog">

SELECT*FROMBLOGWHEREstate= ‘ ACTIVE ’<choose><whentest="title!=null">

    AND title like #{title}

</when><whentest="author!=nullandauthor.name!=null">

    AND author_name like #{author.name}

</when><otherwise>

ANDfeatured=1 </otherwise></choose></select>

trim (where, set)

<where> 可以用在所有的查询条件都是动态的情况<selectid="findActiveBlogLike"

resultType="Blog">

SELECT*FROMBLOG <where><iftest="state!=null">

       state = #{state}

</if><iftest="title!=null">

      AND title like #{title}

</if><iftest="author!=nullandauthor.name!=null">

      AND author_name like #{author.name}

</if></where></select><set> 可以用在动态更新的时候<updateid="updateAuthorIfNecessary">

updateAuthor <set><iftest="username!=null">username=#{username},</if><iftest="password!=null">password=#{password},</if><iftest="email!=null">email=#{email},</if><iftest="bio!=null">bio=#{bio}</if></set>

  where id=#{id}

</update>

foreach看到名字就知道了,这个是用来循环的,可以对集合进行遍历<selectid="selectPostIn"resultType="domain.blog.Post">

SELECT*

FROMPOSTP <where><foreachitem="item"index="index"collection="list"

      open="ID in (" separator="," close=")" nullable="true">
        #{item}

</foreach></where></select>

13. MyBatis如何执行批量操作?

配图

第一种方法:使用foreach 标签

foreach 的主要用在构建in条件中,它可以在SQL 语句中进行迭代一个集合。foreach 标签的属性主要有item ,index ,collection ,open ,separator ,close 。

item     表示集合中每一个元素进行迭代时的别名,随便起的变量名;

index     指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用;

open     表示该语句以什么开始,常用“ ( ” ;

separator 表示在每次进行迭代之间以什么符号作为分隔符,常用“ , ” ;

close     表示以什么结束,常用“ ) ” 。

在使用foreach 的时候最关键的也是最容易出错的就是collection 属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有以下3 种情况:

  1. 如果传入的是单参数且参数类型是一个List 的时候,collection 属性值为list

  2. 如果传入的是单参数且参数类型是一个array 数组的时候,collection 的属性值为array

  3. 如果传入的参数是多个的时候,我们就需要把它们封装成一个M ap 了,当然单参数也可以封装成

map ,实际上如果你在传入参数的时候,在MyBatis 里面也是会把它封装成一个M ap 的,m ap 的

key 就是参数名,所以这个时候collection 属性值就是传入的List 或array 对象在自己封装的m ap里面的k ey看看批量保存的两种用法:

<!-- MySQL下批量保存,可以foreach遍历 mysql支持values(),(),()语法 --> //推荐使用

<insertid="addEmpsBatch">

sql
    INSERT INTO emp(ename,gender,email,did)

VALUES <foreachcollection="emps"item="emp"separator=",">

        (#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})

</foreach></insert><!-- 这种方式需要数据库连接属性allowMutiQueries=true 的支持如jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true--><insertid="addEmpsBatch"><foreachcollection="emps"item="emp"separator=";">

sql
        INSERT INTO emp(ename,gender,email,did)
        VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})

</foreach></insert>

第二种方法:使用ExecutorType.BATCH

Mybatis 内置的ExecutorType 有3 种,默认为simple ,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交s ql ;而batch 模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch 性能将更优; 但batch 模式也有自己的问题,比如在Insert 操作时,在事务没有提交之前,是没有办法获取到自增的i d ,在某些情况下不符合业务的需求。

具体用法如下:

//批量保存方法测试

@ Test

java
public void testBatch() throws IOException{
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    //可以执行批量操作的sqlSession
    SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
    //批量保存执行前时间
    long start = System.currentTimeMillis();
    try {
        EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
        for (int i = 0; i < 1000; i++) {
            mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b",
"1"));
        }
        openSession.commit();
        long end = System.currentTimeMillis();
        //批量保存执行后的时间
        System.out.println("执行时长" + (end - start));
        //批量 预编译sql一次==》设置参数==》10000次==》执行1次 677
        //非批量 (预编译=设置参数=执行 )==》10000次 1121
    } finally {
        openSession.close();
    }
}

mapper 和mapper.xml 如下

java
public interface EmployeeMapper {
    //批量保存员工
    Long addEmp(Employee employee);
}

<mappernamespace="com.jourwon.mapper.EmployeeMapper"

<!-- 批量保存员工 --><insertid="addEmp">

sql
        insert into employee(lastName,email,gender)
        values(#{lastName},#{email},#{gender})

</insert></mapper>

14. 说说Mybatis的一级、二级缓存?

  1. 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为SqlSession ,各个

SqlSession 之间的缓存相互隔离,当 Sessionflush 或 close 之后,该 SqlSession 中的所有

Cache 就将清空,MyBatis 默认打开一级缓存。

配图

  1. 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache ,HashMap 存储,不同之处在
于其存储作用域为 Mapper(Namespace),可以在多个SqlSession之间共享,并且可自定义存储

源,如 Ehcache 。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现

Serializable 序列化交叉( 可用来保存对象的状态) , 可在它的映射文件中配置。

配图

本文整理自三分恶《面渣逆袭》系列的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传