MyBatis进阶

Mapper代理

直接利用session+id来执行sql的方式存在一些问题

  • session执行sql时都需要提供要执行sql的id,而这个id是字符串类型,意味着id是否正确在编译期间是无法获知的,必须等到运行时才能发现错误,
  • sql需要的参数和返回值类都不明确,这也增加了出错的概率

理想的状况:像调用方法一样调用sql,既避免了直接写id的问题,也可以明确指定方法的参数类型和返回值类型

解决方案:

MyBatis通过动态代理来解决,简单的说动态代理(动态生成),就是在运行过程中自动产生一个对象,用它来代理原本已经存在的对象的方法

MyBatis中本来由Executor(被代理对象)来完成sql的执行,现在由代理对象(自动生成)来代理Executor完成,代理对象会将我们的操作转交给Executor

问题是:MyBatis怎么知道代理对象是什么样的对象呢?,这就需要为MyBatis提供Mapper接口,这个接口就是对mapper.xml中的sql语句的声明,与DAO层的接口一毛一样

使用步骤

  1. 创建接口类

    package com.yyh.mapper;
    import com.yyh.pojo.Products;
    public interface ProductsMapper {
        //根据name查询一个Products
        public Products selectProductByName(String name);
    }
  2. 提供响应的sql映射

    <mapper namespace="com.yyh.mapper.ProductsMapper">
        <select id="selectProductByName" parameterType="string" resultType="com.yyh.pojo.Products">
            select *from products where pname = #{name}
        </select>
    </mapper>
  3. 获取代理对象 执行方法完成操作

    @Test
    public void proxyTest(){
        SqlSession session = factory.openSession();
        ProductsMapper mapper = session.getMapper(ProductsMapper.class);
        Products product = mapper.selectProductByName("泰国咖喱");
        System.out.println(product);
        session.close();
    }

注意事项:

  • 必须保证mapper.xml中的namespace与接口的全限定名称一致
  • 方法的名称必须与对应的sql statement的id一致
  • 方法的参数必须与对应的sql statement的parameterType一致
  • 方法的返回值必须与对应的sql statement的resultType一致

XML配置

MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:

  • configuration(配置)
    • properties(属性)
    • settings(设置)
    • typeAliases(类型别名)
    • typeHandlers(类型处理器)
    • objectFactory(对象工厂)
    • plugins(插件)
    • environments(环境配置)
      • environment(环境变量)
        • transactionManager(事务管理器)
        • dataSource(数据源)
    • databaseIdProvider(数据库厂商标识)
    • mappers(映射器)

注意配置文件各个节点个层次是固定,需按照上述的顺序书写否则报错,

单独使用MyBatis的场景是很少的,后续都会将其与Spring进行整合,并由Spring来对MyBatis进行配置,加粗的为需要重点关注,其余的了解即可,若遇到特殊需求可查阅官方文档

属性(properties)

properties可从配置文件或是properties标签中读取需要的参数,使得配置文件各个部分更加独立

内部properties标签

image-20191229194337146

外部配置文件

jdbc.properties位于resource下

driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql:///mybatisDB?serverTimezone=Asia/Shanghai&characterEncoding=utf8
user = root
password = admin

引用方式

image-20191229194607724

当内部和外部属性出现同名时,则优先使用外部的;

别名(typeAliases)

typeAliases用于为Java的类型取别名,从而简化mapper中类名的书写

为某个类定义别名

image-20191229195926617

在mapper.xml中就可以直接使用别名 不区分大小写

image-20191229195501526

当指定package时将批量为包下所有类指定别名为类名小写

<typeAliases>
    <package name="com.yyh.pojo"/>
</typeAliases>

使用package批量设置时很容易出现别名冲突,这是就需要使用@Alias注解来为冲突的类单独设置别名

@Alias("products1")
public class Products {
    private int pid;
    private String pname;
    private float price;
    private Date pdate;
    private String cid;
.....}

下面列出MyBatis已存在的别名

_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
object Object
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

映射(Mappers)

在mapper.xml文件中定义sql语句后,就必须让MyBatis知道,到哪里去找这些定义好的sql,这就需要在配置文件中指出要加载的mapper的位置;MyBatis支持4种方式来加载mapper文件

 <mappers>
<!--        1.指定资源文件的相对目录  相对于classpath
            maven项目会自动将java和resources都添加到classpath中
            所以相对与resources来指定路径即可-->
        <mapper resource="mapper/ProductsMapper.xml"/>

<!--        2.指定文件的绝对路径,MyBatis支持但是一般不用-->
        <mapper url="file:///Users/jerry/Downloads/MYB/src/main/resources/mapper/ProductsMapper.xml"/>

<!--        3.通过指定接口 让MyBatis自动去查找对应的Mapper
            这种方式要求映射文件和接口处于同一目录下,并且名称相同
            要保证上述要求只需要在resources下创建于接口包名相同的目录即可
            注意:运行前先clean,否则可能因为之前已经存在target而不会重新编译,导致无法加载新建的mapper文件
            -->
        <mapper class="com.yyh.mapper.ProductsMapper"/>

<!--        4.指定包名称,扫描包下所有接口和映射文件
            这种方式同样要求映射文件和接口处于同一目录下,并且名称相同-->
        <package name="com.yyh.mapper"/>
    </mappers>

第3,4种方式映射文件目录示例:

image-20191231104520663

动态SQL

动态SQL指的是SQL语句不是固定死的,可以根据某些条件而发生相应的变化,这样的需求非常多见

例如:页面提供的搜索功能,要根据用户给出的一个或多个条件进行查询

image-20191231120319174

在JDBC时代,我们通过Java代码来判断某个条件是否有效然后拼接SQL语句,这是非常繁琐的,MyBatis的动态SQL很好的解决了这个问题,使判断逻辑变得简洁直观;

在Mapper中通过标签来完成动态SQL的生成

1. if

从页面接受参数后我们会将参数打包为对象,然后将对象作为参数传给MyBatis执行查询操作,sql语句需要根据是否存在参数而动态的生成

mapper:

<!-- 根据姓名或cid进行搜索-->
<select id="searchProducts" parameterType="products" resultType="products">
    select *from products
            where 1=1
        <if test="pname != null">
            and pname like '%${pname}%'
        </if>
        <if test="cid != null">
            and cid = #{cid}
        </if>
</select>

测试:

@Test
public void searchTest(){
    SqlSession session = factory.openSession();
    ProductsMapper mapper = session.getMapper(ProductsMapper.class);
    //查询条件对象
    Products condition = new Products();
    condition.setName("新疆");
    condition.setCid("s001");
    //执行查询
    List<Products> product = mapper.searchProducts(condition);
    System.out.println(product);
    session.close();
}

where1=1用于保持sql的正确性,当不存在条件时,则查询全部

2.where

where的作用就是用于取出上面的where 1=1,因为这会让人看起来产生疑惑,其作用是将内部语句中的第一个and去除

<select id="searchProducts" parameterType="products" resultType="products">
    select *from products
    <where>
        <if test="pname != null">
            and pname like '%${pname}%'
        </if>
        <if test="cid != null">
            and cid = #{cid}
        </if>
    </where>
</select>

3. foreach

当一个条件中中需要需要多个参数时则需要将多个参数拼接到一起,例如: in, not in

mapper:

<select id="searchProducts" parameterType="products" resultType="products">
    select *from products
    <where>
        <if test="pname != null">
            and pname like '%${pname}%'
        </if>
        <if test="cid != null">
            and cid = #{cid}
        </if>
        <if test="ids != null">
            <foreach collection="ids" open="and pid in (" close=")" separator="," item="id" index="i">
                #{id}
            </foreach>
        </if>
    </where>
</select>
<!--
<if test="ids != null"> 这里不仅判断属性是否为空还判断集合中是否有元素
foreache 标签属性说明:
    强调:动态sql本质就是在拼接字符串,带着自己拼接sql的思路来编写动态sql会更好理解
  collection    要遍历的集合
  open              拼接的前缀
  close             拼接的后缀
  separator     拼接元素之间的分隔符
  item              遍历得到的临时变量名
  index             当前元素的索引(不常用)
-->

测试代码:

@Test
public void searchTest(){
    SqlSession session = factory.openSession();
    ProductsMapper mapper = session.getMapper(ProductsMapper.class);
    //查询条件对象
    Products condition = new Products();
    int[] ids = new int[]{1,2,3,4,5,};
    condition.setIds(ids);
    //执行查询
    List<Products> product = mapper.searchProducts(condition);
    System.out.println(product);
    session.close();
}

注意需要为POJO(Products)对象增加ids属性

4. set

set标签用于更新语句,当同事要更新多个字段时,我们需要留意当前是否是最后一个set,避免在后面出现,符号,使用set标签后可自动去除最后的逗号

mapper:

<update id="updateProductTest" parameterType="products">
    update products
    <set>
        <if test="pname != null and pname != ''">
            pname = #{pname},
        </if>
        <if test="price != null and price > 0">
            price = #{price},
        </if>
        <if test="pdate != null">
            pdate = #{pdate},
        </if>
        <if test="cid != null and cid != ''">
            cid = #{cid},
        </if>
    </set>
    where pid = #{pid}
</update>

测试代码:

@Test
public void updateTest2(){
    SqlSession session = factory.openSession();
    ProductsMapper mapper = session.getMapper(ProductsMapper.class);
    //获取已有对象
    Products product = mapper.selectProductById(7);
    product.setPname("云南小土豆");
    product.setPrice(10.5f);
    //执行更新
    mapper.updateProductTest(product);
    System.out.println(product);
    session.commit();
    session.close();
}

5. sql与include

Sql中可将重复的sql提取出来,使用时用include引用即可,最终达到sql重用的目的。 例如可以吧sql语句中的字段列表提取出来作为通用的sql片段。然后在sql语句中使用 include 节点引用这个sql片段。

<!--提取片段-->
<sql id="fields">pid,pname,price,cid</sql>

<select id="includeTest" resultType="products">
    select
    <include refid="fields"/> <!-- 引用片段-->
    from products
</select>

高级映射

在一些情况下数据库的记录和POJO对象无法直接映射,包括两种情形:

  • 数据库字段与POJO字段名称不同(可以避免);
  • 关联查询时,需要将关联表的数据映射为另一个类型的POJO(一对一),或List中(一对多);

在MyBatis中通过resultMap来完成自定义映射

1.自定义字段与属性映射

先将Products表中的字段更名为p_xxx,如下所示:

image-20191231144158478

mapper:

<!--自定义映射关系 id:该映射关系的标识    type:映射到的POJO类型此处为别名-->
<resultMap id="product_resultMap" type="products">
    <!--主键-->
    <id column="p_id" property="pid"/>
    <!--其他字段-->
    <result column="p_name" property="pname"/>
    <result column="p_price" property="price"/>
    <result column="p_date" property="pdate"/>
    <result column="p_cid" property="cid"/>
</resultMap>
<!--引用映射关系-->
<select id="selectProductsCustomMapping" resultMap="product_resultMap">
    select *from products
</select>

2. 关联查询

2.1 关联关系

两个表之间记录的对应关系,分为一对一和一对多,而多堆多则是三张表之间的关系,若掌握了两张表之间的一对多关系的处理,则多堆多也就不是问题了,因为本质上多对多就是两个一对多组成的

案例表:

image-20191231152140019

这两张表之间存在一对一和一对多

站在user表角度来看 一个用户可能对应多个订单即一对一

站在order表角度来看 一个订单只能对应一个用户即一对多

POJO类:

Order.java

import java.util.Date;
public class Order {
    private int id,user_id;
    private String number;
    private Date createtime;
    private String note;
    private User user;
      //get/set....
}

User.java

import java.util.Date;
import java.util.List;
public class User {
    private int id;
    private String name;
    private Date birthday;
    private String sex,address;
    private List<Order> orders;
    public User() {
    }
  //get/set....
}

2.2 一对一映射

需求:根据订单编号查询订单信息以及用户名称和地址

sql语句为:

select  o.*,u.username,u.address 
from orders o  join kuser u 
on o.user_id = u.id 
where o.id = 8;
#要查询的字段根据需求来定,这里只需要查询用户的姓名和地址

OrdersMapper.xml:

<!--自定义映射-->
<resultMap id="order_resultMap" type="com.yyh.pojo.Order">
<!--orders表映射-->
    <id column="id" property="id"/>
    <result column="number" property="number"/>
    <result column="createtime" property="createtime"/>
    <result column="note" property="note"/>
    <!--关联的user表映射-->
    <association property="user" javaType="user">
        <id column="user_id" property="id"/>
        <result column="username" property="name"/>
        <result column="address" property="address"/>
    </association>
</resultMap>

<select id="selectOrderByID" parameterType="int" resultMap="order_resultMap">
    select
        o.*,u.username,u.address
    from
        orders o  join kuser u
    on
        o.user_id = u.id
    where
        o.id = #{oid};
</select>

测试代码:

@Test
public void test(){
    SqlSession session = factory.openSession();
    OrdersMapper mapper = session.getMapper(OrdersMapper.class);//记得提供映射接口
    Order order = mapper.selectOrderByID(8);
    System.out.println(order);
    session.close();
}

补充:当连接查询出现重复字段时如:两个表都有id字段,MyBatis简单的取第一个匹配的字段值,很多时候这是不正确的,我们可以给这个重复的字段取个别名来避免,像这样:

<select id="selectOrderByID" parameterType="int" resultMap="order_resultMap">
    select
        u.*,o.*,o.id oid
    from
        orders o  join kuser u
    on
        o.user_id = u.id
    where
        o.id = #{oid};
</select>

理所当然的 在resultMap中则使用oid来进行映射

<id column="oid" property="id"/>

2.3 一对多映射

需求:根据用户编号查询用户信息以及用户所有订单信息

sql语句为:

 select u.*,o.*,o.id oid
 from kuser u left join orders o
 on o.user_id = u.id
 where u.id = 1;

UserMapper.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.yyh.mapper.UserMapper">

    <!--自定义映射-->
    <resultMap id="user_resultMap" type="user" autoMapping="true">
        <result column="username" property="name"/>
        <collection property="orders"  ofType="order" autoMapping="true">
            <result column="oid" property="id"/>
        </collection>
    </resultMap>

    <select id="selectUserByID" parameterType="int" resultMap="user_resultMap">
        select
            u.*,o.*,o.id oid
        from
            kuser u left join orders o
        on
            o.user_id = u.id
        where
            u.id = #{uid}
        </select>
</mapper>

autoMapping="true" 将自动映射字段名与属性名能对应的字段,我们只需要添加对应不上的即可

测试代码:

@Test
public void test2(){
    SqlSession session = factory.openSession();
    UserMapper mapper = session.getMapper(UserMapper.class);
    User user = mapper.selectUserByID(1);
    System.out.println(user);
    session.close();
}

注意:无论如何在collection标签中必须至少存在一个手动映射字段否则,将不会合并重复的主记录(user) 按照官方的说法,建议将手动映射id字段,可提高整体性能:去看看

另外collection标签中 ofType用于指定元素的类型 javaType指定容器类型

2.4嵌套映射,select

当关联查询非常复杂时,可以用嵌套的select,其原理是在映射复杂数据时执行另一个select来完成

<resultMap id="order_resultMap2" type="Order" autoMapping="true">
    <id column="id" property="id"/>
  <!-- 指定嵌套查询 column是传给内层查询的参数 -->
    <association property="user" column="user_id" select="selectUserByUid" javaType="user"/>
</resultMap>

<!-- 外层查询-->
<select id="selectOrderByID2" parameterType="int" resultMap="order_resultMap2">
    select
        *
    from
        orders
    where
        id = #{id}
</select>

<!-- 嵌套查询-->
<select id="selectUserByUid" parameterType="int" resultType="user">
    select *from kuser where id = #{id}
</select>

这种方式同样适用于一对多的关联关系

<!--自定义映射-->
<resultMap id="user_resultMap2" type="user" autoMapping="true">
  <result column="username" property="name"/>
  <collection property="orders"  ofType="order" select="selectOrderByUserID" column="id">
    <id column="id" property="id"/>
  </collection>
</resultMap>

<select id="selectUserByID2" parameterType="int" resultMap="user_resultMap2">
  select
  *
  from
  kuser
  where
  id = #{uid}
</select>

<select id="selectOrderByUserID" resultType="order" parameterType="int">
  select *from orders where user_id = #{uid}
</select>

使用select 与不用select的区别:

不用select时对应的sql语句为关联查询,一次性返回所有数据,然后对其进行去重.提取

使用select时,sql语句多为单表查询,使用外层查询的某个字段作为内层查询的参数,是两条单独的sql语句

下一章:注解开发

posted @ 2019-12-31 19:26  CoderJerry  阅读(...)  评论(...编辑  收藏