认识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>
int addStudent(Student student);
而现在,我们可以直接使用注解来实现,每个操作都有一个对应的注解:
@Insert("insert into student(name, sex) values(#{name}, #{sex})")
int addStudent(Student student);
当然,我们还需要修改一下配置文件中(mybatis-config.xml)的映射器注册:
<mappers>
<mapper class="com.test.mapper.MyMapper"/>//属性就不再是resources了。而是接口的全类名
<!-- 也可以直接注册整个包下的 <package name="com.test.mapper"/> -->
</mappers>
通过直接指定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();
直接通过@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>
现在我们就可以通过注解来自定义映射规则了。那么如何使用注解来完成复杂查询呢?我们还是使用一个老师多个学生的例子:
@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);
我们发现,多出了一个子查询,而这个子查询是单独查询该老师所属学生的信息,而子查询结果作为@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>
同理,@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);
如果现在我希望直接使用注解编写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>
@ResultMap("test")
@Select("select * from student")
List<Student> getAllStudent();
提供了@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;
}
因为这里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);
得到的结果和使用constructor
标签效果一致。
我们发现,当参数列表中出现两个以上的参数时,会出现错误:
@Select("select * from student where sid = #{sid} and sex = #{sex}")
Student getStudentBySidAndSex(int sid, String sex);
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)
原因是Mybatis不明确到底哪个参数是什么,因此我们可以添加@Param
来指定参数名称:
@Select("select * from student where sid = #{sid} and sex = #{sex}")
Student getStudentBySidAndSex(@Param("sid") int sid, @Param("sex") String sex);
所以,就算形参的名字个#{}里的一样也会报错。
探究:要是我两个参数一个是基本类型一个是对象类型呢?
System.out.println(testMapper.addStudent(100, new Student().setName("小陆").setSex("男")));
@Insert("insert into student(sid, name, sex) values(#{sid}, #{name}, #{sex})")
int addStudent(@Param("sid") int sid, @Param("student") Student student);
那么这个时候,就出现问题了,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)
那么我们就通过参数名称.属性的方式去让Mybatis知道我们要用的是哪个属性:
@Insert("insert into student(sid, name, sex) values(#{sid}, #{student.name}, #{student.sex})")
int addStudent(@Param("sid") int sid, @Param("student") Student student);
那么如何通过注解控制缓存机制呢?
@CacheNamespace(readWrite = false)
public interface MyMapper {
@Select("select * from student")
@Options(useCache = false)
List<Student> getAllStudent();
使用@CacheNamespace
注解直接定义在接口上即可,然后我们可以通过使用@Options
来控制单个操作的二级缓存启用。
个人感觉:能使用配置文件还是使用配置文件。方便且容易修改。
探究Mybatis的动态代理机制
在探究动态代理机制之前,我们要先聊聊什么是代理:其实顾名思义,就好比我开了个大棚,里面栽种的西瓜,那么西瓜成熟了是不是得去卖掉赚钱,而我们的西瓜非常多,一个人肯定卖不过来,肯定就要去多找几个开水果摊的帮我们卖,这就是一种代理。实际上是由水果摊老板在帮我们卖瓜,我们只告诉老板卖多少钱,而至于怎么卖的是由水果摊老板决定的。
那么现在我们来尝试实现一下这样的类结构,首先定义一个接口用于规范行为:
public interface Shopper {
//卖瓜行为
void saleWatermelon(String customer);
}
然后需要实现一下卖瓜行为,也就是我们要告诉老板卖多少钱,这里就直接写成成功出售:
public class ShopperImpl implements Shopper{
//卖瓜行为的实现
@Override
public void saleWatermelon(String customer) {
System.out.println("成功出售西瓜给 ===> "+customer);
}
}
最后老板代理后肯定要用自己的方式去出售这些西瓜,成交之后再按照我们告诉老板的价格进行出售:
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); //讨价还价成功,进行我们告诉代理商的卖瓜行为,被代理的方法被代理的方法进行了增强
}
}
现在我们来试试看:
public class Main {
public static void main(String[] args) {
Shopper shopper = new ShopperProxy(new ShopperImpl());
shopper.saleWatermelon("小强");//其实就是对方法进行了增强。代理者的方法对被代理的方法进行增强
}
}
这样的操作称为静态代理,也就是说我们需要提前知道接口的定义并进行实现才可以完成代理,而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);
}
}
通过实现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());
}
通过打印类型我们发现,就是我们之前看到的那种奇怪的类: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;
}
}
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());
}
就我目前的水平,jdk底层的动态代理都似懂非懂,更不要说去看Mybatis的源码了。就目前我能说的就是:框架使用了JDK的自带的动态代理,代理的是mapper接口,还记得我们获取mapper代理类是传入了mapper.class接口具体的原理以后背八股文吧。【先完成再完美】
try(SqlSession sqlSession2 = MybatisUtil.getSession(true)){
TestMapper testMapper2 = sqlSession2.getMapper(TestMapper.class);
student2 = testMapper2.getStudentBySid(1);
}
Mybatis的学习差不多就到这里为止了,不过,同样类型的框架还有很多,Mybatis属于半自动框架,SQL语句依然需要我们自己编写,虽然存在一定的麻烦,但是会更加灵活,而后面我们还会学习JPA,它是全自动的框架,你几乎见不到SQL的影子!