MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的XML或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。MyBatis 本是 Apache 的一个开源项目iBatis,2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github

MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) ,ORM是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。

Mybatis优势

  1. 基于XML文件配置开发,不是硬编码
  2. SQL语句和代码分离
  3. 优化JDBC的使用,高效开发,以及可以自动映射结果集,包括参数的优化(反射)
  4. 支持动态SQL。SQL语句的条件很有可能是不确定的,需要动态根据需求来进行查询或操作。

框架就是一个半成品软件,是一套可重用的、通用的、软件基础代码模型,在框架的基础之上构建软件编写更加高效、规范、通用、可扩展

使用流程

MyBatis执行流程

  1. 搭建环境

    导包:Mysql连接驱动包mysql-connector-j-8.0.32.jar 与 Mybatis包mybatis-3.5.11.jar

  2. 拷贝Mybatis核心配置文件,并编写配置信息文件jdbc.properties与映射XxxMapper.xml文件

    Mybatis-Config.xml

    将连接mysql的具体配置信息抽取到jdbc.properties,在核心配置文件中通过<properties resource="jdbc.properties"/> 关联到jdbc.properties。

    再关联扫描的Mapper文件,Mapper文件用于存储需要执行的sql语句。

    <?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>
        <!-- 引入配置文件信息,这里不能加classpath:。
        resource:引入类路径下的资源,即classpath,所以不需要写classpath:
        url:引入网络路径或磁盘路径下的资源 
        -->
        <properties resource="db.properties"></properties>
        <!--
        environments:配置数据库连接环境信息。可以配置多个environment,
        通过default属性切换不同的environment.
        -->
        <environments default="development">
            <!-- 一个环境 id:为这个环境取唯一一个id名称 -->
            <environment id="development">
                <!-- 事务管理 type:JDBC(支持事务)/MANAGED(什么都不做) -->
                <transactionManager type="JDBC" />
                <!-- 数据源, 连接池 type(POOLED):MyBatis自带的连接池 -->
                <dataSource type="POOLED">
                    <!-- 连接数据库的参数:直接写死的方式 -->
                    <!--
                    <property name="driver" value="com.mysql.jdbc.Driver" />
                    <property name="url" value="jdbc:mysql:///mydb" />
                    <property name="username" value="root" />
                    <property name="password" value="admin" />
                    -->
                    <!-- 连接数据库的参数:使用属性文件的方式 -->
                    <property name="driver" value="${driver}" />
                    <property name="url" value="${url}" />
                    <property name="username" value="${username}" />
                    <property name="password" value="${password}" />
                </dataSource>
            </environment>
        </environments>
        <!--加载sql映射文件-->
        <mappers>
            <mapper resource="cn/itsource/domain/ProductMapper.xml" />
        </mappers>
    </configuration>
    
    <!--属性文件:jdbc.properties-->
    driver=com.mysql.jdbc.Driver
    url=jdbc:mysql:///java1125
    username=root
    password=admin
    
    <?xml version="1.0" encoding="UTF-8"?>
    <!--Mapper映射文件-->
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!-- 
     这个Mapper的主要功能就是写sql
     mapper:根
     namespace:命令空间 (用来确定唯一)以前这个是可以不加的,现在必需加
     namespace的值:接口的完全限定名
     -->
    <mapper namespace="cn.itsource.dao.ProductDao">
        <!-- 
        select :这里面写查询语句
        id:用来确定这条sql语句的唯一,以后我们确定唯一,也就是找sql语句 : namespace + id
        例: cn.itsource.dao.ProductDao.findAll
        resultType : 结果类型(第一条数据返回的对象类型)自己的对象一定是完全限定类名
        -->
        <select id="findAll" resultType="cn.itsource.domain.Product">
            select * from product
        </select>
    </mapper>
    
  3. 编写代码,获得SqlSession执行对应文件的sql

    @Override
    public void save(Product p) {
        InputStream is;
        SqlSession sqlSession = null;
        try {
          	// 1. 加载mybatis的核心配置文件,获取 SqlSessionFactory
            is = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
          	// openSession()方法默认参数为false,表示不开启自动提交,传参true表示自动提交
          	sqlSession = factory.openSession(true);
            // 2. 执行sql
            sqlSession.insert("ProductDao.save",p);
            System.out.println("添加数据成功!");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3.释放资源
            if (sqlSession != null) {
                sqlSession.close();
            }
        } 
    }
    
  4. 测试编写的代码是否实现。

    @Test
    public void testFindAll() throws IOException {
        List<Product> list = dao.findAll();
        System.out.println(list);
        list.stream().forEach(System.out::println);
    }
    

优化

使用Mapper映射器优化

不实用Mapper映射器存在一定的问题。

存在问题

  1. DAO中其实都是在调用SqlSession中的CRUD方法

  2. session中的CRUD规则都是一样的

    第一个参数:必须是namespace+id,都一样,且是硬编码

    第二个参数:如果需要传入的参数,则传入第二个参数

解决方案

使用动态代理机制,在内存中动态生成一个DAO的实现类,其底层基于反射实现。

实现方式

  1. 根据需求,在Mapper包中创建模型相关的Mapper接口,例如:UserMapper;

  2. 在同包下编写映射文件,例如:UserMapper.xml;

    Mapper映射文件的命名空间,必须和接口的"完全限定名"一致;

    定义sql标签的id,需要和"接口的方法名"一致;

  3. 在Mybatis核心配置文件中配置(或注册)Mapper映射文件;

  4. 获取SqlSession调用getMapper(Class<T> clazz)方法(传入需要执行的接口字节码对象)获得当前接口的实现类的对象(该实现类为映射器动态生成的),使用该对象调用方法。

    SqlSession session = MyBatisUtils.getSqlSession();
    UserMapper mapper = session.getMapper(UserMapper.class);
    mapper.findUser;
    
  5. 测试。

domain类别名优化

在Mybatis中,通过在mybatis-config.xml文件中配置typeAlias来设置你自己的别名,将实体类的名称简化,方便开发,别名分为内置别名和自定义别名。

内置别名是Mybatis提供的,可以直接使用。

内置别名

自定义别名,在mybatis核心配置文件中配置,未知在<properties/>标签下。

<typeAliases>
    <!-- 单个配置:在映射文件中就可以直接使用"Product"代替"cn.itsource.domain.Product" -->
    <typeAlias type="cn.itsource.domain.Product" alias="Product" />
    <!-- 包的配置:使用package标签配置一个你要扫描的包,
				 该标签会注册该包下所有的 Java Bean 到 MyBatis 中
    		 项目中使用,添加了包之后,类名或类名首字母小写就是别名 
		-->                
    <package name="cn.itsource.domain" />
</typeAliases>
<?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="cn.itsource.dao.ProductDao">
    <select id="findAll" resultType="Product">
        select * from product
    </select>
</mapper>

传参优化

在需要动态指定表名或字段名的场景可以使用对应的$#

$#的区别:

  1. #:MyBatis会把#{}表达式使用?(占位符)替换,作为一个sql参数使用,故不会有sql注入问题。

    比如name的值为:

    定义SQL: select * from t_user where name = #{name}

    最终SQL: select * from t_user where name = ?

  2. $:MyBatis会把${}表达式的值替换到sql中,作为sql的组成部分。把获取到值直接拼接到SQL中。

模糊匹配:

  • 方案一:直接用#,日志中发现值并没有替换,所以不能直接使用#

    and (name like '%#{keywords}%' or password like '%#{keywords}%')
    
  • 方案二:使用$符号,测试成功,但是可能会出现SQL注入

    and (name like '%${keywords}%' or password like '%${keywords}%')   
    
  • 方案三:用mysql中字符串拼接函数concat,测试成功,也不会出现SQL注入

    and(name like concat('%',#{keywords},'%') or password like concat('%',#{keywords},'%')
    

步骤

  1. 在Mapper映射文件中配置sql语句时使用对应字符动态配置字段与值。

    <select id="findAllByCondition" parameterType="map" resultType="product">
    		<!-- 如果要动态指定列名,请使用${} -->
    		select * from product where ${columnName} = #{value} limit #{index},#{num}
    </select>
    
  2. 执行sql时使用Map传参

    @Test
    public void findAllByCondition(){
        HashMap<String, Object> map = new HashMap<>();
        map.put("column","product_name");
        map.put("value","罗技G9X");
      	map.put("index",0);
      	map.put("num",5);
        List<Product> list = mapper.findAllByCondition(map);
        list.stream().forEach(System.out::println);
    }
    

数据库字段与domain类属性映射

通常会因为数据库字段命名规则与变量名命名规则不一致,导致映射出错,无法将查询的数据封装入domain对象。

解决方案一

可以在写sql语句时给字段起别名,将别名定义成和属性名一致即可。

<select id="selectAll" resultType="Brand">
      select
          id, brand_name as brandName, company_name as companyName, ordered, description, status
      from tb_brand
</select>

解决方案二

将需要复用的SQL片段抽取到 <sql> 标签中

<sql id="brand_column">
		id, brand_name as brandName, company_name as companyName, ordered, description, status
</sql>

在原sql语句中进行引用,refid 指定上述 SQL 片段的id值

<select id="selectAll" resultType="brand">
    select
    <include refid="brand_column" />
    from tb_brand;
</select>

解决方案三

使用resultMap来定义字段和属性的映射关系。在映射配置文件中使用 resultMap 定义数据库字段column和实体类属性property的映射关系

<resultMap id="brandResultMap" type="brand">
    <!--
        id:完成主键字段的映射
        type:映射的java对象类型
        result:完成一般字段的映射
            column:表的列名
            property:实体类的属性名
    -->
    <result column="brand_name" property="brandName"/>
    <result column="company_name" property="companyName"/>
</resultMap>

SQL语句正常编写时类型为 resultMap

<select id="selectAll" resultMap="brandResultMap">
    select * from tb_brand
</select>

获取自增主键

当数据库字段设置了自增长后,可以通过该方法获取到自增长的主键,与其他参数一并传入对象内。获取的方式分为两种。

方法一

通过在XxxMapper映射文件给编写sql的标签设置属性实现:

useGeneratedKeys:true/false:这个属性的意思就是,是否开启返回自增长的ID

keyProperty:这个属性的意思就是自增长的ID返回之后,放在你插入的对象中哪一个字段

keyColumn:数据库中的主键id是哪个,如果主键是第一列那么可以省略不写,如果不是第一列那么必须写

<insert id="save" useGeneratedKeys="true" keyProperty="id"
        parameterType="cn.itsource.domain.Product">
<!-- 注意:如果没有指定例名,请严格按照表中的字段顺序进行占位 -->
        insert into product values(#{id},#{productName},#{dir_id},#{salePrice},#{supplier},#{brand},#{cutoff},#{costPrice})
</insert>

方法二

该方法不常用,如果需要给id设置一个随机数或者唯一值,开发中最常用的方式是在Java代码中通过UUID这个方式进行生成。

<insert id="add" parameterType="cn.itsource.domain.Product">
<!-- 将插入数据的主键返回,返回到product对象中 SELECT LAST_INSERT_ID():得到刚insert进去记录的主键
        值,只适用于自增主键
        keyProperty:selectKey 语句结果应该被设置到的目标属性
        resultType:指定SELECT LAST_INSERT_ID()的结果类型
        order属性指定执行查询主键值SQL语句是在插入语句执行之前还是之后(可取值:after和before)
        注:mysql用after,oracle用before -->
<selectKey keyProperty="id" resultType="java.lang.Long"
        order="AFTER" statementType="PREPARED">
        SELECT LAST_INSERT_ID()
</selectKey>
<!-- 注意:如果没有指定例名,请严格按照表中的字段顺序进行占位 -->
        insert into product values(#{id},#{productName},#{dir_id},#{salePrice},#{supplier},#{brand},#{cutoff},#{costPrice})
</insert>

高级查询

高级查询其实就是多条件查询,在真实开发中,需要根据用户选择的条件来查询某些数据的,例如在逛淘宝、京东的时候,需要筛选手机->品牌->价格。

在Mybatis中,高级查询就是由动态sql实现

功能实现的步骤

  1. 编写接口方法

    • 参数:所有查询条件。三种定义方式
    • 结果:List

    Mybatis针对多参数有三种实现:

    1. 定义多个参数:
    List<Brand> selectByCondition(int status, String companyName,String brandName);
    
    1. 将多个参数封装成一个 实体对象 ,将该实体对象作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容} 时,里面的内容必须和实体类属性名保持一致。
    List<Brand> selectByCondition(Brand brand);
    
    1. 将多个参数封装到map集合中,将map集合作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容} 时,里面的内容必须和map集合中键的名称一致。
    List<Brand> selectByCondition(Map map);
    
  2. 在映射配置文件中编写SQL语句

    BrandMapper.xml 映射配置文件中编写 statement,使用 resultMap 而不是使用 resultType

    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        where status = #{status}
          and company_name like concat("%",#{companyName},"%")
          and brand_name like concat("%",#{brandName},"%")
    </select>
    
  3. 编写测试方法并执行

    @Test
    public void testSelectByCondition() throws Exception{
        //模拟数据
        int status = 1;
        String companyName = "华为";
        String brandName = "华为";
        Map map = new HashMap();
        map.put("companyName",companyName);
        map.put("brandName",brandName);
        map.put("status",status);
        List<Brand> brands = mapper.selectByCondition(map);
        brands.stream().forEach(System.out::println);
    }
    

Mybatis动态sql标签

Mybatis对动态SQL有很强大的支撑,提供了多种标签:

  • if
  • where
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

if标签

作用:

  1. 用于判断条件是否满足,满足就拼接sql,不满足不拼接
  2. 会自动加上空格,避免造成sql语法错误

如果参数是字符串类型,那么需要判断null和空字符串; 如果参数是数值类型,只需要判断null值。

<!-- 下列SQL语句就会根据传递的参数值进行动态的拼接。如果此时status和companyName有值那么就会值拼接这两个条件。 -->
<select id="selectByCondition" resultMap="brandResultMap">
    select *
    from tb_brand
    where
  			1 = 1
        <if test="status != null">
            and status = #{status}
        </if>
        <if test="companyName != null and companyName != '' ">
            and company_name like #{companyName}
        </if>
        <if test="brandName != null and brandName != '' ">
            and brand_name like #{brandName}
        </if>
</select>

where标签

作用:

  1. 替换where关键字
  2. 忽略第一个多余的and 或者 or
  3. 如果所有的参数没有值则不加where关键字
<select id="selectByCondition" resultMap="brandResultMap">
    select *
    from tb_brand
    <where>
        <if test="status != null">
            and status = #{status}
        </if>
        <if test="companyName != null and companyName != '' ">
            and company_name like #{companyName}
        </if>
        <if test="brandName != null and brandName != '' ">
            and brand_name like #{brandName}
        </if>
    </where>
</select>

foreach标签

在批量增删的场景,需要使用foreach标签实现,标签提供了一下属性。

  • collection 指的是集合或者数组,这里接受两种值,如果是集合就写 “list”,如果是数组就写“array”
  • open 开始元素,我们需要使用“( ” 作为开始
  • item 循环的每一个元素,这里的每一个元素就是list中的每一个id
  • separator 分隔符,我们拼接后的sql需要使用“,”来分割多个ID
  • close 结束元素,我们需要使用“ )”作为结束
  1. 批量删除:

    批量删除方法,参数是一个List,即:根据多个ID来删除。

    /**
     * 批量删除
     * @param ids 要删除的数据id
     */
    void batchDelete(List<Integer> ids);
    
    <delete id="batchDelete">
        delete
        from
            tb_brand
        where
            id in
        <!-- 
    		collection 指的是集合或者数组,这里接受两种值,如果是集合就写 “list”,如果是数组就写“array”
        open 开始元素,我们需要使用“( ” 作为开始
    		item 循环的每一个元素,这里的每一个元素就是list中的每一个id
    		separator 分隔符,我们拼接后的sql需要使用“,”来分割多个ID
    		close 结束元素,我们需要使用“ )”作为结束
    		#{id} 循环的内容,这里是把id的值取出来了,比如循环三次就如同 :
    		-->
        <foreach collection="list" open="(" item="id" close=")" separator=",">
            #{id}
        </foreach>
    </delete>
    
  2. 批量插入:

    <!--批量添加-->
    <insert id="batchInsert">
        insert into
            tb_brand(brand_name,company_name,ordered,description)
        values 
            <foreach collection="list"  item="brand" separator=",">
                (#{brand.brandName},#{brand.companyName},#{brand.ordered},#{brand.description})
            </foreach>
    </insert>
    

    这里跟批量删除有点不一样 ,需要的效果是 VALUES ("ls",11,1),("ww",12,1),("cq",13,1) 这种,它其实没有开始符号和结束符号,因为每个值前面都有 ”(“以及”)“ ,所以不能使用 open和close 。这里的循环内容为 (#{emp.username},#{emp.age},#{emp.sex}) ,比如循环两次,用“ , ”分割就是: (#{emp.username},#{emp.age},#{emp.sex}) , (#{emp.username},#{emp.age},#{emp.sex}) 效果。这里的item的值是emp,其实是一个Employee对象,因为list中装的就是Employee对象,当 #{xx}被替换成对应的值之后就形成了VALUES ("ls",11,1),("ww",12,1)这种效果

联表查询中的一对多与多对一

关联关系domain的设计

  • 一对多

    在一方添加多方的List集合对象字段

相关插件

MyBatisX 插件

MybatisX 是一款基于 IDEA 的快速开发插件,为效率而生。

主要功能:

  • XML映射配置文件 和 接口方法 间相互跳转
  • 根据接口方法生成 statement