前言 学习 MyBatis 之前,建议学习一下 Maven 基础,有助于我们的理解 可以参考之前的文章:Maven 基础 官方文档:MyBatis 中文网 参考视频:B站动力节点老杜
一、MyBatis 概述 1.1 框架
框架,framework,本质上是对通用代码进行封装,提前写好一套接口和类,在写项目时直接引入这些接口和类(引入框架),提高开发效率
Java 常用框架
SSM:Spring + SpringMVC + MyBatis
SpringBoot
SpringCloud
……
框架一般以 jar 包形式存在(jar 包中有 class 文件和各种配置文件)
1.2 回顾三层架构
表现层(UI):① 接收前端请求,② 返回 json 数据给前端
业务逻辑层(BLL):① 处理表现层转发的前端请求(具体业务),② 将持久层获取的数据返回到表现层
数据访问层(DAL):操作数据库完成 CRUD,并将数据返回给上一层(业务逻辑层)
MyBatis 就是持久层框架
1.3 JDBC 的不足
SQL 语句写死在 Java 程序中,不灵活。修改 SQL 的话就要修改 Java 代码,违背 OCP 原则(开闭原则)
给 ? 传值很繁琐
将结果集封装成 Java 对象也很繁琐
1.4 了解 MyBatis MyBatis 属于三层架构中的持久层框架,本质上是对 JDBC 的封装,通过 MyBatis 完成 CRUD
ORM
MyBatis 就是一个 ORM 框架,是半自动的 ORM,因为 SQL 语句需要程序员自己编写
MyBatis 框架特点:
支持定制化 SQL、存储过程、基本映射以及高级映射
避免了几乎所有的 JDBC 代码手动设置参数以及获取结果集
支持 XML 开发,也支持注解式开发(XML 方式使用较多,保证了 sql 语句的灵活)
通过接口,将对象和数据相互转化
体积小:两个 jar 包,两个 XML 配置文件
sql 解耦合
提供了基本映射和高级映射标签
提供了 XML 标签,支持动态 SQL 的编写
……
二、MyBatis 入门程序 2.1 环境 软件
intelliJ IDEA:2023.2.1
Navicat for MySQL:16.0.14
MySQL 数据库:8.0.33
组件
MySQL 驱动:8.0.33
MyBatis:3.5.13
JDK:Java 17
JUnit
Logback
2.2 入门程序开发步骤
准备数据库表
IDEA –> new project,配置 JDK
新建一个 Maven 模块 ① 引入 mybatis 依赖和 mysql 依赖 pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependencies > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.13</version > </dependency > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > <version > 8.0.33</version > </dependency > </dependencies >
② 在 src/main/resources 中编写 mybatis 核心配置文件 mybatis-config.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> // 事务管理 <dataSource type ="POOLED" > <property name ="driver" value ="com.mysql.cj.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://localhost:3306/formybatis" /> <property name ="username" value ="root" /> <property name ="password" value ="123456" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="XxxMapper.xml" /> </mappers > </configuration >
③ 在 src/main/resources 中编写 SQL 语句的配置文件,XxxMapper.xml(一张表对应一个),以 insert 语句为例
1 2 3 4 5 6 7 8 9 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="org.mybatis.example.BlogMapper" > <insert id ="xxx" > </insert > </mapper >
④ 编写 MyBatis 程序 SqlSessionFactoryBuilder –> SqlSessionFactory –> SqlSession
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SqlSessionFactoryBuilder ssfb = new SqlSessionFactoryBuilder ();InputStream is = Resources.getResourceAsStream("mybatis 核心配置文件的路径" ); SqlSessionFactory ssf = ssfb.build(is);SqlSession sqlSession = ssf.openSession();sqlSession.insert("SQL 语句的配置文件中对应的 id" ); sqlSession.commit();
2.3 引入日志框架 logback
引入日志框架可以看清楚 MyBatis 执行的具体 sql
在 mybatis 配置文件 <configuration> 中添加 <settings>
STDOUT_LOGGING 是标准日志,MyBatis 已经实现了这种标准日志 1 2 3 4 5 <configuration > <settings > <setting name ="logImpl" value ="STDOUT_LOGGING" > </setting > </settings > </configuration >
我们还可以使用其它日志,例如 SLF4J:
核心配置文件中添加 <settings>
1 2 3 <settings > <setting name ="logImpl" value ="SLF4J" /> </settings >
引入 logback 依赖,这个日志框架实现了 slf4j 规范
1 2 3 4 5 6 <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > 1.2.11</version > <scope > test</scope > </dependency >
引入 logback 配置文件 ① 该文件必须放在类的根路径下,即 src/main/resources 目录下 ② 配置文件名必须为 logback-test 或 logback
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?xml version="1.0" encoding="UTF-8" ?> <configuration scan ="false" > <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder class ="ch.qos.logback.classic.encoder.PatternLayoutEncoder" > <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern > </encoder > </appender > <logger name ="com.apache.ibatis" level ="TRACE" /> <logger name ="java.sql.Connection" level ="DEBUG" /> <logger name ="java.sql.Statement" level ="DEBUG" /> <logger name ="java.sql.PreparedStatement" level ="DEBUG" /> <root level ="DEBUG" > <appender-ref ref ="STDOUT" /> <appender-ref ref ="FILE" /> </root > </configuration >
2.4 完整程序 目录结构 1 2 3 4 5 6 7 8 9 10 11 12 13 Root |--- src |--- main |--- java |--- test |--- FirstMybatis |--- resources |--- logback-test.xml |--- mybatis-config.xml |--- StudentMapper.xml |--- test |--- ... |--- pom.xml
代码 FirstMybatis:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class FirstMybatis { public static void main (String[] args) { SqlSession sqlSession = null ; try { SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder (); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml" )); sqlSession = sqlSessionFactory.openSession(); int count = sqlSession.insert("insertStu" ); System.out.println(count); sqlSession.commit(); } catch (IOException e) { if (sqlSession != null ) { sqlSession.rollback(); } } finally { if (sqlSession != null ) { sqlSession.close(); } } } }
logback-test.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?xml version="1.0" encoding="UTF-8" ?> <configuration scan ="false" > <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder class ="ch.qos.logback.classic.encoder.PatternLayoutEncoder" > <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern > </encoder > </appender > <logger name ="com.apache.ibatis" level ="TRACE" /> <logger name ="java.sql.Connection" level ="DEBUG" /> <logger name ="java.sql.Statement" level ="DEBUG" /> <logger name ="java.sql.PreparedStatement" level ="DEBUG" /> <root level ="DEBUG" > <appender-ref ref ="STDOUT" /> <appender-ref ref ="FILE" /> </root > </configuration >
mybatis-config.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <properties resource ="jdbc.properties" /> <settings > <setting name ="logImpl" value ="SLF4J" /> </settings > <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <dataSource type ="POOLED" > <property name ="driver" value ="${jdbc.driver}" /> <property name ="url" value ="${jdbc.url}" /> <property name ="username" value ="${jdbc.user}" /> <property name ="password" value ="${jdbc.password}" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="StudentMapper.xml" /> </mappers > </configuration >
StudentMapper.xml:
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="org.mybatis.example.BlogMapper" > <insert id ="insertStu" > insert into t_student(sno, sname, ssex) VALUES (null, 'jacky', 'man'); </insert > </mapper >
2.5 封装 MyBatis 工具类 封装前:
1 2 3 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder ();SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml" ));sqlSession = sqlSessionFactory.openSession();
通过观察发现,每次 openSession 时都很繁琐,我们可以封装工具类,简化开发 封装如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class SqlSessionUtil { private static SqlSessionFactory sqlSessionFactory; static { try { sqlSessionFactory = new SqlSessionFactoryBuilder ().build(Resources.getResourceAsStream("mybatis-config.xml" )); } catch (IOException e) { throw new RuntimeException (e); } } private SqlSessionUtil () { } public static SqlSession openSession () { return sqlSessionFactory.openSession(); } }
封装后:
1 SqlSession sqlSession= SqlSessionUtil.openSession();
了解:MyBatis 核心对象作用域 SqlSessionFactoryBuilder 这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession 每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。下面的示例就是一个确保 SqlSession 关闭的标准模式:
1 2 3 try (SqlSession session = sqlSessionFactory.openSession()) { }
三、使用 MyBatis 完成 CRUD
JDBC 代码中占位符采用的是 ? MyBatis 中占位符是 #{ }
3.1 insert(Create) Mapper 中使用 #{ } 占位,不将 sql 写死
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="org.mybatis.example.BlogMapper" > <insert id ="insertStu" > insert into t_student(sno, sname, ssex) VALUES (null, #{sname}, #{ssex}); </insert > </mapper >
使用 Map 集合,动态传参。map 集合的 key 对应 Mapper.xml 中的 #{ }(底层调用了 map.get(key))
1 2 3 4 5 6 7 8 9 10 11 12 13 public class StuMapperTest { @Test public void testInsert () { Map<String, Object> map = new HashMap <>(); map.put("sname" , "mike" ); map.put("ssex" , "man" ); SqlSession sqlSession = SqlSessionUtil.openSession(); sqlSession.insert("insertStu" , map); sqlSession.commit(); sqlSession.close(); } }
也可以使用 bean 类完成传参:将 Mapper 的 #{ } 中修改为属性名即可(底层调用了 bean 类的 get 方法,严格来说:#{ } 中传的参数是 get 方法名去掉 get,剩下的全小写)
3.2 delete(Delete) StuMapper.xml
1 2 3 <delete id ="deleteStu" > delete from t_student where sno = #{sno}; </delete >
测试删除方法
1 2 3 4 5 6 7 @Test public void testDelete () { SqlSession sqlSession = SqlSessionUtil.openSession(); sqlSession.delete("deleteStu" , 11 ); sqlSession.commit(); sqlSession.close(); }
3.3 update(Update) StuMapper.xml
1 2 3 <update id ="updateStu" > update t_student set sno = #{sno}, sname = #{sname}, ssex = #{ssex} where sno = #{sno}; </update >
测试修改方法
1 2 3 4 5 6 7 8 @Test public void testUpdate () { SqlSession sqlSession = SqlSessionUtil.openSession(); Student student = new Student (10L , "Alice" ,"woman" ); sqlSession.update("updateStu" , student); sqlSession.commit(); sqlSession.close(); }
3.4 select(Retrieve) StuMapper.xml
注意:查询时必须写 resultType 属性,不然 mybatis 无法确定返回值类型,会报错
1 2 3 4 5 6 7 8 9 <select id ="selectStu" resultType ="com.shameyang.mybatis.bean.Student" > select * from t_student where sno = #{sno}; </select > <select id ="selectAll" resultType ="com.shameyang.mybatis.bean.Student" > select * from t_student; </select >
测试查询一条记录
1 2 3 4 5 6 @Test public void testSelect () { SqlSession sqlSession = SqlSessionUtil.openSession(); sqlSession.selectOne("selectStu" , 1 ); sqlSession.close(); }
测试查询多条记录
1 2 3 4 5 6 7 @Test public void testSelectAll () { SqlSession sqlSession = SqlSessionUtil.openSession(); List<Student> students = sqlSession.selectList("selectAll" ); students.forEach(System.out::println); sqlSession.close(); }
3.5 SQL Mapper 的 namespace
SQL Mapper 配置文件中,<mapper> 标签的 namespace 属性翻译为命名空间,主要是为了防止 sqlId 冲突
xml 文件中:
1 2 3 4 5 6 7 <mapper namespace ="命名空间" > <insert id ="insertStu" > insert into t_student(sno, sname, ssex) VALUES (null, #{sname}, #{ssex}); </insert > ... </mapper >
Java 程序中:
1 sqlSession.insert("命名空间.insertStu" );
四、MyBatis 核心配置文件解析 mybatis-config.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <properties resource ="jdbc.properties" /> <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC|MANAGED" /> <dataSource type ="UNPOOLED|POOLED|JNDI" > <property name ="driver" value ="{jdbc.driver}" /> <property name ="url" value ="${jdbc.url}" /> <property name ="username" value ="${jdbc.user}" /> <property name ="password" value ="${jdbc.password}" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="StudentMapper.xml" /> </mappers > </configuration >
五、WEB 应用中使用 MyBatis(MVC 架构模式 + 三层架构) 5.1 需求分析 银行转账业务,需要转出账户、转入账户、转账金额
5.2 数据库表的设计和数据准备
5.3 实现步骤 第一步:环境搭建 IDEA 中创建 Maven WEB 应用,配置 Tomcat,然后应用部署到 tomcat
手动添加 java 目录
web.xml 文件的版本比较低,可以从 tomcat 10的样例文件中复制,然后修改
1 2 3 4 5 6 7 8 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns ="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" version ="6.0" metadata-complete ="false" > </web-app >
删除 index.jsp 文件,我们这个项目只使用 html
配置 pom.xml 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.shameyanng</groupId > <artifactId > mybatis-003-web</artifactId > <packaging > war</packaging > <version > 1.0-SNAPSHOT</version > <name > mybatis-003-web</name > <url > http://localhost:8080/bank</url > <dependencies > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.13</version > </dependency > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > <version > 8.0.33</version > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > 1.4.11</version > </dependency > <dependency > <groupId > jakarta.servlet</groupId > <artifactId > jakarta.servlet-api</artifactId > <version > 6.0.0</version > <scope > provided</scope > </dependency > </dependencies > <build > <finalName > mybatis-003-web</finalName > </build > </project >
引入相关配置文件,放到 resources 目录下(类的根路径)
mybatis-config.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > <properties resource ="jdbc.properties" /> <settings > <setting name ="logImpl" value ="SLF4J" /> </settings > <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <dataSource type ="POOLED" > <property name ="driver" value ="${jdbc.driver}" /> <property name ="url" value ="${jdbc.url}" /> <property name ="username" value ="${jdbc.username}" /> <property name ="password" value ="${jdbc.password}" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="AccountMapper.xml" /> </mappers > </configuration >
AccountMapper.xml
1 2 3 4 5 6 7 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="account" > </mapper >
logback.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8" ?> <configuration scan ="false" > <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder class ="ch.qos.logback.classic.encoder.PatternLayoutEncoder" > <pattern > [%thread] %-5level %logger{36} - %msg%n</pattern > </encoder > </appender > <logger name ="com.apache.ibatis" level ="TRACE" /> <logger name ="java.sql.Connection" level ="DEBUG" /> <logger name ="java.sql.Statement" level ="DEBUG" /> <logger name ="java.sql.PreparedStatement" level ="DEBUG" /> <root level ="DEBUG" > <appender-ref ref ="STDOUT" /> <appender-ref ref ="FILE" /> </root > </configuration >
jdbc.properties
1 2 3 4 jdbc.driver =com.mysql.cj.jdbc.Driver jdbc.url =jdbc:mysql://localhost:3306/formybatis jdbc.username =root jdbc.password =123456
第二步:前端页面 index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 银行账户转账</title > </head > <body > <form action ="/bank/transfer" method ="post" > 转出账户:<input type ="text" name ="fromActno" /> <br > 转入账户:<input type ="text" name ="toActno" /> <br > 转账金额:<input type ="text" name ="money" /> <br > <input type ="submit" value ="转账" /> </form > </body > </html >
第三步:MVC 架构 + 三层架构分层 在 src/main/java 包下创建 bean 包、service 包、dao 包、web.controller 包、utils 包
1 2 3 4 5 6 7 8 9 10 11 src.main |---java |---com.shameyang.bank |---bean |---dao |---impl |---service |---impl |---utils |---web.controller |---exception
第四步:封装 SqlSession 工具类 拷贝之前封装过的 SqlSessionUtil 即可,放到 utils 包下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class SqlSessionUtil { private static SqlSessionFactory sqlSessionFactory; static { try { sqlSessionFactory = new SqlSessionFactoryBuilder ().build(Resources.getResourceAsStream("mybatis-config.xml" )); } catch (IOException e) { throw new RuntimeException (e); } } private SqlSessionUtil () { } public static SqlSession openSession () { return sqlSessionFactory.openSession(); } }
第五步:定义 JavaBean 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class Account { private Long id; private String actno; private Double balance; public Account (Long id, String actno, Double balance) { this .id = id; this .actno = actno; this .balance = balance; } public Account () { } @Override public String toString () { return "Account{" + "id=" + id + ", actno='" + actno + '\'' + ", balance=" + balance + '}' ; } public Long getId () { return id; } public void setId (Long id) { this .id = id; } public String getActno () { return actno; } public void setActno (String actno) { this .actno = actno; } public Double getBalance () { return balance; } public void setBalance (Double balance) { this .balance = balance; } }
第六步:DAO 层(Model 层) dao 中需要提供的方法:
转帐前查询余额是否充足:selectByActno
转账时需要更新账户:update
AccountDao
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public interface AccountDao { Account selectByActno (String actno) ; int update (Account act) ; }
AccountDaoImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class AccountDaoImpl implements AccountDao { @Override public Account selectByActno (String actno) { SqlSession sqlSession = SqlSessionUtil.openSession(); Account act = (Account) sqlSession.selectOne("selectByActno" , actno); sqlSession.close(); return act; } @Override public int update (Account act) { SqlSession sqlSession = SqlSessionUtil.openSession(); int count = sqlSession.update("update" , act); sqlSession.commit(); sqlSession.close(); return count; } }
第七步:DAO 实现类映射到 AccountMapper 1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="account" > <select id ="selectByActno" resultType ="com.shameyang.bank.bean.Account" > select * from t_act where actno= #{actno}; </select > <update id ="update" > update t_act set balance = #{balance} where actno = #{actno}; </update > </mapper >
第八步:业务逻辑层(Model 层) 异常类
余额不足
1 2 3 4 5 6 7 8 public class MoneyNotEnoughException extends Exception { public MoneyNotEnoughException () { } public MoneyNotEnoughException (String message) { super (message); } }
应用异常
1 2 3 4 5 6 7 8 9 public class AppException extends Exception { public AppException () { super (); } public AppException (String message) { super (message); } }
AccountService:
1 2 3 4 5 6 7 8 9 10 11 12 public interface AccountService { void transger (String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException; }
AccountServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class AccountServiceImpl implements AccountService { AccountDao accountDao = new AccountDaoImpl (); @Override public void transfer (String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException { Account fromAct = accountDao.selectByActno(fromActno); if (fromAct.getBalance() < money) { throw new MoneyNotEnoughException ("对不起,您的余额不足" ); } try { Account toAct = accountDao.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(fromAct.getBalance() + money); accountDao.update(fromAct); accountDao.update(toAct); } catch (Exception e) { throw new AppException ("转账失败,未知原因!请联系管理员!" ); } } }
第九步:表示层(Controller 层) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @WebServlet("/transfer") public class AccountController extends HttpServlet { AccountService accountService = new AccountServiceImpl (); @Override protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html" ); PrintWriter out = response.getWriter(); String fromActno = request.getParameter("fromActno" ); String toActno = request.getParameter("toActno" ); double money = Double.parseDouble(request.getParameter("money" )); try { accountService.transfer(fromActno, toActno, money); out.print("<h1>转账成功!</h1>" ); } catch (MoneyNotEnoughException | AppException e) { out.print(e); } } }
最后一步:测试 启动服务器,打开浏览器,输入 pom.xml 文件中设置的地址:http://localhost:8080/bank
404 问题:检查 web.xml 文件中 metadata-complete=”true”,如果为 true,表示只支持配置文件,无法注解式开发,改成 false 即可,同时支持配置文件和注解
5.4 事务问题 在之前的转账业务中,更新了两个账户,我们需要保证同时成功或失败,这时需要事务机制 方法:在 transfer 方法开始执行时开启事务,直到两个更新都成功,再提交事务
做出如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class AccountServiceImpl implements AccountService { AccountDao accountDao = new AccountDaoImpl (); @Override public void transfer (String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException { Account fromAct = accountDao.selectByActno(fromActno); if (fromAct.getBalance() < money) { throw new MoneyNotEnoughException ("对不起,您的余额不足" ); } try { Account toAct = accountDao.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); SqlSession sqlSession = SqlSessionUtil.openSession(); accountDao.update(fromAct); String s = null ; s.toString(); accountDao.update(toAct); sqlSession.commit(); sqlSession.close(); } catch (Exception e) { throw new AppException ("转账失败,未知原因!请联系管理员!" ); } } }
我们再次执行程序后,会发现转账失败,但是转账金额消失了!
原因: service 和 dao 中使用的 SqlSession 对象不是同一个
解决方法:我们可以将 SqlSession 对象存放到 ThreadLocal 当中,保证 service 和 dao 的 SqlSession 对象是同一个
修改 SqlSessionUtil:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class SqlSessionUtil { private static SqlSessionFactory sqlSessionFactory; static { try { sqlSessionFactory = new SqlSessionFactoryBuilder ().build(Resources.getResourceAsStream("mybatis-config.xml" )); } catch (IOException e) { throw new RuntimeException (e); } } private SqlSessionUtil () { } private static ThreadLocal<SqlSession> local = new ThreadLocal <>(); public static SqlSession openSession () { SqlSession sqlSession = local.get(); if (sqlSession == null ) { sqlSession = sqlSessionFactory.openSession(); local.set(sqlSession); } return sqlSession; } public static void close (SqlSession sqlSession) { if (sqlSession != null ) { sqlSession.close(); } local.remove(); } }
删除 dao 实现类中所有方法的 commit() 和 close()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class AccountDaoImpl implements AccountDao { @Override public Account selectByActno (String actno) { SqlSession sqlSession = SqlSessionUtil.openSession(); Account act = (Account) sqlSession.selectOne("selectByActno" , actno); return act; } @Override public int update (Account act) { SqlSession sqlSession = SqlSessionUtil.openSession(); int count = sqlSession.update("update" , act); return count; } }
修改 service 实现类中的方法 只添加了 23 行中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class AccountServiceImpl implements AccountService { AccountDao accountDao = new AccountDaoImpl (); @Override public void transfer (String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException { Account fromAct = accountDao.selectByActno(fromActno); if (fromAct.getBalance() < money) { throw new MoneyNotEnoughException ("对不起,您的余额不足" ); } try { Account toAct = accountDao.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); SqlSession sqlSession = SqlSessionUtil.openSession(); accountDao.update(fromAct); String s = null ; s.toString(); accountDao.update(toAct); sqlSession.commit(); SqlSessionUtil.close(sqlSession); } catch (Exception e) { throw new AppException ("转账失败,未知原因!请联系管理员!" ); } } }
5.5 分析当前程序存在的问题 分析 AccountDaoImpl 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class AccountDaoImpl implements AccountDao { @Override public Account selectByActno (String actno) { SqlSession sqlSession = SqlSessionUtil.openSession(); Account act = (Account) sqlSession.selectOne("selectByActno" , actno); return act; } @Override public int update (Account act) { SqlSession sqlSession = SqlSessionUtil.openSession(); int count = sqlSession.update("update" , act); return count; } }
我们不难发现,这个 dao 实现类中的方法只有 openSession 和 执行 CRUD 操作,没有任何业务逻辑。后边我们会学习代理机制,就不用再写实现类了
六、使用 Javassist 生成类 pom.xml 引入依赖:
1 2 3 4 5 <dependency > <groupId > org.javassist</groupId > <artifactId > javassist</artifactId > <version > 3.29.2-GA</version > </dependency >
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class JavassistTest { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("com.shameyang.javassist.Test" ); CtMethod ctMethod = new CtMethod (CtClass.voidType, "execute" , new CtClass []{}, ctClass); ctMethod.setModifiers(Modifier.PUBLIC); ctMethod.setBody("{System.out.println(\"hello world\");}" ); ctClass.addMethod(ctMethod); Class<?> aClass = ctClass.toClass(); Object o = aClass.newInstance(); Method method = aClass.getDeclaredMethod("execute" ); method.invoke(o); } }
运行注意:需要加两个参数,否则会有异常
七、MyBatis 接口代理机制及使用
MyBatis 提供了接口代理机制,可以动态为我们生成 dao 接口的实现类(代理类:dao 接口的代理)
代理模式:在内存中生成 dao 接口的代理类,然后创建代理类的实例
前提:SqlMapper.xml 文件中
namespace 必须是 dao 接口的全限定名称
id 必须是 dao 接口中的方法名
获取 dao 接口代理类对象:
1 XxxDao xxxDao = (XxxDao) SqlSessionUtil.openSession().getMapper(XxxDao.class);
示例代码:
八、MyBatis 技巧 8.1 #{}和${} #{}:先编译 sql 语句,再给占位符传值,底层是 PreparedStatement 实现。可以防止sql注入,比较常用。
${}:先进行 sql 语句拼接,然后再编译 sql 语句,底层是 Statement 实现。存在 sql 注入现象。只有在需要进行 sql 语句关键字拼接的情况下才会用到。
8.2 别名机制 在 Mapper.xml 文件中,resultType 属性用来指定查询结果集的封装类型,这个名字太长,我们可以起别名
在 mybatis-config.xml
文件中使用 <typeAliases>
起别名(不区分大小写)
更多别名参考 MyBatis 官方文档
8.3 mappers SQL 映射文件的配置方式包括四种:
resource:从类路径中加载
url:从指定的全限定资源路径中加载
class:使用映射器接口实现类的全限定类名
package:将包内的映射器接口实现全部注册为映射器
resource 这种方式是从类路径中加载配置文件,所以这种方式要求 SQL 映射文件必须放在 resources 目录下或其子目录下
1 2 3 4 5 <mappers > <mapper resource ="org/mybatis/builder/AuthorMapper.xml" /> <mapper resource ="org/mybatis/builder/BlogMapper.xml" /> <mapper resource ="org/mybatis/builder/PostMapper.xml" /> </mappers >
url 这种方式使用了绝对路径的方式,这种配置对 SQL 映射文件存放的位置没有要求
1 2 3 4 5 <mappers > <mapper url ="file:///var/mappers/AuthorMapper.xml" /> <mapper url ="file:///var/mappers/BlogMapper.xml" /> <mapper url ="file:///var/mappers/PostMapper.xml" /> </mappers >
class 如果使用这种方式必须满足以下条件:
SQL 映射文件和 mapper 接口放在同一个目录下
1 2 3 4 5 6 7 src |---java |---com.shameyang.mybatis.mapper |---mapper 接口 |---resources |---com/shameyang/mybatis/mapper |---SQL 映射文件
SQL 映射文件名必须和 mapper 接口名一致
使用映射器接口实现类的全限定类名 例如:class="com.shameyang.mybatis.mapper.XxxMapper"
package 如果class较多,可以使用这种package的方式,但前提条件和 class 方式一样
1 2 3 4 <mappers > <package name ="com.powernode.mybatis.mapper" /> </mappers >
8.4 IDEA 配置文件模板 如图所示,配置文件模板后,我们可以快速创建 mybatis-config.xml 文件(其它文件同理,巨方便)
8.5 插入数据时获取自动生成的主键 XxxMapper 接口中
1 void insertUseGeneratedKeys (Xxx xxx) ;
XxxMapper.xml
1 2 3 4 5 6 7 <insert id ="insertUseGeneratedKeys" useGeneratedKeys ="true" keyProperty ="id" > ... </insert >
九、MyBatis 参数处理 9.1 单个简单类型参数 简单类型包括:
基本数据类型(byte、short、int、long、float、double、char)
包装类(Byte、Short、Integer、Long、Float、Double、Character)
String
java.util.Date
java.sql.Date
简单类型对于 MyBatis 来说都是可以自动识别类型的
SQL 映射文件中的配置比较完整的写法是:
1 2 3 <select id ="selectByName" resultType ="student" parameterType ="java.lang.String" > select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR} </select >
其中 sql 语句中的 javaType,jdbcType,以及 select 标签中的 parameterType 属性,都是用来帮助 MyBatis 进行类型确定的。不过,这些配置多数是可以省略的,因为 MyBatis 有强大的自动类型推断机制
9.2 Map 参数 这种方式是手动封装 Map 集合,将每个条件以键值对的形式存放到集合中。在使用的时候通过 #{key}
来取值
StudentMapper 接口:
1 List<Student> selectByParamMap (Map<String, Object> paramMap) ;
StudentMapper.xml:
1 2 3 <select id ="selectByParamMap" resultType ="student" > select * from t_student where name = #{nameKey} and age = #{ageKey} </select >
StudentMapperTest:
1 2 3 4 5 6 7 8 9 10 @Test public void testSelectByParamMap () { Map<String, Object> paramMap = new HashMap <>(); paramMap.put("nameKey" , "张三" ); paramMap.put("ageKey" , 20 ); List<Student> students = mapper.selectByParamMap(paramMap); students.forEach(student -> System.out.println(student)); }
9.3 实体类参数 StudentMapper 接口:
1 int insert (Student student) ;
StudentMapper.xml: #{} 里面写的是属性名字。这个属性名其本质上是:set/get 方法名去掉 set/get 之后的名字
1 2 3 <insert id ="insert" > insert into t_student values(null,#{name},#{age},#{height},#{birth},#{sex}) </insert >
StudentMapperTest:
1 2 3 4 5 6 7 8 9 10 11 @Test public void testInsert () { Student student = new Student (); student.setName("李四" ); student.setAge(30 ); student.setHeight(1.70 ); student.setSex('男' ); student.setBirth(new Date ()); int count = mapper.insert(student); SqlSessionUtil.openSession().commit(); }
9.4 多参数 StudentMapper 接口:
1 List<Student> selectByNameAndSex (String name, Character sex) ;
StudentMapper.xml:
1 2 3 <select id ="selectByNameAndSex" resultType ="student" > select * from t_student where name = #{name} and sex = #{sex} </select >
StudentMapperTest:
1 2 3 4 5 @Test public void testSelectByNameAndSex () { List<Student> students = mapper.selectByNameAndSex("张三" , '女' ); students.forEach(student -> System.out.println(student)); }
执行结果:
异常信息描述了:name 参数找不到,可用的参数包括[arg1, arg0, param1, param2]
修改 StudentMapper.xml 配置文件:尝试使用[arg1, arg0, param1, param2]去参数
1 2 3 <select id ="selectByNameAndSex" resultType ="student" > select * from t_student where name = #{arg0} and sex = #{arg1} </select >
运行结果:
再次尝试修改 StudentMapper.xml 文件
1 2 3 <select id ="selectByNameAndSex" resultType ="student" > select * from t_student where name = #{arg0} and sex = #{param2} </select >
通过测试可以看到:
arg0 是第一个参数
param1 是第一个参数
arg1 是第二个参数
param2 是第二个参数
实现原理:实际上在 MyBatis 底层会创建一个 Map 集合,以 arg0/param1为 key,以方法上的参数为 value
例如以下 MyBatis 部分源码:
1 2 3 4 5 6 7 8 Map<String,Object> map = new HashMap <>(); map.put("arg0" , name); map.put("arg1" , sex); map.put("param1" , name); map.put("param2" , sex);
注意:使用mybatis3.4.2之前的版本时:要用#{0}和#{1}这种形式
9.5 @Param 注解(命名参数) 使用 @Param 注解在多参数时,就不需要使用 arg 或 param 参数了,增强了可读性 原理:@Param(key) 中的 key 就是 Map 集合的 key
Mapper 接口中使用 @Param
:
1 List<Student> selectByNameAndAge (@Param("name") String name, @Param("age") int age) ;
Test:
1 2 3 4 5 @Test public void testSelectByNameAndAge () { List<Student> stus = mapper.selectByNameAndAge("张三" , 20 ); stus.forEach(student -> System.out.println(student)); }
Mapper.xml:
1 2 3 <select id ="selectByNameAndAge" resultType ="student" > select * from t_student where name = #{name} and age = #{age} </select >
十、MyBatis 查询专题 10.1 resultMap 结果映射 查询结果的列名和 Java 对象的属性名对应不上怎么办?
as 给列起别名
使用 resultMap 进行结果映射
是否开启驼峰命名自动映射(配置 settings)
使用 resultMap 进行结果映射
当属性名和数据库列名一致时,可以省略。但建议都写上
Mapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <resultMap id ="carResultMap" type ="car" > <id property ="id" column ="id" /> <result property ="carNum" column ="car_num" /> <result property ="brand" column ="brand" javaType ="string" jdbcType ="VARCHAR" /> <result property ="guidePrice" column ="guide_price" /> <result property ="produceTime" column ="produce_time" /> <result property ="carType" column ="car_type" /> </resultMap > <select id ="selectAllByResultMap" resultMap ="carResultMap" > select * from t_car </select >
是否开启驼峰命名自动映射 使用前提:属性名遵循 Java 的命名规范,数据库表的列名遵循 SQL 的命名规范
Java 命名规范:首字母小写,后面每个单词首字母大写,遵循驼峰命名方式
SQL 命名规范:全部小写,单词之间采用下划线分割
比如以下对应关系
实体类中的属性名
数据库表中的列名
stuNum
stu_num
stuName
stu_name
启用该功能: 配置 mybatis-config.xml:
1 2 3 <settings > <setting name ="mapUnderscoreToCamelCase" value ="true" /> </settings >
10.2 返回总记录条数
1 2 3 4 <select id ="selectTotal" resultType ="long" > select count(*) from t_car </select >
1 2 3 4 5 6 @Test public void testSelectTotal () { CarMapper carMapper = SqlSessionUtil.openSession().getMapper(CarMapper.class); Long total = carMapper.selectTotal(); System.out.println(total); }
十一、动态 SQL 顾名思义,动态 SQL 可以满足需要 SQL 语句拼接的业务场景,例如:批量删除、多条件查询
11.1 if 把 where 子句放到 if 标签中
1 2 3 4 5 6 <select id ="findActiveBlogWithTitleLike" resultType ="Blog" > SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test ="title != null" > AND title like #{title} </if > </select >
这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果
如果希望通过 “title” 和 “author” 两个参数进行可选搜索怎么办?只需要再加入一个条件即可
1 2 3 4 5 6 7 8 9 <select id ="findActiveBlogLike" resultType ="Blog" > SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test ="title != null" > AND title like #{title} </if > <if test ="author != null and author.name != null" > AND author_name like #{author.name} </if > </select >
11.2 choose、when、otherwise 有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="findActiveBlogLike" resultType ="Blog" > SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose > <when test ="title != null" > AND title like #{title} </when > <when test ="author != null and author.name != null" > AND author_name like #{author.name} </when > <otherwise > AND featured = 1 </otherwise > </choose > </select >
11.3 where、trim、set 使用 where 标签,就不要在 SQL 中写 where 关键字了。如果子句的开头是 and 或 or,会自动去除
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="findActiveBlogLike" resultType ="Blog" > SELECT * FROM BLOG <where > <if test ="state != null" > state = #{state} </if > <if test ="title != null" > AND title like #{title} </if > <if test ="author != null and author.name != null" > AND author_name like #{author.name} </if > </where > </select >
我们还可以通过 trim 标签来定制 where 元素的功能,例如与 where 元素等价的自定义 trim 元素如下:
1 2 3 4 5 6 7 8 9 <trim prefix ="WHERE" prefixOverrides ="AND |OR " > ... </trim >
set 标签用于动态更新语句
1 2 3 4 5 6 7 8 9 10 <update id ="updateAuthorIfNecessary" > update Author <set > <if test ="username != null" > username=#{username},</if > <if test ="password != null" > password=#{password},</if > <if test ="email != null" > email=#{email},</if > <if test ="bio != null" > bio=#{bio}</if > </set > where id=#{id} </update >
这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)
与 set 等价的 trim 元素如下:
1 2 3 <trim prefix ="SET" suffixOverrides ="," > ... </trim >
11.4 foreach 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="selectPostIn" resultType ="domain.blog.Post" > SELECT * FROM post p WHERE id in <foreach collection ="list" item ="item" index ="index" open ="(" separator ="," close =")" > #{item} </foreach > </select >
11.5 sql 标签和 include 标签 sql 标签用来声明 sql 片段
include 标签用来将声明的 sql 片段包含到某个 sql 语句中
作用:代码复用,易维护
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <sql id ="multiplexCode" > 复用的 sql 片段 </sql > <select id ="selectAllRetMap" resultType ="map" > select <include refid ="multiplexCode" /> from xxx </select > <select id ="selectAllRetListMap" resultType ="map" > select <include refid ="multiplexCode" /> xxx from xxx </select > <select id ="selectByIdRetMap" resultType ="map" > select <include refid ="multiplexCode" /> from xxx where ... </select >
十二、高级映射及延迟加载(多表查询) 在多表中,谁是主表,谁就是 JVM 中的主对象,例如学生班级表(多对一),学生表是主表,Student 对象就是主对象
12.1 多对一 常见的三种方式:
一条 SQL 语句,级联属性映射
一条 SQL 语句,association
两条 SQL 语句,分步查询(比较常用,优点是可复用,而且支持懒加载)
在主表对应的类中,添加关联的副表对象
1 2 3 4 5 6 7 8 public class Student { private Integer sid; private String sname; private Clazz clazz; }
第一种方式:级联属性映射 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.shameyang.mybatis.mapper.StudentMapper" > <resultMap id ="studentResultMap" type ="Student" > <id property ="sid" column ="sid" /> <result property ="sname" column ="sname" /> <result property ="clazz.cid" column ="cid" /> <result property ="clazz.cname" column ="cname" /> </resultMap > <select id ="selectBySid" resultMap ="studentResultMap" > select s.*, c.* from t_student s join t_clazz c on s.cid = c.cid where sid = #{sid} </select > </mapper >
第二种方式:association 其他位置不需要修改,只需要修改 resultMap 中的配置即可
1 2 3 4 5 6 7 8 9 <resultMap id ="studentResultMap" type ="Student" > <id property ="sid" column ="sid" /> <result property ="sname" column ="sname" /> <association property ="clazz" javaType ="Clazz" > <id property ="cid" column ="cid" /> <result property ="cname" column ="cname" /> </association > </resultMap >
第三种方式:分步查询 第一步:修改 resultMap 中 association 的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <resultMap id ="studentResultMap" type ="Student" > <id property ="sid" column ="sid" /> <result property ="sname" column ="sname" /> <association property ="clazz" select ="com.shameyang.mybatis.mapper.ClazzMapper.selectByCid" column ="cid" /> </resultMap > <select id ="selectBySid" resultMap ="studentResultMap" > select sid, sname, cid from t_student s where sid = #{sid} </select >
第二步:ClazzMapper 接口中添加方法(方法名对应 resultMap 中指定的)
1 2 3 public interface ClazzMapper { Clazz selectByCid (Integer cid) ; }
第三步:配置 ClazzMapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.shameyang.mybatis.mapper.ClazzMapper" > <select id ="selectByCid" resultType ="Clazz" > select cid, cname from t_clazz where cid = #{cid} </select > </mapper >
12.2 多对一延迟加载 延迟加载即暂时不访问的数据先不查询,提高程序的执行效率
延迟加载的实现很简单:在 association 标签中添加 fetchType=”lazy” 即可实现局部延迟加载
1 2 3 4 5 6 7 8 <resultMap id ="studentResultMap" type ="Student" > <id property ="sid" column ="sid" /> <result property ="sname" column ="sname" /> <association property ="clazz" select ="com.shameyang.mybatis.mapper.ClazzMapper.selectByCid" column ="cid" fetchType ="lazy" /> </resultMap >
只有当使用到 cid 时,才会执行关联的语句
上面的例子是局部延迟加载,我们还可以通过配置 mybatis-config.xml 进行全局设置
1 2 3 <settings > <setting name ="lazyLoadingEnabled" value ="true" /> </settings >
开启全局延迟加载后,如果不希望某个 sql 延迟加载,将 fetchType 设置为 eager 即可:
1 2 3 4 5 6 7 8 <resultMap id ="studentResultMap" type ="Student" > <id property ="sid" column ="sid" /> <result property ="sname" column ="sname" /> <association property ="clazz" select ="com.shameyang.mybatis.mapper.ClazzMapper.selectByCid" column ="cid" fetchType ="eager" /> </resultMap >
12.3 一对多 一对多,通常在一的一方中有 List 集合属性
1 2 3 4 5 6 7 8 public class Clazz { private Integer cid; private String cname; private List<Student> stus; }
常见的两种实现方式:
第一种方式:collection 1 2 3 4 5 6 7 8 9 10 11 12 13 <resultMap id ="clazzResultMap" type ="Clazz" > <id property ="cid" column ="cid" /> <result property ="cname" column ="cname" /> <collection property ="stus" ofType ="Student" > <id property ="sid" column ="sid" /> <result property ="sname" column ="sname" /> </collection > </resultMap > <select id ="selectClazzAndStusByCid" resultMap ="clazzResultMap" > select * from t_clazz c join t_student s on c.cid = s.cid where c.cid = #{cid} </select >
第二种方式:分步查询 与多对一同理,只是 association 换成了 collection
第一步:配置 collection 标签
1 2 3 4 5 6 7 8 9 10 11 12 13 <resultMap id ="clazzResultMap" type ="Clazz" > <id property ="cid" column ="cid" /> <result property ="cname" column ="cname" /> <collection property ="stus" select ="com.shameyang.mybatis.mapper.StudentMapper.selectByCid" column ="cid" /> </resultMap > <select id ="selectClazzAndStusByCid" resultMap ="clazzResultMap" > select * from t_clazz c where c.cid = #{cid} </select >
第二步:StudentMapper 接口中添加方法
1 List<Student> selectByCid (Integer cid) ;
第三步:配置 StudentMapper.xml
1 2 3 <select id ="selectByCid" resultType ="Student" > select * from t_student where cid = #{cid} </select >
12.4 一对多延迟加载 一对多的延迟加载,与多对一延迟加载同理
十三、MyBatis 的缓存 缓存:cache
缓存的作用:通过减少 IO 的方式,来提高程序的执行效率
MyBatis 的缓存:将 select 语句的查询结果放到缓存(内存)当中,下一次还是这条 select 语句的话,直接从缓存中取,不再查数据库。一方面是减少了 IO,另一方面不再执行繁琐的查找算法。效率大大提升
MyBatis 的缓存包括:
一级缓存:将查询到的数据存储到 SqlSession 中
二级缓存:将查询到的数据存储到 SqlSessionFactory 中
集成其它第三方的缓存:比如 EhCache【Java 语言开发的】、Memcache【C 语言开发的】等
缓存只针对于DQL语句,也就是说缓存机制只对应select语句。
13.1 一级缓存 一级缓存默认开启,不需要配置
原理:只要使用同一个 SqlSession 对象执行同一条 SQL 语句,就会走缓存
一级缓存失效的情况:
两次查询之间,手动清空了一级缓存 1 sqlSession.clearCache();
两次查询之间,执行了增删改操作,即数据发生改变
13.2 二级缓存 默认是开启二级缓存的
1 2 <setting name ="cacheEnabled" value ="true" >
使用二级缓存,需要在 SQL 的映射文件中添加一行:
注意:
二级缓存的相关配置:
eviction:指定从缓存中移除某个对象的淘汰算法。默认采用LRU策略
LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用)
FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰
SOFT:软引用。淘汰软引用指向的对象。具体算法和 JVM 的垃圾回收算法有关
WEAK:弱引用。淘汰弱引用指向的对象。具体算法和 JVM 的垃圾回收算法有关
flushInterval:
二级缓存的刷新时间间隔。单位毫秒。如果没有设置。就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据。除非执行了增删改
readOnly:
true:多条相同的 sql 语句执行之后返回的对象是共享的同一个。性能好。但是多线程并发可能会存在安全问题
false:多条相同的 sql 语句执行之后返回的对象是副本,调用了clone 方法。性能一般。但安全
size:
设置二级缓存中最多可存储的 java 对象数量。默认值1024
13.3 MyBatis 集成 EhCache 我们可以集成第三方缓存,这里以 EhCache 为例:修改 cache 标签的属性即可
第一步:引入 ehcache 依赖
1 2 3 4 5 6 <dependency > <groupId > org.mybatis.caches</groupId > <artifactId > mybatis-ehcache</artifactId > <version > 1.2.2</version > </dependency >
第二步:在类的根路径下新建 ehcache.xml 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?xml version="1.0" encoding="UTF-8" ?> <ehcache xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation ="http://ehcache.org/ehcache.xsd" updateCheck ="false" > <diskStore path ="e:/ehcache" /> <defaultCache eternal ="false" maxElementsInMemory ="1000" overflowToDisk ="false" diskPersistent ="false" timeToIdleSeconds ="0" timeToLiveSeconds ="600" memoryStoreEvictionPolicy ="LRU" /> </ehcache >
第三步:修改 SqlMapper.xml 文件中的 cache 标签,添加 type 属性
1 <cache type ="org.mybatis.caches.ehcache.EhcacheCache" />
十四、MyBatis 的逆向工程 逆向工程:根据数据库表逆向生成 Java 的 pojo 类,SqlMapper.xml 以及 Mapper 接口等
我们可以借助大佬写好的逆向工程插件,完成逆向工程
14.1 逆向工程配置与生成 第一步:基础环境配置
第二步:pom.xml 文件中添加逆向工程插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <build > <plugins > <plugin > <groupId > org.mybatis.generator</groupId > <artifactId > mybatis-generator-maven-plugin</artifactId > <version > 1.4.1</version > <configuration > <overwrite > true</overwrite > </configuration > <dependencies > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > <version > 8.0.33</version > </dependency > </dependencies > </plugin > </plugins > </build >
第三步:配置 generatorConfig.xml
文件名必须叫做:generatorConfig.xml
该文件必须放在类的根路径下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" > <generatorConfiguration > <context id ="DB2Tables" targetRuntime ="MyBatis3" > <plugin type ="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" /> <commentGenerator > <property name ="suppressDate" value ="true" /> <property name ="suppressAllComments" value ="true" /> </commentGenerator > <jdbcConnection driverClass ="com.mysql.cj.jdbc.Driver" connectionURL ="jdbc:mysql://localhost:3306/formybatis" userId ="root" password ="123456" > <property name ="nullCatalogMeansCurrent" value ="true" /> </jdbcConnection > <javaModelGenerator targetPackage ="com.shameyang.mybatis.pojo" targetProject ="src/main/java" > <property name ="enableSubPackages" value ="true" /> <property name ="trimStrings" value ="true" /> </javaModelGenerator > <sqlMapGenerator targetPackage ="com.shameyang.mybatis.mapper" targetProject ="src/main/resources" > <property name ="enableSubPackages" value ="true" /> </sqlMapGenerator > <javaClientGenerator type ="xmlMapper" targetPackage ="com.shameyang.mybatis.mapper" targetProject ="src/main/java" > <property name ="enableSubPackages" value ="true" /> </javaClientGenerator > <table tableName ="t_student" domainObjectName ="Student" /> </context > </generatorConfiguration >
14.2 测试逆向工程(使用 QBC 风格) QBC 风格:Query By Criteria,根据标准查询,即条件都是定义好的,只需要调用像对应的方法,就可以生成标准的条件 比较面向对象,看不到 sql 语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class GeneratorTest { @Test public void testSelectByPrimaryKey () { SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); StudentExample studentExample = new StudentExample (); studentExample.createCriteria() .andSnameEqualTo("Alice" ); studentExample.or().andSnoBetween(2 , 5 ); List<Student> students = mapper.selectByExample(studentExample); System.out.println(students); sqlSession.commit(); sqlSession.close(); } }
十五、MyBatis 使用 PageHelper 15.1 回顾 limit 分页 MySQL 的 limit 后面两个数字:
第一个数字:startIndex(起始下标,下标从0开始)
第二个数字:pageSize(每页显示的记录条数)
已知页码 pageNum,每页显示的记录条数 pageSize
startIndex = (pageNum - 1) * pageSize
标准通用的 MySQL 分页:
1 2 3 4 5 6 select * from tableName limit (pageNum - 1 ) * pageSize, pageSize
获取数据不难,但是获取分页信息(例如总页数)比较难,因此我们可以借助 PageHelper 插件
15.2 使用 PageHelper PageHelper 是 MyBatis 的一个插件,其作用是更加方便地进行分页查询
第一步:pom.xml 引入依赖
1 2 3 4 5 <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper</artifactId > <version > 5.3.3</version > </dependency >
第二步:在 mybatis-config.xml 文件中配置插件
1 2 3 <plugins > <plugin interceptor ="com.github.pagehelper.PageHelper" /> </plugins >
第三步:编写 Java 程序
查询语句之前开启分页功能
查询语句之后封装 PageInfo 对象(PageInfo 对象将来会存储到 request 域当中,在页面上展示)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void testSelectAll () { SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); int pageNum = 1 ; int pageSize = 3 ; PageHelper.startPage(pageNum, pageSize); List<Student> students = mapper.selectAll(); PageInfo<Student> pageInfo = new PageInfo <>(students, 5 ); System.out.println(pageInfo); sqlSession.commit(); sqlSession.close(); }
执行结果: PageInfo{pageNum=1, pageSize=3, size=3, startRow=1, endRow=3, total=5, pages=2, list=Page{count=true, pageNum=1, pageSize=3, startRow=0, endRow=3, total=5, pages=2, reasonable=false, pageSizeZero=false}[Student{sno=1, sname=’tom’, ssex=’man’}, Student{sno=2, sname=’jacky’, ssex=’man’}, Student{sno=8, sname=’jacky’, ssex=’man’}], prePage=0, nextPage=2, isFirstPage=true, isLastPage=false, hasPreviousPage=false, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=2, navigatepageNums=[1, 2]}
执行结果格式化:
1 2 3 4 5 6 7 8 PageInfo{ pageNum=1, pageSize=3, size=3, startRow=1, endRow=3, total=5, pages=2, list=Page{count=true, pageNum=1, pageSize=3, startRow=0, endRow=3, total=5, pages=2, reasonable=false, pageSizeZero=false} [Student{sno=1, sname='tom', ssex='man'}, Student{sno=2, sname='jacky', ssex='man'}, Student{sno=8, sname='jacky', ssex='man'}], prePage=0, nextPage=2, isFirstPage=true, isLastPage=false, hasPreviousPage=false, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=2, navigatepageNums=[1, 2] }
十六、MyBatis 的注解式开发 MyBatis 中也提供了注解式的开发方式,采用注解可以减少 SQL 映射文件的配置
使用注解式开发的话,SQL 语句是在 Java 程序中的,这种方式会给 SQL 语句的维护带来成本
官方这么说的:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句
所以,简单的 SQL 我们使用注解,复杂的 SQL 还是使用之前的 XML
16.1 @Insert 1 2 3 4 public interface StudentMapper { @Insert("insert into t_student(sno, sname, ssex) values(11, 'john', 'man')") int insert () ; }
16.2 @Delete 1 2 3 4 public interface StudentMapper { @Delete("delete from t_student where sno = #{sno}") int delete (Integer sno) ; }
16.3 @Update 1 2 3 4 public interface StudentMapper { @Update("update t_student set sname = #{sname} where sno = #{sno}") int update (@Param("sno") Integer sno, @Param("sname") String sname) ; }
16.4 @Select 1 2 3 4 5 6 7 8 9 public interface StudentMapper { @Select("select * from t_student where sno = #{sno}") @Results({ @Result(column = "sno", property = "sno", id = true), @Result(column = "sname", property = "sname"), @Result(column = "ssex", property = "ssex") }) Student selectBySno (Integer sno) ; }