JAVA框架-Mybatis中(代理、动态SQL和高级映射)

Mapper代理

在上一个博客中,我们直接利用session和<select>标签来执行sql的方式存在一些问题。

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

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

Mybatis提供了动态代理的方式,来解决上面的问题:MyBatis中本来由Executor(被代理对象)来完成sql的执行,现在由代理对象(自动生成)来代理Executor完成,代理对象会将我们的操作转交给Executor

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

我们继续上一篇博客的案例,先书写一个接口类

package mapper;
import Bean.Product;

public interface ProductsMapper {
    Product selectProductById(int id);
}

注意我们的目录结构是这样的:

JAVA框架-Mybatis中(代理、动态SQL和高级映射)

随后我们要记得修改ProductsMapper.xml中的标签以关联我们的Mapper和我们的接口文件:<mapper namespace="mapper.ProductsMapper">

接着我们书写测试代码:

@Test
public void MapperTest(){
    SqlSession session = factory.openSession(true);
    //getMapper方法是Mybatis提供的可将接口进行实现的方法,能够返回接口的实现类
    ProductsMapper mapper = session.getMapper(ProductsMapper.class);
    System.out.println(mapper.toString());
    //这样我们在书写代码的时候就能提前知道我们是否代码书写错误(因为要和我们的接口匹配,不然编译不会通过)
    Product product = mapper.selectProductById(3);
    System.out.println(product);
}

这样我们就实现了动态代理,可以看出对象mapper就是一个代理对象。

注意事项:

  • 必须保证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(映射器)

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

properties

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

JAVA框架-Mybatis中(代理、动态SQL和高级映射)

同时呢,我们还可以将jdbc的配置信息写在独立的文件中,这样能够进一步降低耦合性。

我们可以配置jdbc.properties位于resource包下

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

然后我们在这里引用

JAVA框架-Mybatis中(代理、动态SQL和高级映射)

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

typeAliases

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

动态SQL

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

if

<!-- 根据姓名或cid进行搜索-->
<select id="selectByNameAndCid" parameterType="product" resultType="product">
    select * from products where 1=1
        <if test="name != null">
            and name like ‘%${name}%‘
        </if>
        <if test="id != null">
            and cid = #{id}
        </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.selectByNameAndCid(condition);
    System.out.println(product);
    session.close();
}

其实从这里我们也能够看到,我们Mapper中写SQL的时候,传入参数实际要求的并不是太严格,只要传递的对象(无论是map ,list还是普通的对象)有相应的属性,mybatis就可以自动的去解析并寻找对应关系。

where

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

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

for each

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

<!-- 动态 SQL   for each  -->
<select id="selectByIDs" parameterType="Bean.Product" resultType="Bean.Product">
    <!-- select * from products where id in (1,2,3,4)-->
    select * from products where id in
    <foreach collection="ids"  open="(" item="i" close=")" separator=",">
        #{i}
    </foreach>
</select>
    
    
    
<!--
<if test="ids != null"> 这里不仅判断属性是否为空还判断集合中是否有元素
foreache 标签属性说明:
	强调:动态sql本质就是在拼接字符串,带着自己拼接sql的思路来编写动态sql会更好理解
  collection	要遍历的集合
  open				拼接的前缀
  close				拼接的后缀
  separator		拼接元素之间的分隔符
  item				遍历得到的临时变量名
  index				当前元素的索引(不常用)
-->
@Test
public void dynamicSQL_2(){
    SqlSession session = factory.openSession(true);
    ProductsMapper mapper = session.getMapper(ProductsMapper.class);
    Product p = new Product( );
    List list = new ArrayList();
    list.add(2);
    list.add(3);
    list.add(4);
    p.setIds(list);
    List<Product> products = mapper.selectByIDs(p);
    System.out.println(products);
}

set

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

<update id="updateByID" parameterType="Bean.Product">
    update products
    <set>
        <if test="pname != null and pname != ‘‘">
            name = #{name},
        </if>
        <if test="price != null and price > 0">
            price = #{price},
        </if>
        <if test="pdate != null">
            date = #{date},
        </if>
        <if test="cid != null and cid != ‘‘">
            cid = #{cid},
        </if>
    </set>
    where id = #{id}
</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.updateByID(product);
    System.out.println(product);
    session.commit();
    session.close();
}

include

Sql中可将重复的sql提取出来,使用时用include引用即可,最终达到sql重用的目的。其实就是替换了一下sql语句。

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

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

高级映射

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

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

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

自定义字段与属性的映射

如果我们表中某些字段的名称更改后,我们可以通过高级映射的方式实现查询(实际就是起了个别名,更多的应用在下面)

<!--自定义映射关系 id:该映射关系的标识    type:映射到的Bean类型此处为别名-->
<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>

其中 column 是查询结果返回中对应的哪个字段, property是mybatis返回Bean对象中的某个属性

关联查询

JAVA框架-Mybatis中(代理、动态SQL和高级映射)

两个表之间对应关系,分为一对一和一对多,而多对多则是三张表之间的关系,若掌握了两张表之间的一对多关系的处理,则多对多也就不是问题了,因为本质上多对多就是两个一对多组成的(比如上图老师和学生的关系)

一对一映射

下面我们首先来看一个一对多的关联查询,我们有用户和订单两个表如下:
JAVA框架-Mybatis中(代理、动态SQL和高级映射)

随后我们进行代码层面的书写,首先创建对应的Order和User两个Bean对象(对应的set和get方法就不写了):

package Bean;

import java.util.Date;
/**
 * Created by Jeason Luna on 2020/6/21 21:52
 */
public class Order {
    private int id , user_id;
    private int number;
    private Date createtime;
    private String note;
    private User user;
}

package Bean;

import java.util.Date;
import java.util.List;
/**
 * Created by Jeason Luna on 2020/6/21 21:56
 */
public class User {
    private int id;
    private String username;
    private Date birthday;
    private int sex;
    private String address;
}

然后书写代理接口:

package mapper;

import Bean.Order;

/**
 * Created by Jeason Luna on 2020/6/21 21:58
 */
public interface OrderMapper {

    Order selectByID(int id);
}

最后建立OrderMapper.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">
<!--namespace 用于多个Mapper出现相同的sql时区分不同包-->

<mapper namespace="mapper.OrderMapper">

    <resultMap id="order_map" type="Bean.Order" autoMapping="true">
        <!--association 用于一对一的映射-->
        <id property="id" column="oid" />
        <association property="user" javaType="Bean.User" autoMapping="true">
            <id property="id" column="uid" />
        </association>
    </resultMap>
    
    
    <select id="selectByID" parameterType="int" resultMap="order_map">
<!--        select * from orders where id = #{id}-->
        SELECT * , orders.id  oid , kuser.id uid FROM kuser JOIN orders ON kuser.id = orders.user_id
        WHERE orders.id = #{id}
    </select>

</mapper>

测试代码如下:

import Bean.Order;
import mapper.OrderMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

/**
 * Created by Jeason Luna on 2020/6/21 22:01
 */
public class test2 {

    private SqlSessionFactory factory;

    @Before
    public  void init() throws IOException {
        //获取的工厂构造器
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        //加载配置文件
        InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");
        //获得会话工厂
        factory = builder.build(stream);
    }

    @Test
    public void test1(){
        try(SqlSession session = factory.openSession(true)){
            OrderMapper orderMapper = session.getMapper(OrderMapper.class);
            Order order = orderMapper.selectByID(3);
            System.out.println(order);
        }
    }

}

输出:

Order{id=3, user_id=1, number=113, createtime=Wed May 27 00:00:00 CST 2020, note=‘null‘, user=User{id=1, username=‘王建森‘, birthday=Sun Jun 21 00:00:00 CST 2020, sex=1, address=‘黑龙江‘}}

一对多映射

我们还是使用上面的那两个数据库的表,这次我们统计一个用户有多少的订单。

首先,我们增加Bean.User对象的属性:

public class User {
    private int id;
    private String username;
    private Date birthday;
    private int sex;
    private String address;
    private List<Order> orders;
}

创建相应的动态代理:

package mapper;

import Bean.User;

/**
 * Created by Jeason Luna on 2020/6/22 10:42
 */
public interface UserMapper {
    User selectByID(int id);
}

编写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">
<!--namespace 用于多个Mapper出现相同的sql时区分不同包-->

<mapper namespace="mapper.UserMapper">

    <resultMap id="user_map" type="Bean.User" autoMapping="true">
        <id column="uid" property="id"/>
        <collection property="orders" javaType="list" ofType="Bean.Order" autoMapping="true">
            <id column="oid" property="id"/>
        </collection>
    </resultMap>


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

</mapper>

注意:当我们使用collection标签的时候,我们的内层和外层都需要指定相应的映射(自己指定自己也行),这是因为resultMap中如果不定义类似主键之类的能够区分每一条结果集的字段的话,会引起后面一条数据覆盖前面一条数据的现象。

如果我们不指定,Mybatis不会合并重复的主记录,进而报错如下(注释<id column="uid" property="id"/>):

org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 4

最后我们编写测试代码:

import Bean.Order;
import Bean.User;
import mapper.OrderMapper;
import mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

/**
 * Created by Jeason Luna on 2020/6/22 11:07
 */
public class test3 {

    private SqlSessionFactory factory;

    @Before
    public  void init() throws IOException {
        //获取的工厂构造器
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        //加载配置文件
        InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");
        //获得会话工厂
        factory = builder.build(stream);
    }

    @Test
    public void test1(){
        try(SqlSession session = factory.openSession(true)){
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.selectByID(2);
            System.out.println(user);
        }
    }
}

得到输出如下:

User{id=2, username=‘张建森‘, birthday=Thu Jun 11 00:00:00 CST 2020, sex=2, address=‘吉林‘, 

orders=[
    Order{id=1, user_id=2, number=111, createtime=Wed Jun 10 00:00:00 CST 2020, note=‘null‘, user=null}, 
    Order{id=7, user_id=2, number=167, createtime=Thu Nov 14 00:00:00 CST 2019, note=‘null‘, user=null}, 
    Order{id=8, user_id=2, number=188, createtime=Sat May 30 00:00:00 CST 2020, note=‘null‘, user=null}, 
    Order{id=9, user_id=2, number=199, createtime=Thu Feb 18 00:00:00 CST 2021, note=‘null‘, user=null}
]}

另外我们还可以使用子查询来实现上述功能,修改UserMapper.xml如下

<resultMap id="user_map2" type="Bean.User" autoMapping="true">
    <id column="id" property="id"/>
    <collection property="orders" javaType="list" ofType="Bean.Order" select="selectOrderByUserID" column="id">
    </collection>
</resultMap>


<select id="selectByID2" parameterType="int" resultMap="user_map2">
    select * from kuser where id = #{id}
</select>

<select id="selectOrderByUserID" parameterType="int" resultType="Bean.Order">
    select * from orders where user_id = #{id}
</select>

上面的代码中,property指定数据要放在Bean对象的哪个属性中,javaType是容器类型要和属性相对应,ofType是容器内的元素类型,select是子查询的id,column是要传给子查询的参数。

这样我们就实现了子查询的功能,先利用selectByID2来查询出id = 2(如果我们输入的参数是2的话)的用户信息,随后利用selectOrderByUserID将刚才得到的用户信息中的id号码传入,得到对应订单表的所有该用户的数据,Mybatis会帮我们合并重复数据,这样就实现了子查询。

JAVA框架-Mybatis中(代理、动态SQL和高级映射)

上一篇:oracle创建/删除 用户,表空间


下一篇:MySQL高可用之MHA(二)