• 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);
    }
    

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

    随后我们要记得修改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标签中读取需要的参数,使得配置文件各个部分更加独立

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

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

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

    然后我们在这里引用

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

    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对象中的某个属性

    关联查询

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

    一对一映射

    下面我们首先来看一个一对多的关联查询,我们有用户和订单两个表如下:

    随后我们进行代码层面的书写,首先创建对应的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会帮我们合并重复数据,这样就实现了子查询。

  • 相关阅读:
    善用性能工具进行SQL整体优化
    mysql use index () 优化查询的例子
    mysql优化 explain index
    mysql中explain用法和结果的含义
    MySQL运行状态show status中文详解
    Mysql运行状态查询命令及调优详解
    数据库工具——Navicat Premium使用技巧
    细说mysql索引
    对国家失望:汉末儒生集体沉默(儒家主张积极入世,以经国济世为己任的)
    韦尔股份:打造国际半导体设计行业领先企业(各种企业问题的问答)
  • 原文地址:https://www.cnblogs.com/JeasonIsCoding/p/13232689.html
Copyright © 2020-2023  润新知