mybatis常见面试题链接:2023年-Mybatis常见面试题_是Smoky呢的博客-CSDN博客
MVC架构模式和三层架构
在说Mybatis之前,需要知道MVC架构模式和三层架构的这种思想
MVC架构模式
M:Model,数据层。都是和数据相关,比如实体类、从数据库中返回的数据等。
V:View,视图层。在页面中,动态的展示给用户。比如JSP。
C:Controller,控制层。可以理解为它是一个指挥人,从前端接收到参数后,分别使唤M和V完成业务功能。
三层架构
三层:表现层,业务层,数据持久层
表现层:用户接收用户请求及其参数,将页面展示给用户,调用业务层。
业务层:将标签层传递的参数进行处理,根据不同的业务需求,将不同的结果返回给表现层,调用数据持久层
数据持久层:访问数据库,将结果返回给业务层。
之间的关系
和数据有关,那么业务层和数据访问层都属于"M"
和页面有关,那么表现层属于"C"和"V"
最终的关系:
一、Mybatis概述
1. 为什么要使用Mybatis?
之前JDBC的代码:
// ...... // sql语句写死在java程序中 String sql = "insert into t_user(id,idCard,username,password,birth,gender,email,city,street,zipcode,phone,grade) values(?,?,?,?,?,?,?,?,?,?,?,?)"; PreparedStatement ps = conn.prepareStatement(sql); // 繁琐的赋值:思考⼀下,这种有规律的代码能不能通过反射机制来做⾃动化。 ps.setString(1, "1"); ps.setString(2, "123456789"); ps.setString(3, "zhangsan"); ps.setString(4, "123456"); ps.setString(5, "1980-10-11"); ps.setString(6, "男"); ps.setString(7, "[email protected]"); ps.setString(8, "北京"); ps.setString(9, "⼤兴区凉⽔河⼆街"); ps.setString(10, "1000000"); ps.setString(11, "16398574152"); ps.setString(12, "A"); // 执⾏SQL int count = ps.executeUpdate(); // ...... // ...... // sql语句写死在java程序中 String sql = "select id,idCard,username,password,birth,gender,email,city,s treet,zipcode,phone,grade from t_user"; PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery(); List
userList = new ArrayList<>(); // 思考以下循环中的所有代码是否可以使⽤反射进⾏⾃动化封装。 while(rs.next()){ // 获取数据 String id = rs.getString("id"); String idCard = rs.getString("idCard"); String username = rs.getString("username"); String password = rs.getString("password"); String birth = rs.getString("birth"); String gender = rs.getString("gender"); String email = rs.getString("email"); String city = rs.getString("city"); String street = rs.getString("street"); String zipcode = rs.getString("zipcode"); String phone = rs.getString("phone"); String grade = rs.getString("grade"); // 创建对象 User user = new User(); // 给对象属性赋值 user.setId(id); user.setIdCard(idCard); user.setUsername(username); user.setPassword(password); user.setBirth(birth); user.setGender(gender); user.setEmail(email); user.setCity(city); user.setStreet(street); user.setZipcode(zipcode); user.setPhone(phone); user.setGrade(grade); // 添加到集合 userList.add(user); } // ...... JDBC的不足:
- SQL语句直接写死在Java程序中,相同的语句不能够重复利用。不灵活:当数据库中的表设计进行改变时,那我们就需要该SQL,这样就相当于在修改Java程序,违背了OCP原则(对扩展开放,对修改关闭)
- 给?传值是频繁的,一个?就是一个setxxx语句
- 将结果集封装成Java对象是频繁的,一个字段就是一个getxxx语句
这些Mybatis都能实现,所以需要使用,而且Mybatis是一个框架,属于数据持久层。只需要在框架的基础上做二次开发即可。
2. 什么是Mybatis
- 是一款非常优秀的持久层框架
- 支持自定义SQL(手动编写SQL语句),支持存储过程与高级映射(一对一,一对多,多对多)
- 免除了几乎所有的JDBC代码以及设置参数和封装结果集的工作
- 可以通过简单XML文件或者注解的方式来编写SQL语句
- 是一个ORM框架
2.1 什么是ORM
O:Object,对象。代表的是JVM中所加载的JAVA对象
R:Relational,关系型数据库。指的是Mysql这些数据库
M:Mapping,映射。可以将JVM中的JAVA对象映射成数据库表中的一条记录;或者将数据库中的一条记录映射成JVM中的JAVA对象
注意:数据库中一条记录对应的是JAVA中的一个对象。图上是错误的
二、Mybatis入门程序
建议根据官方手册
中文版:https://mybatis.net.cn/
1. 表设计
汽车表:t_car,字段有以下
- id:主键(自增)【bigint】,自然主键 和业务无关
- car_num:汽车编号【varchar】
- brand:品牌【varchar】
- guide_price:价钱【decimal】
- producer_time:生产日期【char,只需要年月日】
- car_type:汽车类型(燃油车,电动车,新能源)【varchar】
建表:
添加两条数据:
2. 在POM.XML中添加Mybatis依赖和Mysql驱动
org.mybatis mybatis 3.5.10 mysql mysql-connector-java 5.1.8 3. 在resources根目录创建mybatis-conf.xml配置文件
可以参考mybatis手册
注意:
- 这是Mybatis的核心配置文件,文件名不一定叫做mybatis-conf.xml,自定义 随便。
- 这个核心配置文件可以放在任意位置(后面说为什么),这里放在resources根下,相当于放在了类的根路径下(类加载器从这里开始加载)
4. 在resources根目录下创建CarMapper.xml配置文件
可以参考mybatis手册
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,'1003','丰田',11.10,'2021-05-11','燃油车') 注意:
- 标签中的id值必须唯一
- inser ...SQL语句结尾";"可写可不写
- 文件名不一定叫做CarMapper.xml,自定义 随便
- 存放的位置随便(后面说为什么),这里放在resources根下,相当于放在了类的根路径下(类加载器从这里开始加载)
5. 将CarMapper.xml在mybatis-conf.xml中注册
如果这步不做,那么一切都没有意义
如果将这个配置文件放在了resources中的某一个文件夹下,那就是xxx/CarMapper.xml(xxx代表文件名)
6. 编写测试类
在编写前要知道Mybatis是通过什么执行SQL语句的?
我们前面有了一个叫做mybatis-conf.xml的配置文件,读取到这个文件是为了创建SqlSessionFactory
那么怎么读,怎么创建?
- 读:通过Resources这个工具类进行读取(Mybatis提供)
- 创建:通过SqlSessionFactoryBuilder来创建(这个对象直接new即可)
这个SqlSessionFactory是什么,有什么用?
- 是什么:一个创建SqlSession工厂类
- 有什么用:专门创建SqlSession
Mybatis执行SQL语句就是通过这个SqlSession对象
我们都知道session是一次会话,那么SqlSession是个什么东西?
- 表中数据和JAVA对象的一次会话
如何精准的找到需要执行的SQL语句?
- 标签中的id值
���那么按着这流程去编写测试类:
/** * @author 酒萧 * @version 1.0 * @since 1.0 */ public class MybatisTest { public static void main(String[] args) throws IOException { // SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession --> Sql // 创建SqlSessionFactoryBuilder对象 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); // 获取SqlSessionFactory InputStream is = Resources.getResourceAsStream("mybatis-conf.xml"); // mybatis中提供的类:Resources SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); // 参数为一个输入流,也就是读取mybatis-conf.xml // 获取SqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // // 执行sql int count = sqlSession.insert("insertCar");// 参数就是CarMapper.xml中的id System.out.println("成功增加"+count+"条数据"); } }
成功增加1条数据
无论怎么刷新都是原本的两条,这是为什么?
- Mybatis默认亲自接管事务,会开启事务,所以最后需要我们进行手动提交,也就调用commit()
6.1 改造
/** * @author 酒萧 * @version 1.0 * @since 1.0 */ public class MybatisTest { public static void main(String[] args) throws IOException { // SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession --> Sql // 创建SqlSessionFactoryBuilder对象 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); // 获取SqlSessionFactory InputStream is = Resources.getResourceAsStream("mybatis-conf.xml"); // mybatis中提供的类:Resources SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); // 参数为一个输入流,也就是读取mybatis-conf.xml // 获取SqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // // 执行sql int count = sqlSession.insert("insertCar");// 参数就是CarMapper.xml中的id System.out.println("成功增加"+count+"条数据"); // 提交事务 sqlSession.commit(); // 最后记得释放资源 sqlSession.close(); } }
成功增加1条数据
刷新表后,记录改变:
6.2 解决之前问题
哪些问题:mybatis-conf.xml和CarMapper.xml配置文件位置为什么可以随便放?文件名为什么随便起?
6.2.1 mybatis-conf.xml文件
我们在使用resources读取mybatis-con.xml文件后会返回一个输入流。在构建SqlSessionFactory时通过这个输入流对象获取。那么也就说明只要给一个输入流对象就能够返回SqlSessionFactory对象。
测试:使用FileInputStream来去读取这个文件,修改mybatis-cong.xml配置文件所在文件和文件名
- 文件所在位置和文件名:
- 修改程序:
// 获取SqlSessionFactory InputStream is = new FileInputStream(new File("E:\\var\\abc.xml")); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); // 参数为一个输入流,也就是读取mybatis-conf.xml
成功增加1条数据
刷新表后即可看到数据添加成功:
6.2.2 CarMapper.xml文件
我们在mybatis-cong.xml中注册CarMapper.xml的时候是通过一个resource属性去指向我们的CarMapper.xml文件,除了resource属性,还有一个url属性,指向的是某个文件的绝对路径
测试:使用url属性读取CarMapper.xml文件,修改CarMapper.xml配置文件名和存放路径
- 修改属性:
注意:使用url指向绝对路径的时候前面需要加上file:///然后再是文件的绝对路径
- 文件所在位置和文件名:
- 测试程序:
还是最初的
/** * @author 酒萧 * @version 1.0 * @since 1.0 */ public class MybatisTest { public static void main(String[] args) throws IOException { // SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession --> Sql // 创建SqlSessionFactoryBuilder对象 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); // 获取SqlSessionFactory InputStream is = Resources.getResourceAsStream("mybatis-conf.xml"); // mybatis中提供的类:Resources SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); // 参数为一个输入流,也就是读取mybatis-conf.xml // 获取SqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // 执行sql int count = sqlSession.insert("insertCar");// 参数就是CarMapper.xml中的id System.out.println("成功增加"+count+"条数据"); sqlSession.commit(); // 最后记得释放资源 sqlSession.close(); } }
成功增加1条数据
刷新表后即可看到数据添加成功:
三、源码跟踪【如何读取mybatis-con.xml和事务管理器】
1. 如何读取mybatis-conf.xml核心配置文件
1.2 进入这个方法后,里面只调用了另外一个重载方法,直接看重载方法
参数讲解:
- ClassLoader:指的是类加载器,因为我们没有传入,所以为null
- resource:传入的mybatis-conf.xml核心配置文件
1.3 这个classLoaderWrapper调用了它直接的getResourceAsStream(),并将参数传入,先看classLoaderWrapper这个对象(点击去后里面调用构造方法,没有看意义,直接看构造方法)
代码讲解:
- ClassLoader.getSystemClassLoader()这行代码获取的是系统类加载器
- 将系统类加载器赋值给systemClassLoader
- 那么上面classLoaderWrapper的值就是系统类加载器
- 故然我们在程序中也可以直接通过这个类加载器去读取我们的配置文件(等会演示)
1.4 classLoaderWrapper.getResourceAsStream()中首先调用getClassLoaders()
代码讲解:
- 创建了一个ClassLoader对象,并初始化五个类加载器
目的:为了在不同的环境下,使用不同的类加载器加载JAVA对象
1.5 其次调用另外一个重载方法
代码讲解:
- 首先遍历那五个类加载器
- 然后调用getResourceAsStream(),这是JDK中的方法,没必要继续往下看了。
- 在调用这个方法时将我们的mybatis-cong.xml文件传入进去
- 如果能够读取到这个文件,那么这个InputStream对象不为null,并将这个对象返回
- 如果读取不到,那么这个程序返回null值
- 先在mybatis-cong.xml文件前面加上"/"后继续查找,还找不到就返回null值
1.6 classLoaderWrapper.getResourceAsStream()方法结束
代码讲解:
- 判断的对象就是上面的InputStream
- 如果该对象不为null,那么直接返回该对象
- 如果该对象为null,抛出异常
演示1.3中的步骤:
// 获取SqlSessionFactory InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-conf.xml"); // 使用系统类加器去读取配置文件 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); // 参数为一个输入流,也就是读取mybatis-conf.xml
成功增加1条数据
刷新表后即可看到数据添加成功:
2. 事务管理器
我们在上面能够看到如果我们不手动提交事物,那么最终数据将不会影响到表中,这是为什么?
在mybatis-conf.xml文件中有一个
标签,里面有一个type属性,对应的值有两个,一个是JDBC,另一个是MANAGED,官网中也有说明: 2.1 JDBC
当type属性设置为JDBC的时候,Mybatis会亲自接管事务,所以在程序中开启事务和提交事务都需要手动完成。
为什么需要手动提交事务?
在我们获取SqlSession时,底层实际上执行的是conn.setAutoCommit(false)
SqlSession sqlSession = sqlSessionFactory.openSession(); // 底层相当于conn.setAutoCommit(false);
2.1.1 点进这个openSession(),里面调用openSessionFromDataSource(),并传入一个false值(关键所在)
2.1.2 进入openSessionFromDataSource()
代码讲解:
- 首先创建一个初始值为null的Transcation,这个就是事务管理器
- 怎么赋值?
- 通过TranscationFactory.newTranscation()获取
2.1.3 这个方法肯定会返回一个Transcation对象
2.1.4 这个构造方法里面完成了一写初始化操作:只需要关注autoCommit
2.1.5 进入该方法:
代码讲解
- 通过传入的值就行判断,如果判断结果为true就开启事务
- 判断结果为false则没有事务
因为底层执行了conn.setAutoCommit(false),所以我们必须在程序中提交事务
如果我们在获取SqlSession的时候传入true会怎样?
判断:true != true执行结果为false,不会进判断,也就代表并没有开启事物,所以在程序中就算不提交也能增加成功(不演示,这句话不会错!)
2.2 MANAGED
如果type属性设置为MANAGED,那么Mybatis将不会接管事务,由其他容器进行管理
官方原型:
如果我们这时没有其他的容器进行管理,那么就证明该事务没有开启,也就代表没有事务
在mybatis-conf.xml中修改type属性:
测试:
// 获取SqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); // 底层相当于conn.setAutoCommit(false); // 执行sql int count = sqlSession.insert("insertCar");// 参数就是xxxMapper.xml中的id System.out.println("成功增加"+count+"条数据"); // sqlSession.commit(); // 底层相当于conn.commit(); 这里不需要提交 因为没有事务
成功增加1条数据
刷新表后即可看到数据添加成功:
四、简化代码
一些一直重复的代码没必要一直写,只需要关心核心业务即可
1. 重复的代码
// 获取SqlSession SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build("mybatis-conf.xml"); SqlSession sqlSession = factory.openSession(); // 释放资源 sqlSession.close();
2. 封装
2.1 第一种方式:抽取工具类
工具类:
/** * SqlSession工具类 * 用来创建SqlSession */ public class SqlSessionUtil { // 将构造方法私有化,防止别人new对象 private SqlSessionUtil(){} /* 在这个工具类第一次加载的时候,就去读取配置文件,创建SqlSessionFactory 一个SqlSessionFactory对应一个environment */ static SqlSessionFactory sqlSessionFactory = null; static { try { sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-conf.xml")); } catch (IOException e) { throw new RuntimeException(e); } } /** * 获取SqlSession * @return SqlSession */ public static SqlSession openSession(){ return sqlSessionFactory.openSession(); } }
测试:
@Test public void testUtil() { // 通过工具类获取SqlSession SqlSession sqlSession = SqlSessionUtil.openSession(); int count = sqlSession.insert("insertCar"); if(count>0){ System.out.println("增加成功"); }else{ System.out.println("增加失败"); } sqlSession.commit(); // 释放资源 sqlSession.close(); }
增加成功
刷新表后,表中记录改变:
2.2 第二种方式:借助junit中的注解
POM依赖:
junit junit 4.13.2 test 使用哪些注解:
- @Before:在每个@Test所注解的方法执行之前执行
- @Aftre:在每个@Test所注解的方法执行之后执行
抽取:
public class MybatisTest { SqlSession sqlSession; @Before public void before(){ // 在每个@Test执行前创建SqlSession try { SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-conf.xml")); sqlSession = sqlSessionFactory.openSession(); } catch (IOException e) { throw new RuntimeException(e); } } @After public void after(){ // 在每个@Test执行后释放资源 不需要提交事物,因为有查询. sqlSession.close(); } }
测试:
@Test public void testAnnotation(){ int count = sqlSession.insert("insertCar"); if(count>0){ System.out.println("增加成功"); }else{ System.out.println("增加失败"); } sqlSession.commit(); }
增加成功
表中记录修改:
根据个人爱好,自行挑选���
3. 日志
日志这里比较简单,就随便讲一下。
官方原型:
该属性应用在"mybatis-conf.xml"核心配置文件settings标签中
对应的日志组件:
- LOG4J
- SLF4J
- LOG4J2
- STDOUT_LOGGING
- ...
其中STDOUT_LOGGING是Mybatis内置的一个日志组件,直接用就行
如果想用其他日志,需要导入对应的依赖和配置文件,然后将value修改即可(不演示)
使用STDOUT_LOGGING日志组件:
注意:
- 每个标签都有先后顺序,不需要去死记,如果标签位置放错会提示错误信息,然后再进行修改即可。
正确顺序:
如果放错:根据错误信息进行修改
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter. PooledDataSource forcefully closed/removed all connections. PooledDataSource forcefully closed/removed all connections. PooledDataSource forcefully closed/removed all connections. PooledDataSource forcefully closed/removed all connections. Opening JDBC Connection Created connection 1845904670. ==> Preparing: insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,'1003','丰田',11.10,'2021-05-11','燃油车') ==> Parameters: <== Updates: 1 增加成功 Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@6e06451e] Returned connection 1845904670 to pool.
五、使用Mybatis进行数据库的CRUD
在进行编写的时候注意一个问题:之前在CarMapper.xml文件中,是如何将值写入到数据库的?
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,'1003','丰田',11.10,'2021-05-11','燃油车') 不足点:
- 数值写死,以后使用这个语句,都是增加这样的数据
回顾一下之前JDBC中的占位符:
- JDBC中占位符为?,既然JDBC中都有站位符,那么Mybatis中肯定也有。
- Mybatis中的占位符为#{},使用这个占位符底层实际上使用的是PreparedStatement对象
改造:
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{},#{},#{},#{},#{}) 在JDBC中写完?后,会在后面的代码中进行set操作
那在Mybatis中#{}里面需要写入什么?
1. 增加
1.1 使用Map的方式进行传值
准备Map集合:
// 使用Map进行传参 Map
map = new HashMap<>(); map.put("k1","1005"); map.put("k2","法拉利"); map.put("k3",100.0); map.put("k4","2000-05-23"); map.put("k5","电动车"); 整个JAVA程序:
@Test public void testInsert() { SqlSession sqlSession = SqlSessionUtil.openSession(); // 使用Map进行传参 Map
map = new HashMap<>(); map.put("k1","1005"); map.put("k2","法拉利"); map.put("k3",100.0); map.put("k4","2000-05-23"); map.put("k5","电动车"); /* 增加:insert 参数: 1.配置文件中的id值 2.需要传入的参数 */ int count = sqlSession.insert("insertCar", map); // 将map封装好的数据传入 if (count>0) { System.out.println("增加成功"); }else{ System.out.println("增加失败"); } sqlSession.commit(); sqlSession.close(); } 这时在CarMapper.xml中如何接收这些参数?
- 在#{}里面写入Map集合的key,不能随便写
SQL语句:
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{k1},#{k2},#{k3},#{k4},#{k5}) ==> Preparing: insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,?,?,?,?,?)
==> Parameters: 1005(String), 法拉利(String), 100.0(Double), 2000-05-23(String), 电动车(String)
<== Updates: 1
增加成功查看日志可以发现values()中使用的?占位符,足以证明#{}底层使用的就是PreparedStatement对象
表中记录改变:
如果在#{}中,写入的不是Map集合的key会怎样?
SQL语句:
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{abc},#{k2},#{k3},#{k4},#{k5}) 运行程序:
可以看到将一个null添加到了表中:
虽然程序可以正常运行,但是#{abc}的写法导致无法获取到Map集合中的数据,最终导致将null插入表中
虽然使用k1,k2,k3...可以插入,但是可读性太差,为了增强可读性。最好见名知意:
Map
map = new HashMap<>(); map.put("carNum","1005"); map.put("brand","法拉利"); map.put("guidePrice",100.0); map.put("produceTime","2000-05-23"); map.put("carType","电动车");
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType}) 运行程序,表中记录改变:
1.2 使用POJO的方式进行传值
POJO:普通JAVA类
定义一个类名为Car的POJO:
/** * Car * @author 酒萧 * @version 1.0 * @since 1.0 */ public class Car { private Long id; // 使用包装类的原因:从数据库查询的数据可能为null,如果是long = null会报错 private String carNum; private String brand; private Double guidePrice; private String produceTime; private String carType; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getCarNum() { return carNum; } public void setCarNum(String carNum) { this.carNum = carNum; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public Double getGuidePrice() { return guidePrice; } public void setGuidePrice(Double guidePrice) { this.guidePrice = guidePrice; } public String getProduceTime() { return produceTime; } public void setProduceTime(String produceTime) { this.produceTime = produceTime; } public String getCarType() { return carType; } public void setCarType(String carType) { this.carType = carType; } @Override public String toString() { return "Car{" + "id=" + id + ", carNum='" + carNum + '\'' + ", brand='" + brand + '\'' + ", guidePrice=" + guidePrice + ", produceTime='" + produceTime + '\'' + ", carType='" + carType + '\'' + '}'; } }
编写测试方法:
@Test public void testInsetPOJO() { // 获取sqlSession SqlSession sqlSession = SqlSessionUtil.openSession(); // 封装Car Car car = new Car(); car.setCarNum("1006"); car.setBrand("丰田"); car.setGuidePrice(15.0); car.setProduceTime("2022-02-22"); car.setCarType("燃油车"); // 传入car,进行增加 int count = sqlSession.insert("insertCarOfPOJO", car); if (count>0) { System.out.println("增加成功"); }else{ System.out.println("增加失败"); } sqlSession.commit(); sqlSession.close(); }
SQL语句:
- #{}里面先填写POJO的属性
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType}) ==> Preparing: insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,?,?,?,?,?)
==> Parameters: 1006(String), 丰田(String), 15.0(Double), 2022-02-22(String), 燃油车(String)
<== Updates: 1
增加成功程序运行成功,查看数据库:
在CarMapper.xml文件中#{}里面写的是属性,如果写成其他的会怎样?
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{abc},#{brand},#{guidePrice},#{produceTime},#{carType}) 运行程序,出现异常:
错误信息:在Car类中没有找到abc属性的getter方法
修改Car类代码:将getCarNum修改成getAbc
运行程序,成功执行。直接看数据库:
1.3说明:
为什么够能精准的赋值:ORM
- 如果我们选择Map集合的方式进行传参,那么#{}里面需要写map集合的key,如果写入的没和key对应,并不会报错,而是返回null值,并插入数据库中
- 如果我们选择POJO的方式进行传参,那么#{}里面需要写getXxx()-->Xxx-->xxx,也就是getter方法去掉get然后将首字母小写剩余的部分,例如:getName()-->Name-->name,所以#{}应该传入name
- 注意:传入并不是属性,只不过是写入属性而已,最终还是会掉该属性的getter方法。上面演示了
简述一下传入POJO的取值流程 :这里拿#{name}举例(需要有点反射知识)
首先获取对象的字节码-->拿到name-->Name-->getName()-->根据getName()去获取Method对象-->(如果这里获取不到Method对象,后面的不会执行了,直接抛出异常,也就是在xxx类中找不到这个属性的getter方法)使用invoke()调用该方法-->拿到值插入到SQL语句中-->添加到数据库中
2. 删除
需求:删除id为33的记录
删除比较简单,直接写了
SQL语句:
delete from t_car where id = #{id} 注意:#{}里面不一定写id,当占位符只有一个的时候,#{}里面随便写,建议见名知意。
Java程序:
@Test public void testDelete() { SqlSession sqlSession = SqlSessionUtil.openSession(); // 传入id删除记录 int count = sqlSession.delete("deleteCarById", 33); System.out.println("删除"+count+"条记录"); sqlSession.commit(); sqlSession.close(); }
==> Preparing: delete from t_car where id = ?
==> Parameters: 33(Integer)
<== Updates: 1
删除1条记录表中id为33的记录消失:
3. 更新
需求:将id=34中的carNum修改为6666,brand修改为五菱宏光,guide_price修改为50.0,produce_time修改为2000-08-22,car_type修改为新能源
SQL语句:
update t_car set car_num = #{carNum}, brand = #{brand}, guide_price = #{guidePrice}, produce_time = #{produceTime}, car_type = #{carType} where id = #{id} 可以看到参数和Car实体类中的属性一致,使用POJO
使用Map集合也可以
不再多讲,和增加差不多
Java程序:
@Test public void testUpdate() { SqlSession sqlSession = SqlSessionUtil.openSession(); // 封装数据 Car car = new Car(); car.setId(34L); // 在增加时id没有给是因为id会自增,对于更新是根据id,所以id必须要传 car.setCarNum("6666"); car.setBrand("五菱宏光"); car.setProduceTime("2000-08-22"); car.setGuidePrice(50.0); car.setCarType("新能源"); // 传入car执行update语句 int count = sqlSession.delete("updateCar", car); System.out.println("更新了几条记录-->"+count); sqlSession.commit(); sqlSession.close(); }
==> Preparing: update t_car set car_num = ?, brand = ?, guide_price = ?, produce_time = ?, car_type = ? where id = ?
==> Parameters: 6666(String), 五菱宏光(String), 50.0(Double), 2000-08-22(String), 新能源(String), 34(Long)
<== Updates: 1
更新了几条记录-->1程序执行成功,刷新数据库,数据改变:
4. 查询
4.1 单条查询
需求:查询id=1的记录
SQL语句:
Java程序:
@Test public void testSelectById() { SqlSession sqlSession = SqlSessionUtil.openSession(); Object car = sqlSession.selectOne("selectCarById", 1);// 查询一条,返回值也只是一个单单的Object System.out.println(car); sqlSession.close(); }
程序运行异常:
A query was run and no Result Maps were found for the Mapped Statement 'jiuxao.selectCarById'.
【翻译】:对于⼀个查询语句来说,没有找到查询的结果映射。
It's likely that neither a Result Type nor a Result Map was specified.
【翻译】:很可能既没有指定结果类型,也没有指定结果映射。主要意思:对于一个查询来说,需要指定"结果类型"或者"结果映射"。也就是说,如果查询的结果是一个Java对象的话,必须指定这个Java对象的类型。
之前JDBC处理结果集:在程序中手动创建对象然后将值传入到Java对象的属性中。
ResultSet rs = ps.executeQuery(); if(rs.next){ String id = rs.getString("id"); String username = rs.getString("username"); String password = rs.getString("password"); User user = new User(); // 给对象属性赋值 user.setId(id); user.setUsername(username); user.setPassword(password); }
在Mybatis中,对于
修改SQL语句:
select * from t_car where id = #{id} 运行结果:
==> Preparing: select * from t_car where id = ?
==> Parameters: 1(Integer)
<== Columns: id, car_num, brand, guide_price, produce_time, car_type
<== Row: 1, 1001, 宝马, 11.00, 2022-02-22, 燃油车
<== Total: 1
Car{id=1, carNum='null', brand='宝马', guidePrice=null, produceTime='null', carType='null'}可以看到控制台不在报错,证明对于查询语句resultType不能省略,但是在日志,可以看到这台记录的每个字段都查询到了值,但是Car对象中只有id和brand有值,为什么?
因为数据库中的字段和Car类中的属性有的对应不上:
只需要将字段和属性保持一致即可,在Java中修改属性肯定是不可以的,修改数据库的字段更不可以。在数据库中有一个as别名,将数据库中的字段变为类中的属性。利用这个就可以将表中的字段和类中的属性保持一致,从而将返回的结果集全部封装到Java对象中。
SQL语句;
select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = #{id} 执行Java程序,输入结果:
==> Preparing: select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = ?
==> Parameters: 1(Integer)
<== Columns: id, carNum, brand, guidePrice, produceTime, carType
<== Row: 1, 1001, 宝马, 11.00, 2022-02-22, 燃油车
<== Total: 1
Car{id=1, carNum='1001', brand='宝马', guidePrice=11.0, produceTime='2022-02-22', carType='燃油车'}���通过测试得知:如果表中字段和类中属性对应不上的话,使用as别名的方式
���后面还有其他方式,以后再说
4.2 查询全部
需求:查询出t_car表中的全部数据
SQL语句:
因为是返回所有,肯定使用List集合来接收,对于resultType里面写什么类型:泛型的类型,因为等会调用的方法返回的就是List,所以只需要指定里面的泛型即可。
select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car Java程序:
@Test public void testSelectAll() { SqlSession sqlSession = SqlSessionUtil.openSession(); List
carList = sqlSession.selectList("selectCarOfList"); // 调用selectList() 返回的是List集合 carList.forEach(car -> System.out.println(car)); // stream流 lambda写法 不懂先这样写的 sqlSession.close(); } ==> Preparing: select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car ==> Parameters: <== Columns: id, carNum, brand, guidePrice, produceTime, carType <== Row: 1, 1001, 宝马, 11.00, 2022-02-22, 燃油车 <== Row: 2, 1002, 奔驰, 55.00, 2021-05-24, 新能源 <== Row: 22, 1003, 丰田, 11.10, 2021-05-11, 燃油车 <== Row: 32, 1005, 法拉利, 100.00, 2000-05-23, 电动车 <== Row: 34, 6666, 五菱宏光, 50.00, 2000-08-22, 新能源 <== Row: 35, 1006, 丰田, 15.00, 2022-02-22, 燃油车 <== Row: 36, 1006, 丰田, 15.00, 2022-02-22, 燃油车 <== Total: 7 Car{id=1, carNum='1001', brand='宝马', guidePrice=11.0, produceTime='2022-02-22', carType='燃油车'} Car{id=2, carNum='1002', brand='奔驰', guidePrice=55.0, produceTime='2021-05-24', carType='新能源'} Car{id=22, carNum='1003', brand='丰田', guidePrice=11.1, produceTime='2021-05-11', carType='燃油车'} Car{id=32, carNum='1005', brand='法拉利', guidePrice=100.0, produceTime='2000-05-23', carType='电动车'} Car{id=34, carNum='6666', brand='五菱宏光', guidePrice=50.0, produceTime='2000-08-22', carType='新能源'} Car{id=35, carNum='1006', brand='丰田', guidePrice=15.0, produceTime='2022-02-22', carType='燃油车'} Car{id=36, carNum='1006', brand='丰田', guidePrice=15.0, produceTime='2022-02-22', carType='燃油车'} 查询所有东西没有太多,主要是返回的类型要注意一下,是泛型的类型。
5. Mapper.xml中的namespace
namespace翻译过来就是"命名空间"的意思,目的就是为了防止Mapper.xml中每个SQLid有重名的情况
如果重名会怎样:
新建一个CarMapper2.xml文件
select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car 在mybatis-conf.xml中注册CarMapper2.xml
可以看到现在有两个SQLid都为selectCarOfList
运行Java程序:
@Test public void testSelectAll() { SqlSession sqlSession = SqlSessionUtil.openSession(); List
carList = sqlSession.selectList("selectCarOfList"); // 调用selectList() 返回的是List集合 carList.forEach(car -> System.out.println(car)); // stream流 lambda写法 不懂先这样写的 sqlSession.close(); } 异常信息:
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.IllegalArgumentException: selectCarOfList is ambiguous in Mapped Statements collection (try using the full name including the namespace, or rename one of the entries)
【翻译】selectCarOfList在Mapped Statements集合中不明确(请尝试使用命名空间的全名,或重命名其中的一个条目)主要意思:selectOfList重名了,要么使用命名空间,要么修改其中的一个SQLid
使用命名空间后,运行Java程序:
【第一个CarMapper.xml的namespace为"jiuxiao",第二个为"abc"],这里使用第二个
语法为:namespace.SQLid
@Test public void testSelectAll() { SqlSession sqlSession = SqlSessionUtil.openSession(); List
carList = sqlSession.selectList("abc.selectCarOfList"); // 调用selectList() 返回的是List集合 carList.forEach(car -> System.out.println(car)); // stream流 lambda写法 不懂先这样写的 sqlSession.close(); } ==> Preparing: select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car
==> Parameters:
<== Columns: id, carNum, brand, guidePrice, produceTime, carType
<== Row: 1, 1001, 宝马, 11.00, 2022-02-22, 燃油车
<== Row: 2, 1002, 奔驰, 55.00, 2021-05-24, 新能源
<== Row: 22, 1003, 丰田, 11.10, 2021-05-11, 燃油车
<== Row: 32, 1005, 法拉利, 100.00, 2000-05-23, 电动车
<== Row: 34, 6666, 五菱宏光, 50.00, 2000-08-22, 新能源
<== Row: 35, 1006, 丰田, 15.00, 2022-02-22, 燃油车
<== Row: 36, 1006, 丰田, 15.00, 2022-02-22, 燃油车
<== Total: 7
Car{id=1, carNum='1001', brand='宝马', guidePrice=11.0, produceTime='2022-02-22', carType='燃油车'}
Car{id=2, carNum='1002', brand='奔驰', guidePrice=55.0, produceTime='2021-05-24', carType='新能源'}
Car{id=22, carNum='1003', brand='丰田', guidePrice=11.1, produceTime='2021-05-11', carType='燃油车'}
Car{id=32, carNum='1005', brand='法拉利', guidePrice=100.0, produceTime='2000-05-23', carType='电动车'}
Car{id=34, carNum='6666', brand='五菱宏光', guidePrice=50.0, produceTime='2000-08-22', carType='新能源'}
Car{id=35, carNum='1006', brand='丰田', guidePrice=15.0, produceTime='2022-02-22', carType='燃油车'}
Car{id=36, carNum='1006', brand='丰田', guidePrice=15.0, produceTime='2022-02-22', carType='燃油车'}
六、Mybatis核心配置文件详解:
最初样式:
configuration:根标签,表示配置文件
1. environments
表示多环境,以(s)结尾,也就就是说里面可以配置多个环境(environment)
一个environment代表一个sqlSessionFactory对象。
default属性表示默认使用哪个数据源,指向的是environment中的id值,必须给值。如果在创建sqlSessionFactory的时候指定要使用的数据源,不再使用default指向的数据源
测试:
配置两个环境,第一个环境数据库使用"jiuxiao",第二个环境使用"abc",default属性指向第一个环境的id
在写入一个环境:
如何测试:查询两个数据库中id=1的数据:
environment的id为development,数据库为jiuxiao
environment的ib为abc,数据库为abc
SQL语句:
select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = #{id} Java程序:
@Test public void testEnvironment() throws IOException { // 不需要工具类 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); // 如果不指定使用的环境,那么使用default中指向的环境,也就是id=development的环境,当中的数据库为jiuxiao SqlSessionFactory sqlSessionFactory1 = builder.build(Resources.getResourceAsReader("mybatis-conf.xml")); SqlSession sqlSession = sqlSessionFactory1.openSession(); // 执行sql语句 Car car1 = sqlSession.selectOne("selectCarById", 1); System.out.println(car1); sqlSession.close(); // 在build()中有一个重载方法可以指定环境 build("inputstream流","环境的id") SqlSessionFactory sqlSessionFactory2 = builder.build(Resources.getResourceAsReader("mybatis-conf.xml"),"abc"); // 指向id=abc的环境,当中的数据库为abc SqlSession sqlSession1 = sqlSessionFactory2.openSession(); Car car2 = sqlSession1.selectOne("selectCarById", 1); System.out.println(car2); sqlSession.close(); }
执行结果:
【environment的id为development,数据库为jiuxiao】
==> Preparing: select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = ?
==> Parameters: 1(Integer)
<== Columns: id, carNum, brand, guidePrice, produceTime, carType
<== Row: 1, 1001, 宝马, 11.00, 2022-02-22, 燃油车
<== Total: 1
【environment的id为abc,数据库为abc】
==> Preparing: select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = ?
==> Parameters: 1(Integer)
<== Columns: id, carNum, brand, guidePrice, produceTime, carType
<== Row: 1, 1222, 奔驰, 20.20, 2000-05-12, 新能源
<== Total: 1
Car{id=1, carNum='1222', brand='奔驰', guidePrice=20.2, produceTime='2000-05-12', carType='新能源'}2. transactionManager
事物管理器,前面有说。简单讲:
该标签中的type属性只有两个值:
- JDBC:使用JDBC原生的事务管理器,也就是在执行SQL语句之前开始事物conn.setAutoCommit(false),执行后需要提交事物conn.commit()
- MANAGED:将不会接管事物,由其他容器管理,比如Spring。如果没有容器管理,那就代表没有开启事务,也就代表没有事物。没有事物的含义:只要执行一条DML(增删改),就提交一次
3. dataSource
主要用来指定数据源,换句话说就是指定需要使用的数据库连接池。
指定数据源其实都是在获取Collection对象
该标签中的type属性只有三个值:
- UNPOOLED:不使用数据库连接池,但不代表不获取Collection对象。只不过每次获取的都是一个新的连接对象
- 其中的property中可以是:
- key:driver,value:JDBC驱动
- key:url,value:JDBC连接数据库的URL
- key:username,value:登录数据库的用户名
- key:password,value:登录数据库的密码
- key:defaultTransactionIsolationLevel,value:默认的连接事务隔离级别
- key:defaultNetworkTimeout,value:等待数据库操作完成的默认⽹络超时时间
- key:driver.encoding,value:UTF8
- POOLED:使用数据库连接池,每次使用的Collection对象都是从连接池中获取。mybatis中已经有对应的实现
- 其中的property中可以是:
- 除了包含UNPOOLED之外
- key:poolMaximumActiveConnections,value:连接池中最大活跃的连接数(也就是最大可以使用的连接数)
- key:poolMaximumIdleConnections,value:任意时间可能存在的连接数(如果设置为5,这时连接池中有6个空闲连接,就删除一个)
- 其他:参考Mybatis中文网
- JNDI:使用服务器提供的JNDI技术实现。也就是如果我们需要使用druid连接池,选用这个。
测试UNPOOLED和POOLED的区别:
UNPOOLED:
不使用数据库连接池会是什么样的?每次都需要建立连接,消耗资源,消耗时间
Java程序:
@Test public void testDataSource() throws IOException { SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); // 如果不指定使用的环境,那么使用default中指向的环境,也就是id=development的环境,当中的数据库为jiuxiao SqlSessionFactory sqlSessionFactory = builder.build(Resources.getResourceAsReader("mybatis-conf.xml")); // 执行sql语句 for (int i = 0; i < 2; i++) { SqlSession sqlSession = sqlSessionFactory.openSession(); Car car = sqlSession.selectOne("selectCarById", 1); System.out.println(car); sqlSession.close(); } }
运行程序:可以看到两个collection对象不是同一个,就证明第二次是新建了一个连接
POOLED
使用数据库连接池:
Java程序不变,运行结果:两个collection对象都是同一个,说明都是从连接池中获取
4. properties
Mybatis中提供了更加灵活的配置,比如将连接数据库的信息放在这个标签中:
如何使用:
在需要的地方使用${name}的方式引入
也可以使用外部引入的方式
在resources目录下新建jdbc.properties文件:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/jiuxiao
jdbc.username=root
jdbc.password=1234在
标签中使用resource属性进行引入:
Java程序测试:
@Test public void testProperties() { SqlSession sqlSession = SqlSessionUtil.openSession(); Car car = sqlSession.selectOne("selectCarById", 1); System.out.println(car); sqlSession.close(); }
运行结果:
==> Preparing: select id as id, car_num as carNum, brand as brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = ?
==> Parameters: 1(Integer)
<== Columns: id, carNum, brand, guidePrice, produceTime, carType
<== Row: 1, 1001, 宝马, 11.00, 2022-02-22, 燃油车
<== Total: 1
Car{id=1, carNum='1001', brand='宝马', guidePrice=11.0, produceTime='2022-02-22', carType='燃油车'}properties中的属性:
- resource:这个属性从类的根路径下开始查找【常用】
- url:绝对路径,如果这个文件放到了d:/jdbc.properties,那么url写成:file:///d:/jdbc.properties。file:///+文件路径,之前演示过,不演示了。
5. mappers
不讲了,之前说过的。
里面的属性:
- resource:从类的根路径下查找
- 如果有一层目录:目录/mapper.xml
- url:绝对路径,file:///+文件路径
底层原理:
1.
在进行增加或者更新的时候,传入pojo对象,在Mapper.xml文件中的#{}里写入属性就能向数据库增加记录,这是为什么?传入别的还可以吗?
首先我们要知道Mybatis底层执行的是JDBC代码,所有我们在Mapper.xml文件中写的#{},执行的时候变为?占位符,也就是原始JDBC的格式。
答:
为什么:
其次它底层是先获取到#{}中的属性,然后在前面拼接get,并将该属性的首字母大写,最后将剩余的部分拼接到后面。说到这里应该也能想到这就是在将一个属性一点点的拼接成该属性的get方法。所以Mybatis在执行SQL语句的时候,底层相当于在调用get方法来获取值。然后将值添加到数据库中
例如:name-->Name-->getName-->method.invoke()
传入别的可以吗?
可以,在#{}中我写入abc,但是在这个类中并没有,为什么还能。
只要在该类中将某一个get方法修改为getAbc()就可以
例如:将#{name}修改成#{abc},那么只需要将getName()修改为getAbc()即可
2.
在进行查询的时候,如何将返回的结果集,添加到pojo对象中。
答:
在Mybatis执行完SQL语句后,获取到返回的字段。然后和上面一样,只不是前面是拼接set,也就是调用某个属性的set方法,然后将值作为参数传入。
例如返回name字段:先使用原生JDBC的代码,通过字段获取到值,然后在进行添加值。
ps.getString("name")-->name-->Name-->setName()-->setName(值)
这也证明了数据库中的字段要和类中的属性一致。否则:
ps.setString("user_name")-->User_name-->setUser_name(值)
很显然,没有这种方法,所以才没有值
3.
在Mapper.xml里面都是一个个的SQL语句与一些标签。
那么这些东西也对应着一个JAVA对象,叫做:MappedStatement。
这个对象里面主要封装了:SQL语句,标签中的属性,比如resultType等。
如何精准的找到这个对象:使用sqlId,底层里面使用了一个类似于Map集合的东西。key存储的是sqlId,value存储的就是MappenStatement对象。
所以我们开发中,在使用sqlId去调用Mapper.xml中的方法时,底层实际上在操作这个MappedStatement对象
七、Javassist
Javassist是一个可以在程序运行阶段动态的在内存中生成类
Mybatis底层的代理机制中使用的就是Javassist
如何使用?
1. 导入jar
org.javassist javassist 3.29.1-GA 2. 使用
2.1 使用javassist创建一个类与调用方法
注意:它是在内存中创建的,程序执行完后,这个类就会消失。
@Test public void testGenerate() throws Exception { // 获取类池 ClassPool pool = ClassPool.getDefault(); // 制造类 CtClass ctClass = pool.makeClass("com.jiuxiao.dao.impl.AccountImpl"); // 制造方法 String method = "public void insert(){System.out.println(123);}"; CtMethod ctMethod = CtMethod.make(method, ctClass); // 添加到类中 ctClass.addMethod(ctMethod); // 生成字节码 Class> clazz = ctClass.toClass(); // 创建对象 Object obj = clazz.newInstance(); // 获取方法 Method insertMethod = clazz.getDeclaredMethod("insert"); // 调用 insertMethod.invoke(obj); }
运行程序:
123
2.2 让一个类去实现接口与实现方法
准备接口:
package com.jiuxiao.dao; public interface AccountDao { void delete(); } java程序: @Test public void testGenerateInterface() throws Exception { // 获取类池 ClassPool pool = ClassPool.getDefault(); // 创建类 CtClass ctClass = pool.makeClass("com.jiuxiao.dao.impl.AccountDaoImpl"); // 实现接口(需要实现的接口) CtClass ctInterface = pool.makeInterface("com.jiuxiao.dao.AccountDao"); // 添加接口 ctClass.addInterface(ctInterface); // 实现方法(实现方法也就是将接口中的方法写一遍 只不过不是抽象方法) String method = "public void delete(){System.out.println(\"Hello world!\");}"; CtMethod make = CtMethod.make(method, ctClass); // 添加方法 ctClass.addMethod(make); // 获取字节码文件 Class> clazz = ctClass.toClass(); AccountDao account = (AccountDao) clazz.newInstance(); account.delete(); }
运行程序:
Hello world!
3. 在应用中生成接口的实现类
在编写程序中,每个层和每个层之间需要使用接口来进行传递数据(解耦合),对于Dao层编写有必要去写实现类吗?
没必要,因为代码都是一行或者几行,没有太多的逻辑。而且还有重复的地方:
示例:
public class AccountDaoImpl implements AccountDao { @Override public Account selectByActno(String actNo) { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); Account account = (Account) sqlSession.selectOne("account.selectByActno", actNo); return account; } @Override public int update(Account account) { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); int count = sqlSession.update("account.update",account); return count; } }
那么我们可以使用javassist将这些Dao层的实现类,动态的在内存中创建,在程序中不再编写。
3.1 准备工具类和生成实现类的方法
package com.jiuxiao.utils; /** * 生成实现类(代理类) * @author 酒萧 * @since 1.0 * @version 1.0 */ public class GenerateProxy { private GenerateProxy(){} /** * 生成目标接口的实现类 * @param inter 接口 既然需要生成实现类,接口不能少 * @return 实现类(代理类) */ public static Object getMapper(Class inter){ } }
开始编写:
注意:要以一个开发者的角度编写,不能把自己当成使用者
3.2 先将类和接口创建出来。
public static Object getMapper(Class inter){ // 获取类池 ClassPool pool = ClassPool.getDefault(); // 创建实现类 通过传入的接口基础上进行拼接 CtClass ctClass = pool.makeClass(inter.getName() + ".impl" + inter.getSimpleName() + "Proxy"); // com.jiuxiao.dao.CarDao.impl.CarDaoProxy // 生成接口 通过传入的接口来动态的获取 CtClass ctInterface = pool.makeInterface(inter.getName()); // com.jiuxiao.dao.CarDao // 实现接口 ctClass.addInterface(ctInterface); // 实现方法 try { // 创建方法 CtMethod ctMethod = CtMethod.make("方法", ctClass); // 将方法添加到类中 ctClass.addMethod(ctMethod); } catch (Exception e) { throw new RuntimeException(e); } try { // 获取到类对象 Object obj = ctClass.toClass().newInstance(); // 返回 return obj; } catch (Exception e) { throw new RuntimeException(e); } }
就剩下实现方法部分
这部分也是最麻烦的,因为我们不知道接口中有多少的方法,每一个方法的方法名是什么,返回值等。之前都是写死的,现在不能写死,需要动态的获取
3.3 拼接形式参数
// 实现方法 // 1.拿到接口中所有的方法 Method[] methods = inter.getDeclaredMethods(); // 2.开始遍历 Arrays.stream(methods).forEach(method -> { StringBuilder sb = new StringBuilder(); // 必须放在里面 每次都是一个全新的 // void delete(); --> public void delete(){} // 3.拼接public(因为接口中的方法都是公共的 public直接写死即可) sb.append("public"); sb.append(" "); // 空格 // 4.拼接返回值 Class> returnType = method.getReturnType(); sb.append(returnType.getName()); sb.append(" "); // 5.拼接方法名 String name = method.getName(); sb.append(name); // 6.拼接参数 sb.append("("); Class>[] parameterTypes = method.getParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { Class> parameterType = parameterTypes[i]; sb.append(parameterType.getName()); sb.append("arg"+i); // 因为参数有多个,不能重复 arg0.... // 拼接"," if (i != parameterTypes.length - 1){ // (String agr0,String arg1) 0 != 1 , 1 != 1 sb.append(","); } } sb.append(")"); // 7.拼接方法体 });
拼接到这里整个方法的签名语句获取到了,那么方法体也就是一个大麻烦:因为我们不知道别人会在方法中进行什么操作:查询、增加、更新、删除。这些都不知道。
3.4 拼接方法体
先将一些一致的代码写死:因为不需要动态的获取,比如获取SqlSession
sb.append(")"); // 7.拼接方法体 sb.append("{"); // 注意写包名 让javassist找到这是哪个包下的 sb.append("org.apache.ibatis.session.SqlSession sqlSession = com.jiuxiao.utils.SqlSessionUtil.openSqlSession();"); sb.append("}");
下面就是一些crud语句,所以肯定少不了SqlSession对象,需要外界传入过来,形参接收。
public static Object getMapper(SqlSession sqlSession, Class inter){}
那么怎么知道别人在进行哪个语句
只要获取该语句在Mapper.xml中的标签名,就知道别人在干什么、
如何获取:
SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement("sqlId").getSqlCommandType();
那么这个sqlId如何获取?
我们无法获取到,因为sqlId是别人随便写的。所以:既然获取不到,那我们就限制范围
现在我们可以获取到接口的全限名 和 方法名,那么我们就可以将这两个拼接成字符串作为sqlId。
我们之前传入sqlId是通过namespace+标签id拼接,那么现在就要修改成:
namespace-->接口全限名 ,标签id-->方法名
示例:
- 准备接口:
package com.jiuxiao.dao; import com.jiuxiao.pojo.Car; public interface CarDao { Car selectById(Long id); int update(Car car); }
- CarMapper.xml:
select * from t_car where id = #{id} update t_car set car_num=#{carNum},brand=#{brand} where id = #{id} 获取到SqlId继续拼接:
注意:在调用select insert时sqlId由双引号引用起来的
// 7.拼接方法体 sb.append("{"); // 注意写包名 让javassist找到这是哪个包下的 sb.append("org.apache.ibatis.session.SqlSession.SqlSession sqlSession = com.jiuxiao.utils.SqlSessionUtil.SqlSessionUtil.openSqlSession();"); // 获取sqlId String sqlId = inter.getName() +"."+ name; // com.jiuxiao.dao.CarDao.selectById(); // 获取到Mapper.xml中的标签名 select insert ... SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement("sqlId").getSqlCommandType(); // 判断 if (sqlCommandType == SqlCommandType.SELECT){ // 查询语句最终返回的时候,有可能别人会进行强转,所以需要处理一下:提前强转成方法的返回值类型 sb.append("return ("+returnType.getName()+")sqlSession.selectOne(\""+sqlId+"\",arg0);"); // 这里的参数为上面拼接的参数 } if (sqlCommandType == SqlCommandType.INSERT){ // 增加 sb.append("return sqlSession.insert(\""+sqlId+"\",arg0);"); } if (sqlCommandType == SqlCommandType.UPDATE){ // 更新 sb.append("return sqlSession.update(\""+sqlId+"\",arg0);"); } if (sqlCommandType == SqlCommandType.DELETE){ // 删除 sb.append("return sqlSession.delete(\""+sqlId+"\",arg0);"); } sb.append("}");
最后将整个StringBuilder对象传入make方法中
注意:要在循环中。
sb.append("}"); try { // 创建方法 CtMethod ctMethod = CtMethod.make(sb.toString(), ctClass); // 将方法添加到类中 ctClass.addMethod(ctMethod); } catch (Exception e) { throw new RuntimeException(e); }
3.5 最终整个方法的效果
/** * 生成目标接口的实现类 * @param inter 接口 * @param sqlSession sql会话 * @return 实现类(代理类) */ public static Object getMapper(SqlSession sqlSession, Class inter){ // 获取类池 ClassPool pool = ClassPool.getDefault(); // 创建实现类 通过传入的接口基础上进行拼接 CtClass ctClass = pool.makeClass(inter.getName() + ".impl" + inter.getSimpleName() + "Proxy"); // com.jiuxiao.dao.CarDao.impl.CarDaoProxy // 生成接口 通过传入的接口来动态的获取 CtClass ctInterface = pool.makeInterface(inter.getName()); // com.jiuxiao.dao.CarDao // 实现接口 ctClass.addInterface(ctInterface); // 1.拿到接口中所有的方法 Method[] methods = inter.getDeclaredMethods(); // 2.开始遍历 Arrays.stream(methods).forEach(method -> { // 实现方法 StringBuilder sb = new StringBuilder(); // void delete(); --> public void delete(){} // 3.拼接public(因为接口中的方法都是公共的 public直接写死即可) sb.append("public"); sb.append(" "); // 空格 // 4.拼接返回值 Class> returnType = method.getReturnType(); sb.append(returnType.getName()); sb.append(" "); // 5.拼接方法名 String name = method.getName(); sb.append(name); // 6.拼接参数 sb.append("("); Class>[] parameterTypes = method.getParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { Class> parameterType = parameterTypes[i]; sb.append(parameterType.getName()); sb.append(" "); sb.append("arg"+i); // 因为参数有多个,不能重复 arg0.... // 拼接"," if (i != parameterTypes.length - 1){ // (String agr0,String arg1) 0 != 1 , 1 != 1 sb.append(","); } } sb.append(")"); // 7.拼接方法体 sb.append("{"); // 注意写包名 让javassist找到这是哪个包下的 sb.append("org.apache.ibatis.session.SqlSession sqlSession = com.jiuxiao.utils.SqlSessionUtil.openSqlSession();"); String sqlId = inter.getName() +"."+ name; SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType(); // 判断 if (sqlCommandType == SqlCommandType.SELECT){ // 查询 sb.append("return ("+returnType.getName()+")sqlSession.selectOne(\""+sqlId+"\",arg0);"); } if (sqlCommandType == SqlCommandType.INSERT){ // 增加 sb.append("return sqlSession.insert(\""+sqlId+"\",arg0);"); } if (sqlCommandType == SqlCommandType.UPDATE){ // 更新 sb.append("return sqlSession.update(\""+sqlId+"\",arg0);"); } if (sqlCommandType == SqlCommandType.DELETE){ // 删除 sb.append("return sqlSession.delete(\""+sqlId+"\",arg0);"); } sb.append("}"); try { // 创建方法 CtMethod ctMethod = CtMethod.make(sb.toString(), ctClass); // 将方法添加到类中 ctClass.addMethod(ctMethod); } catch (Exception e) { throw new RuntimeException(e); } }); try { // 获取到类对象 Object obj = ctClass.toClass().newInstance(); // 返回 return obj; } catch (Exception e) { throw new RuntimeException(e); } }
3.6 测试程序
拿上面的Car接口进行测试:
3.6.1 根据id查询:
java程序:
@Test public void testProxy() { CarDao mapper = (CarDao) GenerateProxy.getMapper(SqlSessionUtil.openSqlSession(), CarDao.class); mapper.selectById(1L); }
执行结果:
==> Preparing: select * from t_car where id = ?
==> Parameters: 1(Long)
<== Columns: id, car_num, brand, guide_price, produce_time, car_type
<== Row: 1, 1001, 宝马, 11.00, 2022-02-22, 燃油车
<== Total: 13.6.2 更新记录
将id是1的记录修改:car_num=5678,brand=丰田
Java程序:
@Test public void testProxy() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarDao mapper = (CarDao) GenerateProxy.getMapper(sqlSession, CarDao.class); Car car = new Car(); car.setId(1L); car.setCarNum("5678"); car.setBrand("丰田"); mapper.update(car); sqlSession.commit(); }
执行结果:
==> Preparing: update t_car set car_num=?,brand=? where id = ?
==> Parameters: 5678(String), 丰田(String), 1(Long)
<== Updates: 1查看数据库:
这种机制我们能想到,别人肯定也能想到。
Mybatis中提供了一个API,专门用来获取这种实现类对象。也可以说是代理对象
@Test public void testMapper() { // 提供了getMapper(),底层实现就是Javassist。别人已经封装好了 CarDao mapper = SqlSessionUtil.openSqlSession().getMapper(CarDao.class); mapper.selectById(1L); }
执行结果:
==> Preparing: select * from t_car where id = ?
==> Parameters: 1(Long)
<== Columns: id, car_num, brand, guide_price, produce_time, car_type
<== Row: 1, 5678, 丰田, 11.00, 2022-02-22, 燃油车
<== Total: 14. 小结
- 在我们使用getMapper调用接口方法时,底层使用Javassist代理机制,为我们在内存中生成一个代理实现类,帮我们去调用insert update delete select这些方法,转而执行Mapper.xml文件中所定义的SQL语句。
- 调用一个SQL语句需要通过SQLid去指向,这个sqlId通过namespace+标签的id值组成。也就是通过接口的全限名+方法名拼接成的字符串。这个字符串将会作为sqlId。
- 所以以后在Mapper.xml文件中的namespace的值是接口的全限定名,标签中的id值是接口中的方法名。这样在执行接口方法时,Mybatis底层为我们创建的实现类才能找个找个sql语句。
八、#{}和${}的区别
通过SQL语句来查看这两个的区别
1. 根据汽车类型来查询汽车信息
需求:查询汽车类型是新能源
查看数据库:
准备POJO
package com.jiuxiao.pojo; /** * Car * @author 酒萧 * @version 1.0 * @since 1.0 */ public class Car { private Long id; private String carNum; private String brand; private Double guidePrice; private String produceTime; private String carType; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getCarNum() { return carNum; } public void setCarNum(String carNum) { this.carNum = carNum; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public Double getGuidePrice() { return guidePrice; } public String getProduceTime() { return produceTime; } public void setProduceTime(String produceTime) { this.produceTime = produceTime; } public void setGuidePrice(Double guidePrice) { this.guidePrice = guidePrice; } public String getCarType() { return carType; } public void setCarType(String carType) { this.carType = carType; } @Override public String toString() { return "Car{" + "id=" + id + ", carNum='" + carNum + '\'' + ", brand='" + brand + '\'' + ", guidePrice=" + guidePrice + ", produceTime='" + produceTime + '\'' + ", carType='" + carType + '\'' + '}'; } }
准备接口:
/** * Car Mapper接口 */ public interface CarMapper { /** * 根据汽车类型查询汽车信息 * @param carType 汽车类型 * @return 汽车信息 */ List
selectByCarType(String carType); } CarMapper.xml文件:
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = #{carType} 注意:SQL语句中传值的地方使用的是#{}
测试程序:
@Test public void testSelectByCarType() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List
cars = mapper.selectByCarType("新能源"); cars.forEach(car-> System.out.println(car)); sqlSession.close(); } 查看执行日志:
==> Preparing: select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = ?
==> Parameters: 新能源(String)
<== Columns: id, carNum, brand, guidePrice, produceTime, carType
<== Row: 2, 1002, 奔驰, 55.00, 2021-05-24, 新能源
<== Row: 34, 6666, 五菱宏光, 50.00, 2000-08-22, 新能源
<== Row: 39, 1212, 好了, 50.00, 2000-08-22, 新能源
<== Row: 40, 1212, 好了, 50.00, 2000-08-22, 新能源
<== Total: 4sql语句为:
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = ?
将#{}修改为?,并将值传入
将SQL修改${}
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = ${carType} 执行程序查看日志:
==> Preparing: select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = 新能源
==> Parameters:sql语句:
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = 新能源
直接将值拼接在SQL语句中
- #{}:底层使用的是PreparedStatement对象,可以防止SQL注入,先编译SQL语句,然后再执行
- ${}:底层使用的是Statement对象,有SQL注入的风险,直接将值拼接在SQL语句中
什么使用${}:
- 当#{}无法处理时,使用${}。也就是需要先将参数拼接在SQL语句中,在执行。那就使用${},或者SQL语句中需要由外界传入SQL关键字,那么使用${},因为使用#{}的话,会在关键字外面拼接上'',这样的话,那么就不是关键字了,只是一个普通的字符串。比如排序的关键字:asc或者desc,或者表名。
- 如果只是普通的传值,那么优先使用#{}
2. 批量删除
需求:删除汽车id是1和2的记录
数据库:
准备接口:
/** * 根据多个id删除多条记录 * @param ids id字符串 * @return 影响条数 */ int deleteAllById(String ids);
SQL语句:
先使用#{}
delete from t_car where id in (#{ids}) 测试程序:
@Test public void testDeleteAllById() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); int i = mapper.deleteAllById("1,2"); System.out.println(i); sqlSession.commit(); sqlSession.close(); }
执行结果:
可以看到直接是将1,2当成一个整体放入在()中,所以最终的SQL语句为:
delete from t_car where id in ('1,2')
注意:?占位符,在赋值的时候,外面会有一个''包起来
将#{}修改成${}
delete from t_car where id in (${ids}) 再次执行程序查看结果:
提前将1,2拼接在()中。最终删除成功
数据库:
其他方式,等动态SQL中的forEach
3. 模糊查询
需求:查询品牌为奔驰的汽车信息
数据库:
准备接口:
/** * 根据汽车品牌模糊查询 * @param brand 汽车品牌 * @return 汽车信息 */ List
selectByBrandOfLike(String brand); SQL语句:
先使用#{}
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where brand like '%#{brand}%' 测试程序:
@Test public void testSelectByBrandOfLike() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List
cars = mapper.selectByBrandOfLike("奔驰"); cars.forEach(car-> System.out.println(car)); } ?占位符直接被'%%'包起来,这样的话,底层JDBC在赋值的时候找不到这个?,因为JDBC找的占位符是:like ?,但是这个SQL语句却是:like '%?%',只是将?当做这个字符串的一部分。
将#{}换成${}:
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where brand like '%${brand}%' 再次执行程序,查看结果:
将传入的值,提前拼接在'%%'中,这个SQL肯定执行成功。
其他方式:
- 使用mysql中的concat函数:concat('%',#{brand},'%')。它是将函数中的字符进行拼接
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where brand like concat('%',#{brand},'%') 这样的话:最后执行 brand like '%'?'%'
使用双引号将%包起来也是可以的。
另一种:
- 将%拼接到外面:like "%"#{brand}"%"
注意:%外面使用的是双引号,使用单引号查询不出来任何数据。必须使用双引号
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where brand like "%"#{brand}"%" 运行程序,查询结果:
九、核心配置文件的其他属性
1. 别名机制:
在Mapper.xml文件中,有的SQL语句需要返回POJO的类型,那么就需要在标签的resultType属性中写入这个POJO的全限定名。
例如:
Mybatis为我们提供了一种别名机制,也就是配置了这个属性,以后不需要再写全限定名,只需要写入别名即可。
这个在typeAliases标签中,为:typeAliase。
这个标签中有两个属性:
- type:POJO的全限定名
- alias:别名
如果有多个POJO需要起别名,就写多个typeAlias即可
将Mapper.xml文件中的resultType值修改成别名:
测试一下大写和小写有什么区别:
selectByCarType 测试程序:
@Test public void testSelectByCarType() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List
cars = mapper.selectByCarType("新能源"); cars.forEach(car-> System.out.println(car)); sqlSession.close(); } 执行结果:
==> Preparing: select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where car_type = ?
==> Parameters: 新能源(String)
<== Columns: id, carNum, brand, guidePrice, produceTime, carType
<== Row: 34, 6666, 五菱宏光, 50.00, 2000-08-22, 新能源
<== Total: 1
Car{id=34, carNum='6666', brand='五菱宏光', guidePrice=50.0, produceTime='2000-08-22', carType='新能源'}selectByBrandOfLike 测试程序:
@Test public void testSelectByBrandOfLike() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List
cars = mapper.selectByBrandOfLike("奔驰"); cars.forEach(car-> System.out.println(car)); } 测试结果:
==> Preparing: select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where brand like "%"?"%"
==> Parameters: 奔驰(String)
<== Columns: id, carNum, brand, guidePrice, produceTime, carType
<== Row: 41, 1006, 奔驰2, 10.00, 2001-01-20, 油燃车
<== Row: 42, 1005, 奔驰1, 5.00, 2005-05-15, 油燃车
<== Total: 2
Car{id=41, carNum='1006', brand='奔驰2', guidePrice=10.0, produceTime='2001-01-20', carType='油燃车'}
Car{id=42, carNum='1005', brand='奔驰1', guidePrice=5.0, produceTime='2005-05-15', carType='油燃车'}可以看到不管是小写还是大写,都可以运行成功,那么就证明这个别名不区分大小写。但是不能写错
这个typeAlias中的alias属性可以省略,那么省略后,这个别名是什么:
- 类的简名
- 例如:com.jiuxiao.pojo.Car --> car/CAR/Car/cAr/caR
新增一个根据id查询的接口:
/** * 根据汽车id查询信息 * @param id id * @return 汽车信息 */ Car selectById(Integer id);
SQL语句:
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = #{id} 测试程序:
@Test public void testSelectById() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = mapper.selectById(32); System.out.println(car); sqlSession.close(); }
执行结果:
==> Preparing: select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = ?
==> Parameters: 32(Integer)
<== Columns: id, carNum, brand, guidePrice, produceTime, carType
<== Row: 32, 1005, 法拉利, 100.00, 2000-05-23, 电动车
<== Total: 1
Car{id=32, carNum='1005', brand='法拉利', guidePrice=100.0, produceTime='2000-05-23', carType='电动车'}如果我们有多个POJO类,那么就需要配置多个typeAlias标签,所以Mybatis为我们提供了包的机制:
���别名是每个类的简名,不测试。
2. 注册Mapper
注册mapper的方式,也就是在mybatis-conf.xml中的mappers中填写Mapper.xml的方式:
- resource:从类的根路径下开始查找(之前说过)
- url:从任意位置开始查找(之前说过)
还是一种注册的方式是class,指定一个接口,mybatis会从该接口包下查找同名的Mapper.xml文件。
例如:指定的接口为com.jiuxiao.mapper.CarMapper,那么mybatis就会从com/jiuxiao/mapper/包下查找CarMapper.xml文件。
查看目录:先不修改CarMapper.xml,查看会不会有问题
随便运行一个程序:
可以异常信息的主要原因就是:没有找到com.jiuxiao.mapper.CarMapper.seelectById这个SQL语句。至于为什么这么长,是因为接口的全限名+方法名拼接而成
在resources目录下创建com.jiuxiao.mapper.CarMapper目录
注意:在resources下没有包的概念,只有目录,所有在建的时候要么一个一个的建,要么使用这种方式:com/jiuxiao/mapper,如果使用com.jiuxiao.mapper,那么会视为文件名,并不会分层
再次查看目录:上面java是类的根路径,下面resources是类的根路径。只是可视化工具的问题。
再次随便运行一个程序,就会运行成功:
可以看一下编译的缓存:
���提醒:如果使用class的这种方式,接口和xml文件必须在同一个目录下,名字必须一致。
接口有多个,需要配置多个class,所以mybatis提供了包的机制:
这种和class一样,不再演示。
3. 返回主键
在一些业务中,一张表需要使用另一张表的主键,这时另外一张的表的主键不是人工干扰。比如使用数据库的自增。那么我们获取起来就比较麻烦,需要增加完再进行查询,不能增加完就返回主键。
mybatis中提供了一些属性可以获取到增加后字段的值。
准备接口:
/** * 增加完汽车信息后返回主键 * @param car * @return */ int insertReturnId(Car car);
SQL语句:
insert into t_car (id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType}) 如何让主键返回,两个属性
- useGeneratedKeys:是否使用自动生成的主键
- keyProperty:返回的值添加到对象的哪个属性上。
insert into t_car (id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType}) 测试程序:
@Test public void testInsertReturnId() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(); car.setCarNum("1007"); car.setBrand("宝马"); car.setGuidePrice(50.0); car.setProduceTime("2022-10-01"); car.setCarType("新能源"); mapper.insertReturnId(car); // 获取该对象的主键 System.out.println(car.getId()); sqlSession.commit(); sqlSession.close(); }
运行结果:可以看到拿到了43
==> Preparing: insert into t_car (id,car_num,brand,guide_price,produce_time,car_type) values(null,?,?,?,?,?)
==> Parameters: 1007(String), 宝马(String), 50.0(Double), 2022-10-01(String), 新能源(String)
<== Updates: 1
43查看数据库是否一致:
十、Mybatis参数处理
准备表:t_student
插入数据:
POJO:
package com.jiuxiao.pojo; import java.util.Date; public class Student { private Long id; private String name; private String address; private Date birth; private Character sex; private Double height; @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", address='" + address + '\'' + ", birth=" + birth + ", sex=" + sex + ", height=" + height + '}'; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Date getBirth() { return birth; } public void setBirth(Date birth) { this.birth = birth; } public Character getSex() { return sex; } public void setSex(Character sex) { this.sex = sex; } public Double getHeight() { return height; } public void setHeight(Double height) { this.height = height; } }
1. 传入简单类型
简单类型:
- int,byte,short,long,float,double,char
- Integer,Byte,Short,Long,Float,Double,Cahracter
- String
- Date
需求:
- 根据id查询学生
- 根据name查询学生
- 根据birth查询学生
- 根据性别查询学生
准备接口:
先完成第一个需求:根据id查询学生
/** * 根据id查询学生信息 * @param id 学生id * @return 学生信息 */ Student selectById(Long id);
SQL语句:
select * from t_student where id = #{id} 测试程序:
@Test public void testSelectById() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = mapper.selectById(1L); System.out.println(student); sqlSession.close(); }
Student{id=1, name='张三', address='北京', birth=Sat Oct 01 00:00:00 CST 2022, sex=男, height=1.5}
其实想说的是:在SQL映射文件的标签中有一个parameterType属性。
这个属性干什么?
- 可以指定你传入的参数是什么类型,比如你传入的是Long类型,那么这里就需要填写:
再次执行程序,没有什么变化。
- 为了什么:
- 之前我们在进行传入参数的时候没有写过这个属性,为什么也可以。这是因为mybatis底层有类型的自动推算,它可以拿到我们接口中的参数类型,然后再一个个的判断。
- 如果我们写入的这个参数,指定类型,那么mybatis底层就会根据你传入的类型直接进行ps.setLong(),因为mybatis的底层是通过JDBC代码执行,JDBC代码赋值是通过ps.setXxxx(),所以如果我们指定了类型,可以直接让mybatis迅速的知道传入什么类型。提高了程序的执行效率
- 它还有内置的一套别名:查看mybatis中文网
- 我们直接写入别名也是可以的
���这个属性想写就写,不想写就不写,绝大部分都是不需要写的,只不过写了可以提高一些效率
还有另一种指定类型的方式:
- 在#{}中指定
select * from t_student where id = #{id,javaType=Long,jdbcType=BIGINT} 执行程序:
其中:
- javaType:该字段在java中的类型
- jdbcType:该字段在数据库中的类型
它和parameterType一样,可以省略。其中如果参数只有一个的话,#{}里面是可以随便写的
select * from t_student where id = #{gagagaga} 执行程序:
���完成剩下的需求
接口方法:
/** * 通过name查询学生 * @param name 学生name * @return 相关的学生信息 */ List
selectByName(String name); /** * 根据birth查询学生 * @param birth * @return */ List selectByBirth(Date birth); /** * 通过sex查询学生 * @param sex * @return */ List selectBySex(Character sex); SQL语句:
select * from t_student where name = #{name} select * from t_student where birth = #{birth} select * from t_student where sex = #{sex} 测试程序:
- 根据name查询:
@Test public void testSelectByName() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List
students = mapper.selectByName("张三"); students.forEach(student -> System.out.println(students)); sqlSession.close(); } [Student{id=1, name='张三', address='北京', birth=Sat Oct 01 00:00:00 CST 2022, sex=男, height=1.5}]
- 根据birth查询:
@Test public void testSelectByBirth() throws Exception { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date birth = sdf.parse("2022-09-01"); List
students = mapper.selectByBirth(birth); students.forEach(student -> System.out.println(students)); sqlSession.close(); } [Student{id=2, name='李四', address='河南', birth=Thu Sep 01 00:00:00 CST 2022, sex=女, height=1.75}]
- 根据sex查询:
@Test public void testSelectBySex() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List
students = mapper.selectBySex(Character.valueOf('男')); students.forEach(student -> System.out.println(students)); sqlSession.close(); } [Student{id=1, name='张三', address='北京', birth=Sat Oct 01 00:00:00 CST 2022, sex=男, height=1.5}]
2. 传入Map
前面说过,#{}里面必须写Map集合的key。直接写
接口方法:
/** * 根据map中的参数 对数据库增加记录 * @param map * @return */ int insertStudentByMap(Map
map); SQL语句:
insert into t_student(id,name,address,birth,sex,height) values(null,#{姓名},#{地址},#{生日},#{性别},#{身高}) 测试程序:
@Test public void testInsertStudentByMap() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Map
map = new HashMap<>(); map.put("姓名","王五"); map.put("地址","山东"); map.put("生日",new Date()); map.put("性别",'男'); map.put("身高",1.77); mapper.insertStudentByMap(map); sqlSession.commit(); sqlSession.close(); } 执行结果:
数据库:
3. 传入POJO
之前说过,#{}里面必须写POJO中的属性,因为mybatis底层会去调用属性的set/get方法。
接口方法:
/** * 根据pojo对象 对数据库增加记录 * @param student * @return */ int insertStudentByPOJO(Student student);
SQL语句:
insert into t_student(id,name,address,birth,sex,height) values(null,#{name},#{address},#{birth},#{sex},#{height}) 测试程序:
@Test public void testInsertStudentByPOJO() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = new Student(); student.setName("赵六"); student.setAddress("河北"); student.setBirth(new Date()); student.setSex('女'); student.setHeight(1.65); mapper.insertStudentByPOJO(student); sqlSession.commit(); sqlSession.close(); }
执行结果:
数据库:
4. 传入多个参数
需求:通过address和sex查询学生
接口方法:
/** * 根据address和sex查询学生信息 * @param address * @param sex * @return */ List
selectByAddressAndSex(String address,Character sex); SQL语句中的参数应该如何传入,先尝试一下使用参数名:
select * from t_student where address = #{address} and sex = #{sex} 测试程序:
@Test public void testSelectByAddressAndSex() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List
students = mapper.selectByAddressAndSex("河南", '女'); students.forEach(student -> System.out.println(students)); sqlSession.close(); } 执行后发生异常,异常信息:
主要信息:'address'参数找不到,可以尝试使用[arg0,agr1,param1,param2]
那么修改SQL语句中的参数为:arg0,agr1
select * from t_student where address = #{arg0} and sex = #{arg1} 执行程序成功:
它还说可以使用param1,param2
select * from t_student where address = #{param1} and sex = #{param2} 执行程序成功。
那么两个混着来呢:
select * from t_student where address = #{arg0} and sex = #{param2} 执行程序依旧成功。
通过程序可以看到,如果参数有多个时:
- arg0是第一个参数
- arg1是第二个参数
- param1是第一个参数
- param2是第二个参数
底层原理:mybatis在底层会创建一个Map集合,将arg0/param1作为key,方法上的参数作为value
比如:
Map
map = new HashMap<>(); map.put("arg0",name); map.put("arg1",sex); map.put("param1",name); map.put("param2",sex); 底层截图:
5. @param注解
上面的那种可读性太差,所以mybatis为我们提供了这种注解,可以替代arg0和arg1
用法:
List
selectByAddressAndSex(@Param("address") String address,@Param("sex") Character sex); 那么使用了注解后,arg0和arg1还能使用吗?
select * from t_student where address = #{arg0} and sex = #{arg0} 运行程序发生异常,异常信息:
主要:找不到'arg0',可以使用[address,sex,param1,param2]
可以看到param1和param2还可以继续使用,不演示了。
将StudentMapper.xml文件修改成addres和sex:
select * from t_student where address = #{address} and sex = #{sex} 执行程序:
为什么使用了注解后,arg0和arg1不能使用。使用的是注解中的参数呢?
6. @param源码
断点打在执行调用接口方法那里
底层使用的是JDK动态代理
- 第一步,进入到MapperProxy这个代理类的invoke()方法,每个JDK代理类中都有这个方法
- 进入另一个重载方法:调用execute(),这是一个执行器
- 进入到MappedMethod类中:
- 这行代码是在获取SQL映射文件中的标签名:我们执行的是查询,所以下一步会进入到select
- 因为我们的返回值是一个List集合,为多个参数,所以下一步会走到executeForMany方法
- 进入到这个方法:
- 这个param就是那个Map集合,所以真正的来源在于这个convertArgsToSqlCommandParam方法
- 再进
- 进来后就会发现这里和前面的一模一样,只不过这里的names变了,里面存储的不再是arg0和arg1,存储的是address和sex,所以说使用了注解后arg0和arg1不能在使用,但是在程序的下面依然创建param1和param2,这也是为什么param1和param2还能用的原因,因为在最后的Map集合中存在。
names样式:
- 走完方法,出来后:
十一、Mybatis结果集处理
POJO:Car
package com.jiuxiao.pojo; /** * Car * @author 酒萧 * @version 1.0 * @since 1.0 */ public class Car { private Long id; private String carNum; private String brand; private Double guidePrice; private String produceTime; private String carType; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getCarNum() { return carNum; } public void setCarNum(String carNum) { this.carNum = carNum; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public Double getGuidePrice() { return guidePrice; } public String getProduceTime() { return produceTime; } public void setProduceTime(String produceTime) { this.produceTime = produceTime; } public void setGuidePrice(Double guidePrice) { this.guidePrice = guidePrice; } public String getCarType() { return carType; } public void setCarType(String carType) { this.carType = carType; } @Override public String toString() { return "Car{" + "id=" + id + ", carNum='" + carNum + '\'' + ", brand='" + brand + '\'' + ", guidePrice=" + guidePrice + ", produceTime='" + produceTime + '\'' + ", carType='" + carType + '\'' + '}'; } }
数据库表:t_car
1. 返回一条Car记录
需求:查询id为32的汽车信息
接口方法:
package com.jiuxiao.mapper; import com.jiuxiao.pojo.Car; public interface CarMapper { /** * 根据id查询汽车信息,返回一条记录 * @return */ Car selectById(Long id); }
SQL语句:
返回字段先使用*的方式,这种会有什么后果
select * from t_car where id = #{id} 测试程序:
@Test public void testSelectById() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = mapper.selectById(32L); System.out.println(car); sqlSession.close(); }
执行结果:
Car{id=32, carNum='null', brand='法拉利', guidePrice=null, produceTime='null', carType='null'}
可以看到除了id和brand有值,其他都为空,这是因为数据库表中的字段和POJO对象中的属性名没有映射,所以之前在查询的时候使用as别名的方式来映射
修改SQL语句:
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = #{id} 再次运行程序,查看执行结果:
Car{id=32, carNum='1005', brand='法拉利', guidePrice=100.0, produceTime='2000-05-23', carType='电动车'}
全部有值,证明都数据库表中的字段和POJO对象中的属性名一一映射上了。
2. 返回多条Car记录
需求:查询所有的汽车信息
接口方法:
/** * 查询所有的汽车信息,返回多条记录 * @return */ List
selectAll(); SQL语句:
select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car 测试程序:
@Test public void testSelectAll() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List
cars = mapper.selectAll(); cars.forEach(car-> System.out.println(car)); sqlSession.close(); } 执行结果:
Car{id=32, carNum='1005', brand='法拉利', guidePrice=100.0, produceTime='2000-05-23', carType='电动车'}
Car{id=34, carNum='6666', brand='五菱宏光', guidePrice=50.0, produceTime='2000-08-22', carType='新能源'}
Car{id=41, carNum='1006', brand='奔驰2', guidePrice=10.0, produceTime='2001-01-20', carType='油燃车'}
Car{id=42, carNum='1005', brand='奔驰1', guidePrice=5.0, produceTime='2005-05-15', carType='油燃车'}
Car{id=43, carNum='1007', brand='宝马', guidePrice=50.0, produceTime='2022-10-01', carType='新能源'}如果返回多条记录,使用一个Car接收会怎样?
/** * 测试返回多条记录,使用一个Car对象接收 * @return */ Car selectAll();
SQL语句不变,测试程序返回值换成Car对象。运行程序出现异常:
TooManyResultsException:应该返回一条记录或者没有记录,但是却返回了5条记录。
可以看到底层调用的是selectOne(),所以这种方式不行。
3. 返回一条Map集合
场景:当从数据库中查询的字段没有合适的POJO对象接收时,可以使用Map集合进行接收。
需求:根据car_num为6666的汽车信息
接口方法:
/** * 根据car_num查询 返回一条记录 使用map集合接收 * @param carNum * @return */ Map
selectByCarNumReturnMap(String carNum); SQL语句:返回类型为map,因为返回的结果是一条记录。
不需要和POJO对象属性名映射,所以直接使用"*"
select * from t_car where car_num = #{carNum} 测试程序:
@Test public void testSelectByCarNumReturnMap() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Map
map = mapper.selectByCarNumReturnMap("6666"); System.out.println(map); sqlSession.close(); } 执行结果:将字段作为map的key,值作为map的value。
{car_num=6666, id=34, guide_price=50.00, produce_time=2000-08-22, brand=五菱宏光, car_type=新能源}
4. 返回多条Map集合
需求:模糊查询汽车品牌是奔驰的记录
准备接口:
/** * 根据brand模糊查询,返回多条map集合 * @param brand * @return */ List
SQL语句:因为返回值是List,所以底层会调用selectList(),因此只需要关心集合中的每条元素类型即可
select * from t_car where brand like "%"#{brand}"%" 测试程序:
@Test public void testSelectByBrandLikeReturnMap() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List
执行结果:
{car_num=1006, id=41, guide_price=10.00, produce_time=2001-01-20, brand=奔驰2, car_type=油燃车}
{car_num=1005, id=42, guide_price=5.00, produce_time=2005-05-15, brand=奔驰1, car_type=油燃车}5. 返回大Map集合
大Map-->map集合中嵌套map
场景:通过某个值,查询某条记录。如果使用List,那么就需要先遍历,然后再一个个的判断。使用大Map的方式,将这个值作为map的key,记录作为Map集合。这样能够快速的查找
需求:查询所有的记录
/** * 查询所有 返回一个大Map * 将数据库表中的id作为map的key * 将数据库表中的每条记录作为map的value * @return */ @MapKey("id") // 数据库中哪个字段作为map的key Map
> selectAllReturnMap(); SQL语句:
select * from t_car 测试程序:
@Test public void testSelectAllReturnMap() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Map
> maps = mapper.selectAllReturnMap(); System.out.println(maps); sqlSession.close(); } 执行结果:
{
32={car_num=1005, id=32, guide_price=100.00, produce_time=2000-05-23, brand=法拉利, car_type=电动车},
34={car_num=6666, id=34, guide_price=50.00, produce_time=2000-08-22, brand=五菱宏光, car_type=新能源},
41={car_num=1006, id=41, guide_price=10.00, produce_time=2001-01-20, brand=奔驰2, car_type=油燃车},
42={car_num=1005, id=42, guide_price=5.00, produce_time=2005-05-15, brand=奔驰1, car_type=油燃车},
43={car_num=1007, id=43, guide_price=50.00, produce_time=2022-10-01, brand=宝马, car_type=新能源}
}Map
> maps = mapper.selectAllReturnMap();
Mapmap = maps.get(32L);
System.out.println(map);{car_num=1005, id=32, guide_price=100.00, produce_time=2000-05-23, brand=法拉利, car_type=电动车}
6. resultMap手动映射
如果一直起别名就会特别麻烦,所以mybatis提供了resultMap这个属性,将返回的结果集一一映射。
如何配置:
如何使用:
select * from t_car 测试这个查询全部,执行结果:
Car{id=32, carNum='1005', brand='法拉利', guidePrice=100.0, produceTime='2000-05-23', carType='电动车'}
Car{id=34, carNum='6666', brand='五菱宏光', guidePrice=50.0, produceTime='2000-08-22', carType='新能源'}
Car{id=41, carNum='1006', brand='奔驰2', guidePrice=10.0, produceTime='2001-01-20', carType='油燃车'}
Car{id=42, carNum='1005', brand='奔驰1', guidePrice=5.0, produceTime='2005-05-15', carType='油燃车'}
Car{id=43, carNum='1007', brand='宝马', guidePrice=50.0, produceTime='2022-10-01', carType='新能源'}7. 使用开启驼峰命名自动映射:
使用的前提:
- 属性名需要遵循Java中的命名规范,数据中的字段名需要遵循SQL的命名规范
- java:首字母小写,后面的每个首字母大写
- SQL:全部小写,每个单词使用下划线分割
java属性名
SQL字段名
carNum
car_num
carType
car_type
如何开启:
在mybatis-conf.xml核心配置文件的settings中配置:
将上方起别名的SQL语句修改成"*":
select * from t_car where id = #{id} 执行该测试程序,运行结果:
Car{id=32, carNum='1005', brand='法拉利', guidePrice=100.0, produceTime='2000-05-23', carType='电动车'}
8. 返回总条数
接口方法:
/** * 获取总条数 * @return */ Long selectCount();
SQL语句:
select count(1) from t_car 测试程序:
@Test public void testSelectCount() { Long total = SqlSessionUtil.openSqlSession().getMapper(CarMapper.class).selectCount(); System.out.println("总条数:"+total); }
执行结果:
==> Preparing: select count(1) from t_car
==> Parameters:
<== Columns: count(1)
<== Row: 5
<== Total: 1
总条数:5
十二、动态SQL
为什么要使用动态SQL?
可以根据不同的条件查询不同的结果,让SQL语句变得更加灵活。
POJO:
package com.jiuxiao.pojo; /** * Car * @author 酒萧 * @version 1.0 * @since 1.0 */ public class Car { private Long id; private String carNum; private String brand; private Double guidePrice; private String produceTime; private String carType; public Car() { } public Car(Long id, String carNum, String brand, Double guidePrice, String produceTime, String carType) { this.id = id; this.carNum = carNum; this.brand = brand; this.guidePrice = guidePrice; this.produceTime = produceTime; this.carType = carType; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getCarNum() { return carNum; } public void setCarNum(String carNum) { this.carNum = carNum; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public Double getGuidePrice() { return guidePrice; } public String getProduceTime() { return produceTime; } public void setProduceTime(String produceTime) { this.produceTime = produceTime; } public void setGuidePrice(Double guidePrice) { this.guidePrice = guidePrice; } public String getCarType() { return carType; } public void setCarType(String carType) { this.carType = carType; } @Override public String toString() { return "Car{" + "id=" + id + ", carNum='" + carNum + '\'' + ", brand='" + brand + '\'' + ", guidePrice=" + guidePrice + ", produceTime='" + produceTime + '\'' + ", carType='" + carType + '\'' + '}'; } }
数据库:
1. if标签
该标签中只有一个test属性,这个属性的值要么是true或者false。如果是true那么标签中的SQL语句将拼接在前面的SQL语句后面
需求:根据汽车品牌、价格、类型多条件查询
接口方法:
public interface CarMapper { /** * 多条件查询 * @param brand 品牌 * @param carType 类型 * @param guidePrice 价格 * @return */ List
selectByBrandOrTypeOrGuidePrice(@Param("brand") String brand, @Param("carType") String carType, @Param("guidePrice") Double guidePrice); } SQL语句:
参数要求:
- test中传入的参数是方法中@param注解中的的参数名
- 如果没有使用@param注解,那么就是arg0 或者 param1
- 如果参数是一个POJO对象,里面传入的是该对象中的属性
在mybatis中,不能使用&&,提供了and 和 or
select * from t_car where brand like "%"#{brand}"%" and car_type = #{carType} and guide_price > #{guidePrice} 测试程序:
- 先将参数全部传入:
@Test public void testSelectByBrandOrTypeOrGuidePrice() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); // 先将条件全部传入 List
cars = mapper.selectByBrandOrTypeOrGuidePrice("奔驰","油燃车",5.0); cars.forEach(car-> System.out.println(car)); sqlSession.close(); } 执行结果:每个条件都成立,全部拼接在后面。
- 只传入第一个参数,剩下的为null:
@Test public void testSelectByBrandOrTypeOrGuidePrice() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); // 只传入第一个参数 List
cars = mapper.selectByBrandOrTypeOrGuidePrice("奔驰",null,null); cars.forEach(car-> System.out.println(car)); sqlSession.close(); } 执行结果:后面两个不成立的SQL语句直接忽略
- 全为null会怎样:
@Test public void testSelectByBrandOrTypeOrGuidePrice() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); // 都不传 List
cars = mapper.selectByBrandOrTypeOrGuidePrice(null,null,null); cars.forEach(car-> System.out.println(car)); sqlSession.close(); } 执行程序后发生异常:
异常的原因:where后面没有跟条件,为了保证就算都不传入参数也能继续查询,在where后面给一个永久成立的语句,修改之后的SQL语句:
select * from t_car where 1=1 and brand like "%"#{brand}"%" and car_type = #{carType} and guide_price > #{guidePrice} 在测试一下三个参数都不传入,执行结果:
2. where标签
作用:让where子句更加灵活,能够根据判断随时添加where或删除where关键字。
当where标签中条件都不成立,那么where标签保证不会生成where子句
可以自动删除某些条件前面的and或者or,注意:是删除前面,后面的无法删除。
继续使用上一个需求,修改SQL语句即可:
select * from t_car and brand like "%"#{brand}"%" and car_type = #{carType} and guide_price > #{guidePrice}
- 三个参数传入:
@Test public void testSelectByBrandOrTypeOrGuidePrice() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); // 先将条件全部传入 List
cars = mapper.selectByBrandOrTypeOrGuidePrice("奔驰","油燃车",5.0); cars.forEach(car-> System.out.println(car)); sqlSession.close(); } 执行结果,可以看到自动的在条件前面拼接一个WHERE,并将最前面的and删除了:
- 如果三个参数都不传入:
@Test public void testSelectByBrandOrTypeOrGuidePrice() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); // 先将条件全部传入 List
cars = mapper.selectByBrandOrTypeOrGuidePrice(null,null,null); cars.forEach(car-> System.out.println(car)); sqlSession.close(); } 执行结果,条件都不成立,那么WHERE子句不会添加:
3. trim标签
该标签中的属性:
- prefix:在trim标签中的语句前添加内容
- suffix:在trim标签中的语句后添加内容
- prefixOverrides:剔除前缀
- suffixOverrides:剔除后缀
需求:增加一个汽车信息,通过trim和if标签动态的添加需要提交的参数,没有提交的为null
接口方法:
/** * 通过trim标签和if标签来动态的进行增加 * @param car * @return */ int insertByTrim(Car car);
SQL语句:
insert into t_car id, valuescar_num, brand, guide_price, produce_time, car_type null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType} 测试程序:
@Test public void testInsertByTrim() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(); car.setCarNum("1008"); car.setBrand("比亚迪"); car.setGuidePrice(20.0); car.setProduceTime("1990-05-25"); car.setCarType("电动车"); int count = mapper.insertByTrim(car); System.out.println(count); sqlSession.commit(); sqlSession.close(); }
执行结果:
数据库:
4. set标签
使用在update语句中,用来生产set关键字。同时去掉多余","
比如我们只提交不为空的字段,值为空或者为空串,不修改原来的值。
需求:修改id为42的汽车信息
接口方法:
/** * 根据set标签动态的更新提交的数据 * @param car * @return */ int updateBySet(Car car);
SQL语句。先不使用set,看看不提交的数据会变成什么:
update t_car set car_num = #{carNum}, brand = #{brand}, guide_price = #{guidePrice}, produce_time = #{produceTime}, car_type = #{carType} where id = #{id} 测试程序:
- 提交全部数据:
@Test public void testUpdateBySet() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(42L, "1234", "兰博基尼", 50.1, "1988-12-10", "电动车"); int i = mapper.updateBySet(car); System.out.println(i); sqlSession.commit(); sqlSession.close(); }
执行结果:
数据库:
- 只提交一部分,看看其他的字段会发生什么:
@Test public void testUpdateBySet() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(42L, "6666", "特斯拉",null,null,null); int i = mapper.updateBySet(car); System.out.println(i); sqlSession.commit(); sqlSession.close(); }
执行结果:将null传入
数据库:没有提交的数据变为了null
使用set标签,修改SQL语句:
update t_car where id = #{id} car_num = #{carNum}, brand = #{brand}, guide_price = #{guidePrice}, produce_time = #{produceTime}, car_type = #{carType}, 测试程序:
@Test public void testUpdateBySet() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(44L, "5678", "丰田", null,null,null); int i = mapper.updateBySet(car); System.out.println(i); sqlSession.commit(); sqlSession.close(); }
执行结果:只在set后面拼接了满足条件的更新语句
数据库:
5. forEach标签
用来循环传入的数据或者集合,动态的生成SQL语句。
属性:
- collection:指定需要遍历的数据或者集合
- item:每次遍历后的元素,也就是数据或集合中的每个元素
- separator:每个循环语句之间的分割符
- open:以什么开始
- close:以什么结尾
collection里面放什么:
- 如果参数类型是数组:
- 只能使用array或者arg0
- 如果参数类型是集合:
- 只能使用list或者arg0
- 如果使用了@param注解:
- 只能使用注解中的参数名或者param1
5.1 批量删除
需求:删除id=42,43,44的汽车信息
- 使用in的方式:
接口方法:
/** * 使用in的方式批量删除 * @param ids * @return */ int deleteAllByIn(Long[] ids);
SQL语句,这里的参数如何填写,先写一下参数名试试:
delete from t_car where id in #{id} 测试程序:
@Test public void testDeleteAllByIn() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Long[] ids = {42L,43L,44L}; int count = mapper.deleteAllByIn(ids); System.out.println(count); sqlSession.commit(); sqlSession.close(); }
执行程序,抛出异常,异常信息:
绑定异常:没有找到'id',但是可以使用[array,arg0]
修改SQL语句中的参数,将collection修中的ids修改成array:
delete from t_car where id in #{id} 再次执行程序:
数据库:
删除成功,这就证明底层的Map数据是这样的:
Map
map = new HashMap<>(); map.put("array",ids); map.put("arg0",ids); 如果使用注解,那么参数就不能使用array。需要使用注解中的参数名:
接口方法:
/** * 使用in的方式批量删除 * @param ids * @return */ int deleteAllByIn(@Param("ids") Long[] ids);
异常信息:
没有找到'array',但是可以使用[ids,param1]
- 使用or的方式批量删除
需求:刪除id为34,41的汽车信息
修改SQL语句:
delete from t_car where id = #{id} 测试程序不变:
数据库:
5.2 批量增加
接口方法:
int insertAll(@Param("cars") List
cars); SQL语句:
insert into t_car id,car_num, brand, guide_price,produce_time,car_type valuesnull,#{car.carNum},#{car.brand},#{car.guidePrice},#{car.produceTime},#{car.carType} 测试程序:
@Test public void testInsertAll() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car1 = new Car(null,"1006","宝马",10.2,"2005-05-15","新能源"); Car car2 = new Car(null,"1007","奔驰",50.5,"1889-12-05","油燃车"); Car car3 = new Car(null,"1008","比亚迪",5.2,"2020-02-22","电动车"); List
carList = Arrays.asList(car1, car2, car3); int count = mapper.insertAll(carList); System.out.println(count); sqlSession.commit(); sqlSession.close(); } 数据库:
6. SQL标签与include
sql:用来声明代码片段
include:用来引入代码片段
使用:
id,car_num, brand, guide_price,produce_time,car_type select from t_car and brand like "%"#{brand}"%" and car_type = #{carType} and guide_price > #{guidePrice} 测试这个查询:
十三、高级映射与延迟加载
数据库表:
学生表:t_stu
班级表:
表之间的关系:
1. 多对一
java中如何表现,如何去设计POJO:
- 因为每个学生只对应一个班级,所以可以将这个班级当成学生类的属性。
班级类:t_clazz
package com.jiuxiao.pojo; /** * 班级类 * 一方 */ public class Clazz { private Integer cid; private String cname; @Override public String toString() { return "Clazz{" + "cid=" + cid + ", cname='" + cname + '\'' + '}'; } public Integer getCid() { return cid; } public void setCid(Integer cid) { this.cid = cid; } public String getCname() { return cname; } public void setCname(String cname) { this.cname = cname; } }
学生类:t_stu
package com.jiuxiao.pojo; /** * 学生表 * 多方 */ public class Student { private Integer sid; private String sname; // 持有一方的对象 private Clazz clazz; @Override public String toString() { return "Student{" + "sid=" + sid + ", sname='" + sname + '\'' + ", clazz=" + clazz + '}'; } public Integer getSid() { return sid; } public void setSid(Integer sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } public Clazz getClazz() { return clazz; } public void setClazz(Clazz clazz) { this.clazz = clazz; } }
查询的几种方式:
- 一条SQL语句,借助resultMap使用级联属性映射
- 一条SQL语句,借助resultMap中的associaction标签
- 两条SQL语句,借助resultMap分步查询(常用:可复用,支持懒加载)
需求:查询学生姓名以及所在班级
1.1 级联属性映射
接口方法:
public interface StudentMapper { /** * 使用级联属性方式 查询学生和所在班级 * @param sid 学生id * @return 学生信息 */ Student selectById(Integer sid); }
SQL语句:
select s.sid,s.sname,c.cname from t_stu s left join t_clazz c on s.cid = c.cid where s.sid = #{sid} 测试程序:
@Test public void testSelectById() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = mapper.selectById(1); System.out.println("学生名称--> "+student.getSname()); System.out.println("所在班级--> "+student.getClazz().getCname()); sqlSession.close(); }
执行结果:
学生名称--> 小明
所在班级--> 高三一班1.2 使用association
接口方法:
/** * 使用association完成连表查询 * @param sid * @return */ Student selectByIdOfAssociation(Integer sid);
SQL语句:
select s.sid,s.sname,c.cname from t_stu s left join t_clazz c on s.cid = c.cid where s.sid = #{sid} 测试程序:
@Test public void testSelectByIdOfAssociation() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = mapper.selectByIdOfAssociation(2); System.out.println("学生名称--> "+student.getSname()); System.out.println("所在班级--> "+student.getClazz().getCname()); sqlSession.close(); }
执行结果:
学生名称--> 小花
所在班级--> 高三一班���1.3 分步查询
只需要修改这几个位置:
第一处:不需要指向一个java类,去指向一个sqlId。其中column属性作为传入该sqlId的参数
select sid,sname,cid from t_stu where sid = #{sid} 第二处:在clazzMapper接口中添加该方法:
public interface ClazzMapper { Clazz selectById(Integer cid); }
以及SQL语句:
select cname from t_clazz where cid = #{cid} 测试原来的程序,查看执行结果:可以看到执行了两条SQL语句。
1.3.1延迟记载
- 重复调用:如果这时有一个需求是通过学生的id查询学生信息,并不需要对应的班级信息那么就只执行查询学生的这条SQL。相同,如果只需要查询班级也是一样
- 懒加载:
- 什么使用使用到就什么时候加载
- 不使用就不会加载
如何设置懒加载:
- 局部设置:只能让当前resultMap起作用
添加一个fetchType=lazy
测试:只查询学生姓名
@Test public void testSelectByIdOfAssociation() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = mapper.selectByIdOfAssociation(2); System.out.println("学生名称--> "+student.getSname()); // 不去获取班级信息 // System.out.println("所在班级--> "+student.getClazz().getCname()); sqlSession.close(); }
执行结果:只执行了一条SQL语句
将注释放开再次执行:先去查的学生表,因为设置了懒加载。所以等调用的时候才执行
- 全局设置懒加载
在mybatis-conf.xml设置
这样每个resultMap里面都会有懒加载,如果某个不想使用懒加载,将其中的fetchType设置成eager
2. 一对多
一对多:一方为主表,在这里Clazz为主表,这个POJO如何设计:
在clazz类中使用一个集合存储student。对应多个学生
public class Clazz { private Integer cid; private String cname; private List
studentList; // 构造方法 // tostring // setter getter } 查询的几种方式:
- 使用collection标签
- 分步查询
2.1 使用collection
接口方法:
/** * 使用collection进行查询班级信息 已经对应的学生 * @param cid * @return */ Clazz selectByIdOfCollection(Integer cid);
SQL语句:
select c.cid,c.cname,s.sid,s.sname from t_clazz c left join t_stu s on c.cid = s.cid where c.cid = #{cid} 测试程序:
@Test public void testSelectByIdOfCollection() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); ClazzMapper mapper = sqlSession.getMapper(ClazzMapper.class); Clazz clazz = mapper.selectByIdOfCollection(1001); System.out.println("班级名称--> "+clazz.getCname()); clazz.getStudentList().forEach(student -> System.out.println("学生姓名--> "+student.getSname())); sqlSession.close(); }
执行结果:
班级名称--> 高三一班
学生姓名--> 小明
学生姓名--> 小花2.2 使用分步查询
修改clazzMapper.xml文件:
select cid,cname from t_clazz where cid = #{cid} 编写studentMapper接口方法:
List
selectByCid(Integer cid); SQL语句:
select sname from t_stu where cid = #{cid} 执行原来的程序,结果:
因为在mybatis-conf.xml配置文件中设置了全局懒加载,所以直接使用:
将学生那一部分注释:
@Test public void testSelectByIdOfCollection() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); ClazzMapper mapper = sqlSession.getMapper(ClazzMapper.class); Clazz clazz = mapper.selectByIdOfCollection(1001); System.out.println("班级名称--> "+clazz.getCname()); // 不获取学生信息 // clazz.getStudentList().forEach(student -> System.out.println("学生姓名--> "+student.getSname())); sqlSession.close(); }
再次执行:只有一条SQL语句
如果这里不想使用懒加载,那么在collection中设置fetchType="eager"即可
十四、Mybatis缓存
缓存:cache
缓存的作用:通过减少IO的方式,在内存中操作,从而提高程序的执行效率
mybatis中的缓存:将查询后的语句放在缓存中,下一次的查询语句如果和上一次的查询语句一致,那么就直接从缓存中,不会去查询库。
mybatis中的缓存:
- 一级缓存:将查询到的结果放在sqlSession中,作用域是同一个sqlSession。两个不同的sqlSession对象, 对应两个缓存
- 二级缓存:将查询到的结果放在sqlSessionFactory中,作用域是同一个sqlSessionFactory。两个不同的sqlSessionFactory对象,对应两个缓存
- 第三方缓存组件:EhCache【java语言开发】,Memcache【C语言开发】
Mybatis中的缓存只针对DQL有效,也就是只存储select语句
1. 一级缓存
一级缓存默认是开启的,不需要任何配置。
只要查询语句都是同一个,都在一个sqlSession中,那么就会走缓存。
public interface CarMapper { /** * 根据id查询汽车信息 * @param id * @return */ Car selectById(Long id); }
select * from t_car where id = #{id} @Test public void testSelectById() throws IOException { // 不能用工具类 SqlSessionFactory build = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-conf.xml")); SqlSession sqlSession = build.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); // 这条查询语句,会被存储到sqlSession中 Car car1 = mapper.selectById(32L); System.out.println(car1); // 第二次相同的查询语句,走缓存 Car car2 = mapper.selectById(32L); System.out.println(car2); sqlSession.close(); }
什么情况下不走缓存:
- sqlSession对象不同
- 查询语句不同
@Test public void testSelectById() throws IOException { // 不能用工具类 SqlSessionFactory build = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-conf.xml")); // 第一个sqlSession对象 SqlSession sqlSession1 = build.openSession(); CarMapper mapper = sqlSession1.getMapper(CarMapper.class); // 这条查询语句,会被存储到sqlSession1中 Car car1 = mapper.selectById(32L); System.out.println(car1); // 第二个sqlSession对象 SqlSession sqlSession2 = build.openSession(); CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class); // 这条查询语句,会被存储到sqlSession2中 Car car2 = mapper2.selectById(32L); System.out.println(car2); sqlSession1.close(); sqlSession2.close(); }
如何让一级缓存失效:
- 在两条查询语句之间,手动清空一级缓存
sqlSession.clearCache(); SqlSession sqlSession = build.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); // 这条查询语句,会被存储到sqlSession中 Car car1 = mapper.selectById(32L); System.out.println(car1); // 手动清空缓存 sqlSession.clearCache(); // 第二次相同的查询语句,走缓存 Car car2 = mapper.selectById(32L); System.out.println(car2); sqlSession.close();
- 在两条查询语句之间,执行了insert update delete操作。【无论哪张表,都会清空缓存】
/** * 删除学生信息 */ int deleteStudent();
delete from t_student where id = #{1} 2. 二级缓存
查询语句都是在同一个sqlSessionFactory对象,那么就会走二级缓存
开启二级缓存:
- 在mybatis-conf.xml中设置
全局地开启或关闭所有映射文件中已配置的任何缓存。默认是true,无需配置 - 在需要使用二级缓存的sqlMappe.xml文件中添加配置:
- 在二级缓存中的实体类必须实现序列化接口,也就是实现java.io.Serializable接口
- sqlSession对象进行提交或关闭后,才会写入到二级缓存中。
public class Car implements Serializable {
// ...
}@Test public void testSelectById() throws IOException { // 同一个sqlSessionFactory对象 SqlSessionFactory build = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-conf.xml")); // 第一个sqlSession SqlSession sqlSession1 = build.openSession(); // 这条查询过后会进入到sqlSession1的一级缓存中 CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class); Car car1 = mapper1.selectById(32L); System.out.println(car1); // 关闭sqlSession后,才会进入到二级缓存中 sqlSession1.close(); // 第二个sqlSession SqlSession sqlSession2 = build.openSession(); // 这条查询过后会进入到sqlSession2的一级缓存中 CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class); Car car2 = mapper2.selectById(32L); System.out.println(car2); sqlSession2.close(); }
二级缓存如何失效:只要两次查询之间,执行insert update delete操作,缓存就会失效【无论是哪一张表】
十五、Mybatis逆向工程
- pom依赖:
org.mybatis.generator mybatis-generator-maven-plugin 1.4.1 true
- 类路径下创建generatorConfig.xml
十六、Mybatis注解式开发
SQL语句通过注解的方式进行操作,不需要xml文件。
1. 增加 @Insert
@Insert("insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})") int insert(Car car); @Test public void testInsert() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(); car.setCarNum("1122"); car.setBrand("比亚迪"); car.setProduceTime("2022-02-22"); car.setGuidePrice(50.0); car.setCarType("电动车"); int count = mapper.insert(car); System.out.println(count); sqlSession.commit(); sqlSession.close(); }
2. 删除 @Delete
@Delete("delete from t_car where id = #{id}") int deleteById(Integer id);
@Test public void testDeleteById() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); int count = mapper.deleteById(54); System.out.println(count); sqlSession.commit(); sqlSession.close(); }
3. 更新 @Update
@Update("update t_car set car_num = #{carNum},brand = #{brand},guide_price = #{guidePrice},produce_time = #{produceTime},car_type = #{carType} where id = #{id}") int update(Car car);
@Test public void testUpdate() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(); car.setId(32L); car.setCarNum("6666"); car.setBrand("宝马"); car.setProduceTime("1899-05-12"); car.setGuidePrice(5.20); car.setCarType("油燃车"); int count = mapper.update(car); System.out.println(count); sqlSession.commit(); sqlSession.close(); }
4. 查询 @Select
@Select("select * from t_car where id = #{id}") Car selectById(Long id);
@Test public void testSelectById() { SqlSession sqlSession = SqlSessionUtil.openSqlSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = mapper.selectById(32L); System.out.println(car); sqlSession.close(); }
5. 映射 @Results
@Select("select * from t_car where id = #{id}") @Results({ @Result(property = "id",column = "id"), @Result(property = "carNum",column = "car_num"), @Result(property = "brand",column = "brand"), @Result(property = "guidePrice",column = "guide_price"), @Result(property = "produceTime",column = "produce_time"), @Result(property = "carType",column = "car_type"), }) Car selectById(Long id);