mybatis

赖卓成2023年7月19日
大约 13 分钟

5-1-mybatis快速入门

01-简介

  • 一款优秀的持久层框架、用于简化JDBC开发
  • iBatis
  • 官网:点击跳转

持久层:

  • 负责将数据保存到数据库的那一层代码
  • JavaEE三层架构:表现层、业务层、持久层

框架:

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

JDBC缺点:

  • 硬编码:数据库连接信息经常变动,JDBC那套代码我们是写死的字符串、sql也存在硬编码、
  • 操作繁琐:手动设置参数、手动封装结果集

mybatis免除了几乎所有的JDBC代码,以及设置参数和获取结果集的工作。

02-环境搭建

需求:

  • 创建user表、添加数据
  • 创建模块、导入坐标
  • 编写Mybatis核心配置文件->替换连接信息、解决硬编码问题
  • 编写SQL映射文件->统一管理SQL语句,解决硬编码问题
  • 编码
    • 定义pojo类
    • 加载核心配置文件、获取SqlSessionFactory对象
    • 获取SqlSession对象,执行SQL语句
    • 释放资源

ddl:

create database mybatis;
use mybatis;

drop table if exists tb_user;

create table tb_user(
	id int primary key auto_increment,
	username varchar(20),
	password varchar(20),
	gender char(1),
	addr varchar(30)
);



INSERT INTO tb_user VALUES (1, 'zhangsan', '123', '男', '北京');
INSERT INTO tb_user VALUES (2, '李四', '234', '女', '天津');
INSERT INTO tb_user VALUES (3, '王五', '11', '男', '西安');


创建module,导入依赖:

    <dependencies>

        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.5</version>
        </dependency>
        <!-- mysql 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.32</version>
        </dependency>
        
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>

        <!-- 添加slf4j日志api -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.20</version>
        </dependency>
        <!-- 添加logback-classic依赖 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!-- 添加logback-core依赖 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>	

    </dependencies>

日志配置:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
        CONSOLE :表示当前的日志信息是可以输出到控制台的。
    -->
    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%level] %blue(%d{HH:mm:ss.SSS}) %cyan([%thread]) %boldGreen(%logger{15}) - %msg %n</pattern>
        </encoder>
    </appender>

    <logger name="com.lzc" level="DEBUG" additivity="false">
        <appender-ref ref="Console"/>
    </logger>


    <!--

      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF
     , 默认debug
      <root>可以包含零个或多个<appender-ref>元素,标识这个输出位置将会被本日志级别控制。
      -->
    <root level="DEBUG">
        <appender-ref ref="Console"/>
    </root>
</configuration>

03-核心配置文件

参考官网,创建mybatis-config.xml,点击跳转

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://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://www.iocaop.com:3306/mybatis?serverTimezone=UTC&amp;useSSL=false&amp;useServerPrepStmts=true"/>
                <property name="username" value="root"/>
                <property name="password" value="911823"/>
            </dataSource>

        </environment>
    </environments>
    
    <mappers>
        <!--加载SQL映射文件--> 
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

创建tb_user对应的实体类:

/**
 * 用户
 *
 * @author lzc
 * @date 2023/07/19
 */
@Data
public class User {

    /**
     * id
     */
    private Integer id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 性别
     */
    private char gender;

    /**
     * 地址
     */
    private String addr;

}

创建UserMapper.xml,需要将这个xml文件加载,在mybatis-config.xml中已经做过了。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test">
    <select id="selectAll" resultType="com.lzc.mybatis.pojo.User">
        select * from tb_user;
    </select>
</mapper>

之类没有Mapper接口,所以命名空间使用test

编写测试类:

public class MybatisDemo {
    public static void main(String[] args) throws Exception {
        // 加载核心配置文件
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 获取sqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        List<User> userList = sqlSession.selectList("test.selectAll");

        System.out.println("userList = " + userList);

    }
}

sqlSession.selectList("test.selectAll");这一步直接使用命名空间.id就可以执行mapper.xml中对应的SQL语句

执行结果:

image-20230719021218724

image-20230719021229303

05-Mapper代理开发

在上面我们已经通过sqlSession实现了通过namespace和id,执行SQL,查询数据库,但是存在问题:namespace和id存在硬编码问题

参考官网:

image-20230719230152968

目的:

  • 解决原生方式中的硬编码问题
  • 简化后期执行SQL

步骤:

  • 定义SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放在同一目录下
  • 设置SQL映射文件中的namespace为Mapper接口全限定名
  • 在Mapper接口中定义方法,方法名就是SQL映射文件中SQL语句的id,并且保持参数类型和返回值类型一致。
  • 编码:
    • 通过SqlSession的getMapper方法获取Mapper接口的代理对象
    • 调用对应的方法完成SQL执行

如果Mapper接口每次和SQL映射文件名称相同,并且在同一目录下,可以通过扫包的方式简化SQL映射文件的加载

        <package name="com.lzc.mybatis.mapper"/>

创建接口,与SQL映射文件同名,并定义方法,与需要执行的SQL的id一致:

public interface UserMapper {

    /**
     * 查询所有用户
     *
     * @return {@link User}
     */
    List<User> selectAll();
}

修改SQL映射文件的namespace:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lzc.mybatis.mapper.UserMapper">
    <select id="selectAll" resultType="com.lzc.mybatis.pojo.User">
        select * from tb_user;
    </select>
</mapper>

编码,获取代理对象,执行方法:

public class MybatisDemo02 {
    public static void main(String[] args) throws Exception {
        // 加载核心配置文件
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 获取sqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> userList = mapper.selectAll();
        System.out.println("userList = " + userList);

        // 释放资源
        sqlSession.close();

    }
}

执行可以看到:这是代理对象,通过代理对象调用对应的方法,相当于:

        SqlSession sqlSession = sqlSessionFactory.openSession();
        List<User> userList = sqlSession.selectList("UserMapper.selectAll");

image-20230719232044833

执行结果:

image-20230719232057072

06-核心配置文件补充

参考官网:点击跳转

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--起别名,在Mapper.xml中就不需要写全限定名了,默认别名就是类名,不包括包名-->
    <typeAliases>
        <package name="com.lzc.mybatis.pojo"/>
    </typeAliases>

<!--配置多个环境,通过default进行切换-->
    <environments default="development">
        <!--开发环境-->
        <environment id="development">
<!--    事务管理,默认jdbc,后面交给spring,这里不管        -->
            <transactionManager type="JDBC"/>
            <!--数据库配置信息-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://www.iocaop.com:3306/mybatis?serverTimezone=UTC&amp;useSSL=false&amp;useServerPrepStmts=true"/>
                <property name="username" value="root"/>
                <property name="password" value="911823"/>
            </dataSource>
        </environment>

        <environment id="test">
            <transactionManager type="JDBC"/>
            <!--数据库配置信息-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://www.iocaop.com:3306/mybatis?serverTimezone=UTC&amp;useSSL=false&amp;useServerPrepStmts=true"/>
                <property name="username" value="root"/>
                <property name="password" value="911823"/>
            </dataSource>

        </environment>


    </environments>

    <mappers>
        <!--加载SQL映射文件-->
        <mapper resource="mapper/UserMapper.xml"/>
<!--        <package name="com.lzc.mybatis.mapper"/>-->
    </mappers>
</configuration>

5-2-mybatis查询

07-环境准备

表:

-- 删除tb_brand表
drop table if exists tb_brand;
-- 创建tb_brand表
create table tb_brand
(
    -- id 主键
    id           int primary key auto_increment,
    -- 品牌名称
    brand_name   varchar(20),
    -- 企业名称
    company_name varchar(20),
    -- 排序字段
    ordered      int,
    -- 描述信息
    description  varchar(100),
    -- 状态:0:禁用  1:启用
    status       int
);
-- 添加数据
insert into tb_brand (brand_name, company_name, ordered, description, status)
values ('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),
       ('华为', '华为技术有限公司', 100, '华为致力于把数字世界带入每个人、每个家庭、每个组织,构建万物互联的智能世界', 1),
       ('小米', '小米科技有限公司', 50, 'are you ok', 1);


SELECT * FROM tb_brand;

商品表实现如下功能:

查询:

  • 查询所有数据
  • 查看详情
  • 条件查询

添加

修改:

  • 修改所有字段
  • 修改动态字段

删除:

  • 删除一个
  • 批量删除

创建实体类:

/**
 * 品牌类
 *
 * @author lzc
 * @date 2023/07/18
 */
@Data
public class Brand {

    /**
     * id 主键
     */
    private Integer id;

    /**
     * 品牌名称
     */
    private String brandName;

    /**
     * 企业名称
     */
    private String companyName;

    /**
     * 排序字段
     */
    private Integer ordered;

    /**
     * 描述信息
     */
    private String description;

    /**
     * 状态:0:禁用  1:启用
     */
    private Integer status;
}

安装插件Mybatis-X

08-查询所有数据

创建接口:

public interface BrandMapper {

    List<Brand> selectAll();
}

创建对应的BrandMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lzc.mybatis.mapper.BrandMapper">
    <select id="selectAll" resultType="com.lzc.mybatis.pojo.Brand">
        select * from tb_brand;
    </select>
</mapper>

加载Mapper.xml

        <mapper resource="mapper/BrandMapper.xml"/>

获取代理对象,执行SQL:

public class MybatisDemo03 {
    public static void main(String[] args) throws Exception{
        // 加载核心配置文件
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 获取sqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 这里获取到的是Proxy动态代理对象
        BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);

        List<Brand> brands = mapper.selectAll();
        System.out.println("brands = " + brands);
    }
}

image-20230719234841589

09-查询详情

根据id查询某条记录详情:

创建方法:

public interface BrandMapper {

    List<Brand> selectAll();

    Brand selectById(@Param("id") Integer id);
}

编写SQL:

    <select id="selectById" resultType="com.lzc.mybatis.pojo.Brand">
        select * from tb_brand where id = #{id}
    </select>

编码运行:

public class MybatisDemo04 {
    public static void main(String[] args) throws Exception{
        // 加载核心配置文件
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 获取sqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 这里获取到的是Proxy动态代理对象
        BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);

        Brand brand = mapper.selectById(1);
        System.out.println("brand = " + brand);
    }
}

image-20230720000653593

总结:

  • 参数占位符

    • #{}:执行SQL时会将#{}占位符替换为?,将来自动设置参数值
    • ${}:拼SQL,会存在SQL注入问题
    • 使用时机:参数传递使用#{},如果要对表名、列名进行动态设置,只能使用${}进行SQL拼接
  • parameterType:用于设置参数类型,可以省略

  • SQL语句中特殊字符处理

    • 转义字符

    • <![CDATA[内容]]>
      

10-条件查询

多条件查询:

定义好resultMap

    <resultMap id="BrandResultMap" type="com.lzc.mybatis.pojo.Brand">
        <id column="id" property="id"/>
        <result column="brand_name" property="brandName"/>
        <result column="company_name" property="companyName"/>
        <result column="ordered" property="ordered"/>
        <result column="description" property="description"/>
        <result column="status" property="status"/>
    </resultMap>

第一种:散装参数

    Brand selectByCondition(@Param("brandName") String brandName,
                            @Param("companyName") String companyName,
                            @Param("status") Integer status);

第二种:对象做参数

    Brand selectByCondition(Brand brand);

第三种:Map做参数

    Brand selectByCondition(Map map);

对应的都是同一个SQL映射:

    <select id="selectByCondition" resultMap="BrandResultMap">
        SELECT * FROM tb_brand WHERE
        brand_name LIKE CONCAT('%', #{brandName}, '%')
        AND company_name LIKE CONCAT('%', #{companyName}, '%')
        AND status = #{status}
    </select>

Mapper接口中的方法可以重载,但是映射文件中的id不能重复,重载是一种可以实现不同查询条件的灵活方式

编码调用:

public class MybatisDemo05 {
    public static void main(String[] args) throws Exception{
        // 加载核心配置文件
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 获取sqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 这里获取到的是Proxy动态代理对象
        BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);

        // 散装参数
        Brand brand = mapper.selectByCondition("小","小",1);
        System.out.println("brand = " + brand);


        // 对象参数
        Brand query = new Brand();
        query.setBrandName("小");
        query.setCompanyName("小");
        query.setStatus(1);

        brand = mapper.selectByCondition(query);
        System.out.println("brand = " + brand);


        // Map参数
        HashMap<String, Object> map = new HashMap<>();
        map.put("brandName","小");
        map.put("companyName","小");
        map.put("status",1);
        brand = mapper.selectByCondition(map);
        System.out.println("brand = " + brand);
    }
}

image-20230720013658943

总结:

散装参数:参数数量超过1,需要使用@Param(SQL中的参数占位符的名称)

实体类封装参数:只需要保证SQL中的参数名和实体类属性名对应上,即可设置成功。

map集合参数:只需要保证SQL中的参数名和map集合的键的名称对应上,即可设置成功。

11-多条件-动态条件查询

    <select id="selectByCondition" resultMap="BrandResultMap">
        SELECT * FROM tb_brand 
        <where>
            <if test="brandName != null and brandName != ''">
                brand_name LIKE CONCAT('%', #{brandName}, '%')
            </if>

            <if test="companyName != null and companyName != ''">
                AND company_name LIKE CONCAT('%', #{companyName}, '%')
            </if>

            <if test="status != null">
                AND status = #{status}
            </if>
        </where>
    </select>

动态SQL:

if:用于判断参数是否有值,使用test属性进行条件判断

存在问题:第一个条件不需要逻辑运算法

解决方法:使用恒等式1=1或者<where>标签替换where关键字

12-单条件-动态条件查询

    <select id="selectByConditionWhen" resultMap="BrandResultMap">
        SELECT * FROM tb_brand
        <where>
            <choose>
                <when test="brandName != null and brandName != ''">
                    brand_name LIKE CONCAT('%', #{brandName}, '%')
                </when>

                <when test="companyName != null and companyName != ''">
                    AND company_name LIKE CONCAT('%', #{companyName}, '%')
                </when>

                <when test="status != null">
                    AND status = #{status}
                </when>
            </choose>
        </where>
    </select>

5-3-mybatis-添加、修改、删除

14-添加-主键返回

public class MybatisDemo06 {

    public static void main(String[] args) throws Exception {

        // 加载核心配置文件
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 获取sqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 这里获取到的是Proxy动态代理对象
        BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);

        // 需要插入的数据
        Brand brand = new Brand();
        brand.setBrandName("波导手机");
        brand.setCompanyName("波导手机有限公司");
        brand.setStatus(1);
        brand.setOrdered(1);
        brand.setDescription("手机中的战斗机");

        // 插入数据
       int row =  mapper.insertByBrand(brand);
       if (row>0) {
           System.out.println("插入成功");
           System.out.println("row = " + row);
       }

       // 提交事务
       sqlSession.commit();
    }
}

    /**
     * 插入品牌
     *
     * @param brand 品牌
     * @return int
     */
    int insertByBrand(Brand brand);
    <insert id="insertByBrand">
        insert into tb_brand(brand_name, company_name, ordered, description, status) values (#{brandName},
        #{companyName}, #{ordered}, #{description}, #{status})
    </insert>

运行:

image-20230720173338815

这时候,返回值是影响的行数,需要返回主键,修改一下:

    <insert id="insertByBrand" useGeneratedKeys="true" keyProperty="id">
        insert into tb_brand(brand_name, company_name, ordered, description, status) values (#{brandName},
        #{companyName}, #{ordered}, #{description}, #{status})
    </insert>

这样,插入后的id的值,会设置到原来的对象中:

image-20230720173801298

image-20230720173807937

15-修改-修改全部字段

    <update id="update">
        update tb_brand set
        brand_name = #{brandName},
        company_name = #{companyName},
        ordered = #{ordered},
        description = #{description},
        status = #{status}
        where id = #{id}
    </update>

16-修改-修改动态字段

    <update id="updateDynamic">
        update tb_brand
        <set>
            <if test="brandName != null and brandName != ''">
                brand_name = #{brandName},
            </if>
            <if test="companyName != null and companyName != ''">
                company_name = #{companyName},
            </if>
            <if test="ordered != null">
                ordered = #{ordered},
            </if>
            <if test="description != null and description != ''">
                description = #{description},
            </if>
            <if test="status != null">
                status = #{status}
            </if>
        </set>
        where id = #{id}
    </update>

17-删除-批量删除

    /**
     * 批量删除
     *
     * @param ids id
     */
    void deleteByIds(@Param("ids") List<Integer> ids);
    <delete id="deleteByIds">
        delete from tb_brand where id in
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>

如果在xml中的foreachcollection属性需要使用变量名称,则要加注解@Param("ids")

如果是数组,可以不加注解,在collection属性,使用array

19-参数传递

mybatis接口可以接受各种参数:

  • 单个参数
    • POJO
    • Map
    • Collection
    • 其他
  • 多个参数
    • 基本数据类型、String、包装类、等,需要使用@Param注解。为什么?

Mybatis提供了ParamNameResolver类来进行参数封装。

当mybatis发现接口参数是多个时,会创建Map集合,将参数封装进去,map需要对应的键值。

默认基本数据类型为:

map.put("param1",参数值1);
map.put("param2",参数值2);

如果写了@Param("key")

则会是:

map.put("key",参数值1);

看源码:ParamNameResolver类的getNamedParams方法

  public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      Object value = args[names.firstKey()];
      return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
    } else {
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

断点调试:

先不加注解:

image-20230727115305866

image-20230727150806880

加注解:

image-20230727150933737

原来的arg0等被替换了。在xml中,可以使用注解指定的参数,也可以使用param1等。

如果是集合,则会走wrapToMapIfCollection方法转成map:

  public static Object wrapToMapIfCollection(Object object, String actualParamName) {
    if (object instanceof Collection) {
      ParamMap<Object> map = new ParamMap<>();
      map.put("collection", object);
      if (object instanceof List) {
        map.put("list", object);
      }
      Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
      return map;
    } else if (object != null && object.getClass().isArray()) {
      ParamMap<Object> map = new ParamMap<>();
      map.put("array", object);
      Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
      return map;
    }
    return object;
  }

总结:

image-20230727151713985

20-注解开发

Loading...