• 【JavaWeb的从0到1构建知识体系(六)】认识Mybatis(下)


    认识Mybatis

    使用注解开发(凡是配置文件的内容都可以用java代码实现,注解或java的配置类)

    在之前的开发中,我们已经体验到Mybatis为我们带来的便捷了,我们只需要编写对应的映射器,并将其绑定到一个接口上,即可直接通过该接口执行我们的SQL语句,极大的简化了我们之前JDBC那样的代码编写模式。那么,能否实现无需xml映射器配置,而是直接使用注解在接口上进行配置呢?答案是可以的,也是现在推荐的一种方式(也不是说XML就不要去用了,由于Java 注解的表达能力和灵活性十分有限,可能相对于XML配置某些功能实现起来会不太好办,但是在大部分场景下,直接使用注解开发已经绰绰有余了

    首先我们来看一下,使用XML进行映射器编写时,我们需要现在XML中定义映射规则和SQL语句,然后再将其绑定到一个接口的方法定义上,然后再使用接口来执行:

    <insert id="addStudent">
        insert into student(name, sex) values(#{name}, #{sex})
    </insert>
    
    • 1
    • 2
    • 3
    int addStudent(Student student);
    
    • 1

    而现在,我们可以直接使用注解来实现,每个操作都有一个对应的注解:

    @Insert("insert into student(name, sex) values(#{name}, #{sex})")
    int addStudent(Student student);
    
    • 1
    • 2

    当然,我们还需要修改一下配置文件中(mybatis-config.xml)的映射器注册:

    <mappers>
        <mapper class="com.test.mapper.MyMapper"/>//属性就不再是resources了。而是接口的全类名
        <!--  也可以直接注册整个包下的 <package name="com.test.mapper"/>  -->
    </mappers>
    
    • 1
    • 2
    • 3
    • 4

    通过直接指定Class,来让Mybatis知道我们这里有一个通过注解实现的映射器。

    我们接着来看一下,如何使用注解进行自定义映射规则

    @Results({
            @Result(id = true, column = "sid", property = "sid"),
            @Result(column = "sex", property = "name"),
            @Result(column = "name", property = "sex")
    })
    @Select("select * from student")
    List<Student> getAllStudent();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    直接通过@Results注解,就可以直接进行配置了,此注解的value是一个@Result注解数组,每个@Result注解都都一个单独的字段配置,其实就是我们之前在XML映射器中写的:

    <resultMap id="test" type="Student">
        <id property="sid" column="sid"/>
        <result column="name" property="sex"/>    
      	<result column="sex" property="name"/>
    resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    现在我们就可以通过注解来自定义映射规则了。那么如何使用注解来完成复杂查询呢?我们还是使用一个老师多个学生的例子:

    @Results({
            @Result(id = true, column = "tid", property = "tid"),
            @Result(column = "name", property = "name"),
            @Result(column = "tid", property = "studentList", many =
                @Many(select = "getStudentByTid")
            )
    })
    @Select("select * from teacher where tid = #{tid}")
    Teacher getTeacherBySid(int tid);
    
    @Results({
            @Result(id = true, column = "sid", property = "sid"),
            @Result(column = "sex", property = "name"),
            @Result(column = "name", property = "sex")
    })
    @Select("select * from student inner join teach on student.sid = teach.sid where tid = #{tid}")
    List<Student> getStudentByTid(int tid);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们发现,多出了一个子查询,而这个子查询是单独查询该老师所属学生的信息,而子查询结果作为@Result注解的一个many结果,代表子查询的所有结果都归入此集合中(也就是之前的collection标签),注意最后一个@Result注解里的column="tid"表示这个集合里面所有的学生都属于这个老师。

    <resultMap id="asTeacher" type="Teacher">
        <id column="tid" property="tid"/>
        <result column="tname" property="name"/>
        <collection property="studentList" ofType="Student">
            <id property="sid" column="sid"/>
            <result column="name" property="name"/>
            <result column="sex" property="sex"/>
        collection>
    resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    同理,@Result也提供了@One子注解来实现多对一的关系表示,类似于之前的assocation标签:

    @Results({
            @Result(id = true, column = "sid", property = "sid"),
            @Result(column = "sex", property = "name"),
            @Result(column = "name", property = "sex"),
            @Result(column = "sid", property = "teacher", one =
                @One(select = "getTeacherBySid")
            )
    })
    @Select("select * from student")
    List<Student> getAllStudent();
    @Results({
    	@Result(id = true,column = "tid",property = "tid"),
    	@Result(column = "tname",property = "name")
    })
    @Select("select * from teacher inner join teach on teacher.tid = teach.tid where sid = #{sid}")
    Teacher getTeacherBySid(int sid);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果现在我希望直接使用注解编写SQL语句但是我希望映射规则依然使用XML来实现,这时该怎么办呢?

    <resultMap id="test" type="Student">
        <id column="sid" property="sid"/>
        <result column="name" property="name"/>
        <result column="sex" property="sex"/>
        <association property="teacher" javaType="Teacher">
            <id column="tid" property="tid"/>
            <result column="tname" property="name"/>
        association>
    resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    @ResultMap("test")
    @Select("select * from student")
    List<Student> getAllStudent();
    
    • 1
    • 2
    • 3

    提供了@ResultMap注解,直接指定ID即可,这样我们就可以使用XML中编写的映射规则了。

    那么如果出现之前的两个构造方法的情况,且没有任何一个构造方法匹配的话,该怎么处理呢?

    @Data
    @Accessors(chain = true)
    public class Student {
    
        public Student(int sid){
            System.out.println("我是一号构造方法"+sid);
        }
    
        public Student(int sid, String name){
            System.out.println("我是二号构造方法"+sid+name);
        }
    
        private int sid;
        private String name;
        private String sex;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    因为这里sql返回了三个字段,所以已有的两个构造方法一个都不匹配,那我们只能自己写配置(或注解)告诉Mybatis框架使用哪一个。

    我们可以通过@ConstructorArgs注解来指定构造方法:
    值得注意的是,指定构造方法后,若此字段被填入了构造方法作为参数,将不会通过反射给字段单独赋值,而构造方法中没有传入的字段,依然会被反射赋值

    @ConstructorArgs({
            @Arg(column = "sid", javaType = int.class),
            @Arg(column = "name", javaType = String.class)
    })
    @Select("select * from student where sid = #{sid} and sex = #{sex}")
    Student getStudentBySidAndSex(@Param("sid") int sid, @Param("sex") String sex);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    得到的结果和使用constructor标签效果一致。

    我们发现,当参数列表中出现两个以上的参数时,会出现错误:

    @Select("select * from student where sid = #{sid} and sex = #{sex}")
    Student getStudentBySidAndSex(int sid, String sex);
    
    • 1
    • 2
    Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: 
    ### Error querying database.  Cause: org.apache.ibatis.binding.BindingException: Parameter 'sid' not found. Available parameters are [arg1, arg0, param1, param2]
    ### Cause: org.apache.ibatis.binding.BindingException: Parameter 'sid' not found. Available parameters are [arg1, arg0, param1, param2]
    	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:153)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76)
    	at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:87)
    	at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145)
    	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86)
    	at com.sun.proxy.$Proxy6.getStudentBySidAndSex(Unknown Source)
    	at com.test.Main.main(Main.java:16)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    原因是Mybatis不明确到底哪个参数是什么,因此我们可以添加@Param来指定参数名称:

    @Select("select * from student where sid = #{sid} and sex = #{sex}")
    Student getStudentBySidAndSex(@Param("sid") int sid, @Param("sex") String sex);
    
    • 1
    • 2

    所以,就算形参的名字个#{}里的一样也会报错。

    探究:要是我两个参数一个是基本类型一个是对象类型呢?

    System.out.println(testMapper.addStudent(100, new Student().setName("小陆").setSex("男")));
    
    • 1
    @Insert("insert into student(sid, name, sex) values(#{sid}, #{name}, #{sex})")
    int addStudent(@Param("sid") int sid, @Param("student")  Student student);
    
    • 1
    • 2

    那么这个时候,就出现问题了,Mybatis就不能明确这些属性是从哪里来的:

    ### SQL: insert into student(sid, name, sex) values(?, ?, ?)
    ### Cause: org.apache.ibatis.binding.BindingException: Parameter 'name' not found. Available parameters are [student, param1, sid, param2]
    	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:196)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:181)
    	at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62)
    	at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145)
    	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86)
    	at com.sun.proxy.$Proxy6.addStudent(Unknown Source)
    	at com.test.Main.main(Main.java:16)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    那么我们就通过参数名称.属性的方式去让Mybatis知道我们要用的是哪个属性

    @Insert("insert into student(sid, name, sex) values(#{sid}, #{student.name}, #{student.sex})")
    int addStudent(@Param("sid") int sid, @Param("student")  Student student);
    
    • 1
    • 2

    那么如何通过注解控制缓存机制呢?

    @CacheNamespace(readWrite = false)
    public interface MyMapper {
    
        @Select("select * from student")
        @Options(useCache = false)
        List<Student> getAllStudent();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用@CacheNamespace注解直接定义在接口上即可,然后我们可以通过使用@Options来控制单个操作的二级缓存启用

    个人感觉:能使用配置文件还是使用配置文件。方便且容易修改。

    探究Mybatis的动态代理机制

    在探究动态代理机制之前,我们要先聊聊什么是代理:其实顾名思义,就好比我开了个大棚,里面栽种的西瓜,那么西瓜成熟了是不是得去卖掉赚钱,而我们的西瓜非常多,一个人肯定卖不过来,肯定就要去多找几个开水果摊的帮我们卖,这就是一种代理。实际上是由水果摊老板在帮我们卖瓜,我们只告诉老板卖多少钱,而至于怎么卖的是由水果摊老板决定的。
    img
    那么现在我们来尝试实现一下这样的类结构,首先定义一个接口用于规范行为:

    public interface Shopper {
    
        //卖瓜行为
        void saleWatermelon(String customer);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后需要实现一下卖瓜行为,也就是我们要告诉老板卖多少钱,这里就直接写成成功出售:

    public class ShopperImpl implements Shopper{
    
        //卖瓜行为的实现
        @Override
        public void saleWatermelon(String customer) {
            System.out.println("成功出售西瓜给 ===> "+customer);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最后老板代理后肯定要用自己的方式去出售这些西瓜,成交之后再按照我们告诉老板的价格进行出售:

    public class ShopperProxy implements Shopper{
        //这就是被代理的对象
        private final Shopper impl;
    
        public ShopperProxy(Shopper impl){
            this.impl = impl;
        }
    
        //代理卖瓜行为
        @Override
        public void saleWatermelon(String customer) {
            //首先进行 代理商讨价还价行为
            System.out.println(customer + ":哥们,这瓜多少钱一斤啊?");
            System.out.println("老板:两块钱一斤。");
            System.out.println(customer + ":你这瓜皮子是金子做的,还是瓜粒子是金子做的?");
            System.out.println("老板:你瞅瞅现在哪有瓜啊,这都是大棚的瓜,你嫌贵我还嫌贵呢。");
            System.out.println(customer + ":给我挑一个。");
    
            impl.saleWatermelon(customer);   //讨价还价成功,进行我们告诉代理商的卖瓜行为,被代理的方法被代理的方法进行了增强
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    现在我们来试试看:

    public class Main {
        public static void main(String[] args) {
            Shopper shopper = new ShopperProxy(new ShopperImpl());
            shopper.saleWatermelon("小强");//其实就是对方法进行了增强。代理者的方法对被代理的方法进行增强
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样的操作称为静态代理,也就是说我们需要提前知道接口的定义并进行实现才可以完成代理,而Mybatis这样的是无法预知代理接口的(就是我们写的mapper接口),我们就需要用到动态代理。

    JDK提供的反射框架就为我们很好地解决了动态代理的问题,在这里相当于对JavaSE阶段反射的内容进行一个补充。

    public class ShopperImpl implements Shopper{
    
        //卖瓜行为的实现
        @Override
        public void saleWatermelon(String customer) {
            System.out.println("成功出售西瓜给 ===> "+customer);
        }
    }
    
    public class ShopperProxy implements InvocationHandler {
        //被代理的目标对象
        Object target;
        public ShopperProxy(Object target){
            this.target = target;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String customer = (String) args[0];
            System.out.println(customer + ":哥们,这瓜多少钱一斤啊?");
            System.out.println("老板:两块钱一斤。");
            System.out.println(customer + ":你这瓜皮子是金子做的,还是瓜粒子是金子做的?");
            System.out.println("老板:你瞅瞅现在哪有瓜啊,这都是大棚的瓜,你嫌贵我还嫌贵呢。");
            System.out.println(customer + ":行,给我挑一个。");
            return method.invoke(target, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    通过实现InvocationHandler来成为一个动态代理,我们发现它提供了一个invoke方法,用于调用被代理对象的方法并完成我们的代理工作现在就可以通过 Proxy.newProxyInstance来生成一个动态代理类:

    public static void main(String[] args) {
        Shopper impl = new ShopperImpl();
        Shopper shopper = (Shopper) Proxy.newProxyInstance(impl.getClass().getClassLoader(),
                impl.getClass().getInterfaces(), new ShopperProxy(impl));
        shopper.saleWatermelon("小强");
      	System.out.println(shopper.getClass());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    通过打印类型我们发现,就是我们之前看到的那种奇怪的类:class com.sun.proxy.$Proxy0,因此Mybatis其实也是这样的来实现的(肯定有人问了:Mybatis是直接代理接口啊,你这个不还是要把接口实现了吗?)那我们来改改,现在我们不代理任何类了,直接做接口实现:

    public class ShopperProxy implements InvocationHandler {
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String customer = (String) args[0];
            System.out.println(customer + ":哥们,这瓜多少钱一斤啊?");
            System.out.println("老板:两块钱一斤。");
            System.out.println(customer + ":你这瓜皮子是金子做的,还是瓜粒子是金子做的?");
            System.out.println("老板:你瞅瞅现在哪有瓜啊,这都是大棚的瓜,你嫌贵我还嫌贵呢。");
            System.out.println(customer + ":行,给我挑一个。");
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    public static void main(String[] args) {
        Shopper shopper = (Shopper) Proxy.newProxyInstance(Shopper.class.getClassLoader(),
                new Class[]{ Shopper.class },   //因为本身就是接口,所以直接用就行
                new ShopperProxy());
        shopper.saleWatermelon("小强");
        System.out.println(shopper.getClass());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    就我目前的水平,jdk底层的动态代理都似懂非懂,更不要说去看Mybatis的源码了。就目前我能说的就是:框架使用了JDK的自带的动态代理,代理的是mapper接口,还记得我们获取mapper代理类是传入了mapper.class接口具体的原理以后背八股文吧。【先完成再完美

     try(SqlSession sqlSession2 = MybatisUtil.getSession(true)){
                TestMapper testMapper2 = sqlSession2.getMapper(TestMapper.class);
                student2 = testMapper2.getStudentBySid(1);
            }
    
    • 1
    • 2
    • 3
    • 4

    Mybatis的学习差不多就到这里为止了,不过,同样类型的框架还有很多,Mybatis属于半自动框架,SQL语句依然需要我们自己编写,虽然存在一定的麻烦,但是会更加灵活,而后面我们还会学习JPA,它是全自动的框架,你几乎见不到SQL的影子!

  • 相关阅读:
    ubuntu20安装nginx支持多站点及代理配置
    双十一期间高预算广告增加,开发者如何精细化运营才能达到抢量增收目标?
    【OpenCV】- 模板匹配(浩瀚星空只为寻找那一抹明月)
    SSM客户管理系统CRM开发mysql数据库web结构java编程计算机网页源码eclipse项目
    HOOPS平台助力Xometry数字化转型:即时报价产品实现三维模型轻量化、Web端可视化!
    程序员是一个需要天赋的职业吗?
    python的数组:列表、数组模块array与第三方库numpy的数组
    攻防世界WEB练习区(backup、cookie、disabled_button)
    jdbc 执行批处理任务
    Python用一行代码,截取图片
  • 原文地址:https://blog.csdn.net/weixin_44063572/article/details/127770205