Skip to content

Latest commit

 

History

History
534 lines (525 loc) · 24 KB

MyBatis.md

File metadata and controls

534 lines (525 loc) · 24 KB

搭建MyBatis

  • 创建核心配置文件(可从MyBatis官方文档获取)
    <?xml version="1.0" encoding="UTF-8" ?>
    <!-- 存在于MyBatis官方文档中 -->
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "https://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <!-- 核心配置文件中的标签是有序的,应当按顺序配置 -->
        <!-- 引入properties文件 -->
        <properties resource="jdbc.properties"/>
        <typeAliases>
            <!-- 配置别名,简化mapper.xml中的类名引用 -->
            <!-- alias默认为类名,且不区分大小写 -->
            <typeAlias type="indi.beta.pojo.User" alias="user"/>
            <!-- 将某个软件包下所有类都使用其类名作为别名,用于类过多的情况 -->
            <package name="indi.beta.pojo"/>
        </typeAliases>
        <environments default="development">
            <environment id="development">
                <!-- transactionManager: JDBC(原生的事务管理方式,手动提交) | MANAGED -->
                <transactionManager type="JDBC"/>
                <!-- dataSource: POOLED(使用连接池) | UNPOOLED(不使用连接池) | JNDI(使用上下文数据源) -->
                <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="org/mybatis/example/BlogMapper.xml"/>
        </mappers>
    </configuration>
  • 创建映射文件
    • 映射文件: java对象与数据库对象之间映射关系。
    • 一般来说,一张表对应了一个映射文件。推荐对每张表建立单独的mapper
    • 两个一致
      • 映射文件namespace与全类名必须一致
      • 映射文件中SQL语句的id与mapper接口中的方法名一致
    • 添加一条映射
      <?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="indi.beta.mapper.UserMapper">
          <insert id="insertUser">
              insert into t_table(id, name) values (3001, 'AlphaDog')
          </insert>
          <delete id="deleteUserById">
              delete from t_table where id=3001
          </delete>
          <!-- 查询语句必须设置resultType或rsultMap属性,从而实现查询结果与java类的映射 -->
          <!-- resultType用于数据表字段与java类属性一致的情况,resultMap用于不一致的情况 -->
          <select id="queryUserById" resultType="indi.beta.pojo.User">
              select * from t_table where id=15
          </select>
      </mapper>
    • 在mybatis配置中导入该映射
    <!-- 配置映射文件 -->
    <mappers>
        <mapper resource="mappers/UserMapper.xml"/>
        <!-- 将目录下所有mapper.xml全部导入,要求被导入的xml包名与对应的接口类包名相同,且文件名(不含后缀)相同 -->
        <package name="indi.beta.mapper"/>
    </mappers>
  • 执行
    // 加载核心配置文件
    InputStream resource = Resources.getResourceAsStream("mybatis-config.xml");
    // 获取SqlSessionFactoryBuilder
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    // 获取SqlSessionFactory
    SqlSessionFactory factory = builder.build(resource);
    // 获取SqlSession。可以为openSession提供参数true实现自动提交
    SqlSession sqlSession = factory.openSession();
    // 获取mapper接口对象
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    // 执行mapper中的方法
    int result = mapper.insertUser();
    sqlSession.commit();
    System.out.println("Row affected: " + result);
  • 配置log4j
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
    <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
        <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
            <param name="Encoding" value="UTF-8"/>
            <layout class="org.apache.log4j.PatternLayout">
                <param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \n"/>
            </layout>
        </appender>
        <logger name="java.sql">
            <level value="debug"/>
        </logger>
        <logger name="org.apache.ibatis">
            <level value="info"/>
        </logger>
        <root>
            <level value="debug"/>
            <appender-ref ref="STDOUT"/>
        </root>
    </log4j:configuration>
    <!-- <setting name="logImpl" value="LOG4J"/>添加到mybatis配置文件中 -->

MyBatis获取参数

  • 通过#{}或${}获取单个参数
    <!-- User queryUserById(int id) -->
    <select id="queryUserById" resultType="user">
        select * from t_table where id=#{id}
    </select>
    • #{}与其中包括的变量名无关,但不能缺省。推荐设置为方法的参数名
    • 字面量为字符串时,应当使用单引号将${}包裹起来
    • 获取单个字面量时,${}与#{}时等效的
    • ${}的底层实现是字符串拼接,#{}底层实现是占位符赋值。${}可能出现sql注入
    • #{}会被替换为?,如果sql中本身就含有sql,则会产生冲突
    • #{}会自动将值的左右两侧添加单引号,这是很多问题的根源
  • 获取多个参数(自动)
    <!-- User queryUserById(int id) -->
    <select id="checkUser" resultType="user">
        select * from t_table where id=#{arg0} and name=#{arg1}
    </select>
    • 使用arg作为键时,从0开始。使用param作为键时,从1开始。二者可以混用,但不推荐
    • 键的创建是自动的
    • 也可以使用${},依然需要注意字符串拼接问题
  • 获取多个参数(手动)
    <!-- User queryUserById(int id, String name) -->
    <select id="checkUser" resultType="user">
        select * from t_table where id=#{my_key_1} and name=#{my_key_2}
    </select>
    • 将方法的参数类型设置为Map,并将键设置为字符串
    • 在mapper.xml中使用自定义的键即可访问
  • 获取实体类对象(推荐)
    <!-- User insertUser(User user) -->
    <!-- User {int id; String name;} -->
    <select id="checkUser" resultType="user">
        select * from t_table where id=#{id} and name=#{name}
    </select>
    • 直接使用实体类的属性进行访问
    • 需要提供getter和setter -使用@Param注解为变量赋键(推荐)
    <!-- User checkUser(@Param("userId") int id, @Param("UserName") String name); -->
    <select id="checkUser" resultType="user">
        select * from t_table where id=#{userId} and name=#{userName}
    </select>
    • 即使使用了@Param注解,依然可以使用param作为键来获取参数

各种查询

  • 聚合函数
    • sql中的聚合函数在mybatis中依然是可用的,只是需要将resultType指定为相应的返回值类型
  • 将结果返回为Map集合
    <!-- Map<String, Objects> getUserByIdToMap(int id);-->
    <select id="getUserByIdToMap" resultType="Map">
        select * from t_table where id=#{id}
    </select>
    • 返回值的键为字段名,返回值的值为属性值
    • 返回记录有多条时,需要将方法返回值类型改为List<Map<String, Objects>>
    • 返回记录有多条时,可以在mapper方法前通过@MapKey注解将某个字段设置为键
  • 模糊查询
    • 由于#{}采用的是占位符赋值方式,在解析时首先被替换为?。在模糊查询中,该?会出现在字符串中,被认为是字符串的一部分,导致占位符无法被赋值
    • 解决方案1: 采用${}替换#{}
      <!-- List<User> getUserByLike(String name);-->
      <select id="getUserByLike" resultType="user">
          select * from t_table where name like '${name}%'
      </select>
    • 解决方案2: 采用sql的concat函数进行拼接
      <!-- List<User> getUserByLike(String name);-->
      <select id="getUserByLike" resultType="user">
          select * from t_table where name like concat(#{name}, '%')
      </select>
      ```xml
    • 解决方案3: 采用双引号将模糊符号包裹起来
      <!-- List<User> getUserByLike(String name);-->
      <select id="getUserByLike" resultType="user">
          select * from t_table where name like #{name}"%"
      </select>
  • 批量删除
    <!-- int batchDelete(String ids);-->
    <delete id="batchDelete">
        delete from t_table where id in (${ids})
    </delete>
    • 此时不能使用#{}, 因为它会将多个值拼接为1个字符串,导致sql总是找不到匹配的记录。当条件字段是数值型时,会报数值转换异常
  • 动态表名
    select * from ${t_name}
    • 动态表名只能使用${},因为表名不能为字符串,而#{}会自动添加单引号
  • 添加时获取自增主键
    • 由于insert的返回值是受影响行数,因此只能需要考虑将主键值通过mapper方法的参数传递出来
    <!-- void insertUserReturnAutoIncrementKey(User user);-->
    <insert id="insertUserReturnAutoIncrementKey" useGeneratedKeys="true" keyProperty="id">
        insert into t_table values(null, #{name})
    </insert>
    • 利用java传引用的特性,传入的参数不需要设置id,而会通过mybatis自动将其值设置为自增主键值
    • 通过java语句可以查看user被赋予的id

映射关系: 解决字段名与属性名不一致问题

  • 使用字段别名
    • 当字段名与属性名不一致时,该属性无法被赋值,因此最终结果为null
    • 通过将sql字段设置与属性名一致的别名可以解决该问题
  • 通过核心配置文件中的setting进行配置(官方的下划线转驼峰)
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    - _**要求属性名与字段名必须对应,除了命名规则导致的不一致外**_
  • 通过resultMap
    <!-- 配置映射规则 -->
    <!-- type为数据库对应的java类的全类名或alias -->
    <resultMap id="empResultMap" type="employee">
        <!-- id用于配置主键映射 -->
        <!-- propertu为属性名,column为字段名 -->
        <id property="employeeId" column="employee_id"/>
        <!-- result用于配置非主键映射 -->
        <result property="firstName" column="first_name"/>
        <result property="lastName" column="last_name"/>
        <result property="email" column="email"/>
        <result property="phoneNumber" column="phone_number"/>
        <result property="hireDate" column="hire_date"/>
        <result property="jobId" column="job_id"/>
        <result property="salary" column="salary"/>
        <result property="commissionPct" column="commission_pct"/>
        <result property="managerId" column="manager_id"/>
        <result property="departmentId" column="department_id"/>
    </resultMap>
    <!-- 使用resultMap代替resultType进行映射 -->
    <select id="getEmployeeById" resultMap="empResultMap">
        select * from employees where employee_id=#{id}
    </select>
  • 解决多对一(多个字段对应1个属性)的映射关系: 级联赋值
    <select id="getEmployeeWithJobHistoryById" resultMap="simplifiedEmployeeMap">
        select e.employee_id e_id, concat(e.first_name, e.last_name) name, j.employee_id j_e_id, j.start_date j_s_date, j.end_date j_e_date, j.job_id j_j_id, j.department_id j_d_id from employees e right join job_history j on e.employee_id = j.employee_id where e.employee_id=#{id};
    </select>
    
    <resultMap id="simplifiedEmployeeMap" type="simplifiedEmployee">
        <!-- SimplifiedEmployee包含id name jobHistory属性,其中jobHistory为JobHistory对象 -->
        <id property="id" column="eid"/>
        <result property="name" column="name"/>
        <result property="jobHistory.employeeId" column="j_e_id"/>
        <result property="jobHistory.startDate" column="j_s_date"/>
        <result property="jobHistory.endDate" column="j_e_date"/>
        <result property="jobHistory.jobId" column="j_j_id"/>
        <result property="jobHistory.departmentId" column="j_d_id"/>
    </resultMap>
  • 解决多对一(多个字段对应1个属性)的映射关系: association
    <select id="getEmployeeWithJobHistoryById" resultMap="simplifiedEmployeeMap">
          select e.employee_id e_id, concat(e.first_name, e.last_name) name, j.employee_id j_e_id, j.start_date j_s_date, j.end_date j_e_date, j.job_id j_j_id, j.department_id j_d_id from employees e right join job_history j on e.employee_id = j.employee_id where e.employee_id=#{id};
    </select>
    
    <resultMap id="simplifiedEmployeeMap" type="simplifiedEmployee">
        <id property="id" column="eid"/>
        <result property="name" column="name"/>
        <association property="jobHistory" javaType="indi.beta.bean.JobHistory">
            <result property="employeeId" column="j_e_id"/>
            <result property="startDate" column="j_s_date"/>
            <result property="endDate" column="j_e_date"/>
            <result property="jobId" column="j_j_id"/>
            <result property="departmentId" column="j_d_id"/>
        </association>
    </resultMap>
  • 解决多对一(多个字段对应1个属性)的映射关系: 分步查询
    • 建立第一个查询,获取第二个查询所需要的关键字
    <!-- SimplifiedEmployee getEmployeeWithDepartment(@Param("eid") int id);-->
    <select id="getEmployeeWithDepartment" resultMap="employeeWithDepartmentMap">
        select employee_id, concat(first_name, ' ', last_name) name, department_id from employees where employee_id=#{eid}
    </select>
    <resultMap id="employeeWithDepartmentMap" type="simplifiedEmployee">
        <id property="id" column="employee_id"/>
        <result property="name" column="name"/>
        <!-- Department getDepartmentById(@Param("did") int id);-->
        <!-- association中的column是连接条件,select是子查询对应的mapper方法的全路径 -->
        <association property="department" select="indi.beta.mapper.DepartmentMapper.getDepartmentById" column="department_id"/>
    </resultMap>
    • 建立第二个查询(子查询),将第一个查询中缺少的字段查询出来
    <select id="getDepartmentById" resultType="department">
        select * from departments where department_id=#{did}
    </select>
    • 延迟加载。如无必要,不执行子查询,从而提升查询效率
    <setting name="lazyLoadingEnabled" value="true"/>
    • 可通过association的属性fetchType="eager"单独设置某个子查询为非延迟加载
  • 解决一对多查询(某个属性的查询结果为多条记录): collection
    <select id="getDepartmentWithEmpById" resultMap="getDepartmentWithEmpByIdMap">
        select * from departments d left join employees e on d.department_id=e.department_id where d.department_id=#{did}
    </select>
    <resultMap id="getDepartmentWithEmpByIdMap" type="department">
        <id property="departmentId" column="department_id"/>
        <result property="departmentName" column="department_name"/>
        <result property="managerId" column="manager_id"/>
        <result property="locationId" column="location_id"/>
        <!-- 设置属性的属性名以及对应的java类。employees是department下的一个list集合 -->
        <collection property="employees" ofType="simplifiedEmployee">
            <id property="id" column="employee_id"/>
            <result property="name" column="first_name"/>
        </collection>
    </resultMap>
  • 解决一对多查询(某个属性的查询结果为多条记录): 分步查询
    • 主查询
    <select id="getDepartmentWithEmpById" resultMap="getDepartmentWithEmpByIdMap">
        select * from departments where department_id=#{did}
    </select>
    <resultMap id="getDepartmentWithEmpByIdMap" type="department">
        <id property="departmentId" column="department_id"/>
        <result property="departmentName" column="department_name"/>
        <result property="managerId" column="manager_id"/>
        <result property="locationId" column="location_id"/>
        <collection property="employees"
                    select="indi.beta.mapper.SimplifiedEmployeeMapper.getEmployeeOfDepartment"
                    column="department_id"/>
    </resultMap>
    • 子查询
    <select id="getEmployeeOfDepartment" resultMap="employeeMap">
        select employee_id id, concat(first_name, last_name) name, department_id from employees where department_id=#{did}
    </select>
    <resultMap id="employeeMap" type="simplifiedEmployee">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
    </resultMap>

动态SQL

  • if
    <!-- List<SimplifiedEmployee> getEmployeeByCondition(SimplifiedEmployee employee);-->
    <select id="getEmployeeByCondition" resultType="simplifiedEmployee">
        select employee_id id, concat(first_name, ' ', last_name) name, department_id from employees where 1=1
        <if test="id != null">
            employee_id=#{id}
        </if>
        <if test="departmentId != null">
            and department_id=#{departmentId}
        </if>
    </select>
    • if用于自适应添加条件
    • 1=1是恒成立条件,用于避免第一个if不满足时导致的"where and"语法错误
    • if中的属性来自于mapper方法的形参的属性
  • where
    <!-- List<SimplifiedEmployee> getEmployeeByCondition(SimplifiedEmployee employee);-->
    <select id="getEmployeeByCondition" resultType="simplifiedEmployee">
        select employee_id id, concat(first_name, ' ', last_name) name, department_id from employees
        <where>
            <if test="id != null">
                employee_id=#{id}
            </if>
            <if test="departmentId != null">
                and department_id=#{departmentId}
            </if>
        </where>
    </select>
    • where标签中有为真的条件时,自动在sql中追加where条件;否则不会生成where
    • where标签可以自适应删除条件连接符,例如and or,不需要再使用1=1
    • where不能将内容后的and or去除,因此十分不推荐将and或or写在if的末尾
  • trim
    <!-- List<SimplifiedEmployee> getEmployeeByCondition(SimplifiedEmployee employee);-->
    <select id="getEmployeeByCondition" resultType="simplifiedEmployee">
        select employee_id id, concat(first_name, ' ', last_name) name, department_id from employees
        <trim prefix="where" suffix="" prefixOverrides="and|or" suffixOverrides="and|or">
            <if test="id != null">
                employee_id=#{id}
            </if>
            <if test="departmentId != null">
                and department_id=#{departmentId}
            </if>
        </trim>
    </select>
    • 自动追加where,自动删除特定的开头或结尾
    • trim只会在内部存在内容时才会生效
    • prefixOverrides和suffixOverrides用于删除,多个值时使用"|"隔开即可
    • prefix和suffix用于添加
  • choose when otherwise
    <!-- List<SimplifiedEmployee> getEmployeeByCondition(SimplifiedEmployee employee);-->
    <select id="getEmployeeByCondition" resultType="simplifiedEmployee">
        select employee_id id, concat(first_name, ' ', last_name) name, department_id from employees
        <where>
            <choose>
                <when test="id != null and id != '' and id != 0">
                    employee_id=#{id}
                </when>
                <when test="departmentId != null and departmentId != 0">
                    department_id=#{departmentId}
                </when>
                <otherwise>
                    name=#{name}
                </otherwise>
            </choose>
        </where>
    </select>
    • choose when otherwise相当于java的if-else结构
    • 必须使用where进行包裹,否则会出现sql语句中where的缺失
    • 当有条件满足时,后续的所有条件都不再执行,也不会添加到sql中
  • foreach
    <delete id="deleteBatchEmployee">
        delete from t_table where id in 
        <foreach collection="eidList" item="eid" separator="," open="(" close=")">
            #{eid}
        </foreach>
    </delete>
    <insert id="insertBatchEmployee">
        insert into t_table values
        <foreach collection="employees" item="employee" separator=",">
            (null, #{employee.name})
        </foreach>
    </insert>
    • seperator: 设置分隔符,前后会自动添加空格
    • open: 设置左侧开始符号
    • close: 设置右侧关闭符号
    • collection: 集合名称
    • item: 集合中每次迭代的变量的临时名称
    • foreach会自动添加占位符"?"
    • 对于insert,不应当设置open="(" close=")",该操作是针对整体的,会导致sql语法错误。括号应当在foreach内部手动添加
  • sql
    <sql id="empWithDept">employee_id, concat(first_name, ' ', last_name) name, department_id</sql>
    • 将常用的查询字段进行提取,并使用别名进行替代
    <select id="getEmployeeWithDepartment" resultMap="employeeWithDepartmentMap">
        select <include refid="empWithDept"/> from employees where employee_id=#{eid}
    </select>
    • 在需要引用的地方使用使用别名代替复杂的字段

MyBatis缓存

  • 一级缓存(默认开启)
    • SqlSession级别。通过同一个SqlSession查询的数据会被缓存,下次访问时直接从缓存获取
    • 失效场景
      • 不同的SqlSession进行查询
      • 同一个SqlSession但查询条件不同
      • 同一个SqlSession的两次相同查询之间发生了增删改(无论是否发生在查询的记录上)
      • 使用了手动清空
  • 二级缓存
    • SqlSessionFactory级别。通过同一个工厂创建的Sqlsession查询的结果会被缓存,下次访问时直接从缓存获取
    • 二级缓存开启的条件
      • 配置文件中设置属性cacheEnabled="true"(默认为true)
      • 映射文件最前端设置
        • eviction: 缓存回收策略。LRU(最近最少使用,默认) | FIFO(先进先出) | SOFT(软引用) | WEAK(弱引用)
        • flushInterval: 刷新间隔(毫秒)。默认不设置,仅在调用语句时刷新
        • size: 引用数目。设置最大的缓存对象数
        • readOnly: 只读。设置为true时对相同的查询返回同一个实例,因此是高性能的。设置为false返回的是缓存对象的拷贝。默认为false
        • type: 设置二级缓存的实现方式。不配置则使用mybatis原生的二级缓存
      • 二级缓存必须在Sqlsession关闭或提交后有效(没有关闭或提交时保存在一级缓存中)
      • 查询结果所转换的实体类必须实现序列化的接口(Seri)
    • 二级缓存失效情况
      • 两次查询之间发生了增删改
    • 缓存命中率: 从缓存中查询的次数在总查询次数中的占比
  • 缓存查询的顺序:
    • 优先查询二级缓存,找不到则查询一级缓存,依然没有找到则查询数据库
    • SqlSession关闭后,一级缓存的数据会写入二级缓存

MyBatis逆向工程

  • 逆向工程旨在通过数据表生成对应的mapper、实体类、mapper.xml
  • 逆向工程会生成相应的Example类,在其中提供了针对每个字段的条件。通过new Example()创建条件并提供给selectByExample方法即可实现条件查询
    EmployeeExample example = new EmployeeExample();
    example.createCriteria().andSalaryBetween(3000.0, 3200.0);
    example.or().andSalaryBetween(6000.0, 6200.0);
    List<Employee> employees = mapper.selectByExample(example);
  • xxSelective方法与对应的非Selective方法的区别在于,前者将会避免修改类属性为null的字段,而后者将会把对应字段设置为null

自动分页

  • 依赖
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.2.0</version>
    </dependency>
  • 使用
    EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
    Page<Object> page = PageHelper.startPage(1, 5);   // 开启分页(当前页数,页面条目数)
    List<Employee> employees = mapper.selectByExample(null);
    PageInfo<Employee> pageInfo = new PageInfo<>(employees, 7);   // 获取分页信息(分页数据,当前导航分页的数量)
    • pageInfo提供了众多属性,能够极大地简化浏览器页面中超链接的设置