一、MyBatis 简介
1、MyBatis 历史
-
MyBatis 最初是 Apache 的一个开源项目 iBatis,10 年由 Apache Software Foundation 迁移到 Google Code,iBatis3.x 正式更名为 MyBatis,13 年 11 月迁移到 GitHub
-
MyBatis 是一个基于 Java 的持久层框架。提供的持久层框架包括 SQL Maps 和 Data Access Object(DAO)
2、MyBatis 特性
- 支持定制化 SQL、存储过程以及高级映射(例如一对多)
- 避免了几乎所有 JDBC 代码和手动设置参数以及获取结果集
- 可以使用简单的 XML 或注解用于配置和原始映射、将接口和 Java 的 POJO(Plan Old Java Object,普通的 Java 对象)映射成数据库中的记录
- 是一个半自动 ORM(Object Relation Mapping(对象与关系型数据库之间的映射))框架
3、与其他持久层技术对比
- JDBC
- SQL 夹杂在 Java 代码中,耦合性太高,导致硬编码内伤(代码写死了)
- 维护不易,实际开发中 SQL 变化频繁都需要重新编译
- 代码冗长,开发效率低
- Hibernate 和 JPA
- 操作简便开发快
- 长难复杂 SQL 需要绕过框架
- 内部自动生成 SQL 不容易做特殊优化
- 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难(查询某几个字段可能需要自己写,默认功能会字段全查)
- 反射操作过多,数据库性能下降
- MyBatis
- 轻量级,高性能
- SQL 和 Java 编码分离,功能边界清晰,Java 代码专注业务,SQL 语句专注数据
- 开发效率稍逊于 Hibernate 但是完全可以接受
二、搭建 MyBatis
1、开发环境
- idea、maven、MySQL、MyBatis
2、创建 maven 工程
- 先创建空项目,然后创建模块 MyBatis_demo1 配置 pom.xml
<groupId>org.example</groupId>
<artifactId>Mybatis_demo1</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<build>
<finalName>SpringbootMybatis</finalName>
<plugins>
<!-- 我每次没有这个插件都报错 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Mybatis核心 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.3</version>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
3、创建 MyBatis 核心配置文件
- resource 下创建 mybatis-config.xml:
<?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">
<!--事务管理的类型是最原始的JDBC-->
<transactionManager type="JDBC"/>
<!--使用数据库连接池,会对当前数据连接进行保存,下次就可以直接从缓存取-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!--引入映射文件,等你创建了你的映射文件再来这修改成你的-->
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
4、创建 Mapper 接口
-
Mapper 接口主要用来写操作数据库数据的方法
-
创建个 t_user 表:
-
DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(20) NULL DEFAULT NULL, `password` varchar(20) NULL DEFAULT NULL, `age` int(11) NULL DEFAULT NULL, `sex` char(1) NULL DEFAULT NULL, `email` varchar(20) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE )
-
对应创建个实体类,在 src/main/java 下新建个 com.mybatis.pojo 包存放 User 类
-
@Data @AllArgsConstructor public class User { private Integer id; private String ysername; private String paassword; private Integer age; private String sex; private String email; }
-
相应的,pojo 同级目录创建 mapper 包,其中创建 UserMapper 接口
-
public interface UserMapper { /** * 添加用户信息 * @return 添加结果 */ int insertUser(); }
-
每个 mapper 接口对应一个操作数据库的 xml 映射文件,以下为 UserMapper.xml
-
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!--namespace 与 mapper 接口的全类名保持一致--> <mapper namespace="com.mybatis.mapper.UserMapper"> <!--映射文件中 SQL 的 id 与 mapper 接口的方法名也就是 insertUser() 一致;--> <insert id="insertUser"> insert into t_user values(null,'张三','123',23,'女','123@qq.com') </insert> </mapper>
-
最后在 mybatis 的配置文件引入你写的映射文件也就是上面的:
-
<!--引入映射文件,等你创建了你的映射文件再来这修改成你的--> <mappers> <mapper resource="mappers/UserMapper.xml"/> </mappers>
-
此时数据库的 t_user表对应 User 实体类,User 实体类有它的数据操作接口 UserMapper,SQL 语句不可能写在 java 文件中,所以有映射文件 UserMapper.xml,此时就有:数据库表->实体类->接口->接口映射文件,打通了与数据库的连接。
-
此处我的包名命名并不规范,应该为com.企业名.项目名.模块名,比如导入 MyBatis 某个类时你会是
import org(非盈利组织).apache(apache公司).ibatis(ibatis项目).io(输入输出流功能模块).*
三、MyBatis 的增删改查
测试添加功能
- Mybatis 中提供了操作数据库的会话对象叫做 SQLSession,所以首先来获取它
-
那么第一步加载配置文件,通过 Resources 类的静态方法 getResourceAsStream 来读取配置文件获取对应的字节输入流,还有其他的以文件形式之类的来获取,那么此时根据配置类文件名来获取
InputStream is = Resources.getResourceAsStream("mybatis-config.xml")
-
然后获取SqlSessionFactoryBuilder对象,这是提供 sqlSession 工厂对象的构建对象
-
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder()
-
Session:代表 java 与数据库之间的会话,例如 HttpSession 就是 java 和浏览器之间的会话
-
SqlSessionFactory:生产 SqlSession 的工厂
-
-
然后获取 SqlSessionFactory,通过 SqlSessionFactory 的构建对象来生产 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is)
-
然后获取 SqlSession,通过Session工厂对象也就是 SqlSessionFactory 来打开 SqlSession
SqlSession SqlSession = sqlSessionFactory.openSession();
-
然后获取接口对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class)
-
测试一下我们之前在 UserMapper.xml 中写的 Sql 语句的添加功能(方法底层使用了代理模式)
int result = mapper.insertUser()
-
由于我们在配置文件中写了事物管理方式是最原始的 JDBC 方式,所以需要手动提交或回滚事务,在这里使用 SqlSession 提交事务
sqlSession.commit()
-
如果插入的中文数据乱码了,那么修改数据库配置的 url 的编码方式,即将 mybatis-config.xml 中的 url 修改为:
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull"/>
- ‘&’ 为转义字符,想表示 ‘&’ 的
-
上述添加测试功能优化
-
SqlSession默认不自动提交事务,若需自动提交可以
SqlSessionFactory.openSession(true)
-
添加日志管理,输出事务中的 sql 语句:
-
添加 log4j 依赖:
-
<!-- log4j日志 --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
-
resource 下加入 log4j.xml(log4j 配置文件)
-
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender"> <param name="Encoding" value="UTF-8" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \n" /> </layout> </appender> <logger name="java.sql"> <level value="debug" /> </logger> <logger name="org.apache.ibatis"> <level value="info" /> </logger> <root> <level value="debug" /> <appender-ref ref="STDOUT" /> </root> </log4j:configuration>
-
日志的级别:FATAL(致命)>ERROR(错误)>WARN(警告)>INFO(信息)>DEBUG(调试)
-
从左到右打印的内容越来越详细
-
-
查询功能
-
找到 sql 语句的依据是根据(全名类 + 方法名)或者说 (命名空间 + sqlid)
-
但是如果像之前的增删改一样的步骤,先在 mapper 文件写方法,然后在映射文件 mapper.xml 写 sql 如下,这样是会报错的
-
<select id="selectUserById"> select * from t_user where id = 4 </select>
-
为什么?因为增删改操作的返回值可以是受影响行数,但是查询呢?你要对应实体对象,对应实体对象列表,亦或是某个字段的值?人家怎么知道你要什么,所以你需要设置查询结果,resultType 或者 resultMap。
-
修改上面的 sql 映射部分
-
<select id="selectUserById" resultType="com.mybatis.pojo.User"> select * from t_user where id = 4 </select>
-
resultType:默认映射类型,就是查询到的结果,根据字段名和属性名的对应关系,将字段的值赋给映射类型的属性,也就是表字段名和对象属性名尽量保持一致
-
resultMap:自定义映射:字段名和属性名不一致、多对一、一对多时就需要用这个了
四、核心配置文件详解
enviroments
- 从这个复数形式就可以看是可以包含多个环境的,所以是用来配置多个连接数据库的环境的、
- 属性:
- default:默认数据库环境,例如
<enviroments default="env1">
- default:默认数据库环境,例如
enviroment
-
属性:
- id:表示连接数据库环境的唯一标识,不能重复,例如
<enviroment id="env1">
- id:表示连接数据库环境的唯一标识,不能重复,例如
-
<transactionManager>:设置事务管理方式
- type:事务管理方式,例如 JDBC|MANAGED
- JDBC:当前环境中执行 SQL 时使用 JDBC 原生的事务管理方式,也就是事务的提交或回滚需要手动处理
- MANAGED:被管理,例如 Spring
- type:事务管理方式,例如 JDBC|MANAGED
-
<datasource>:数据源配置,Spring 整合后其实都不需要配置数据源了,Spring 会提供
- type:数据源类型,有 UNPOOLED|POLLED|JNDI 三种
- UNPOOLED:
- 这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。UNPOOLED 类型的数据源仅仅需要配置以下 5 种属性:
- driver – 这是 JDBC 驱动的 Java 类全限定名,也就是数据库驱动名(并不是 JDBC 驱动中可能包含的数据源类)。
- url – 这是数据库的 JDBC URL 地址,也就是数据库连接地址。
- username – 连接数据库的用户名。
- password – 连接数据库的密码。
- defaultTransactionIsolationLevel – 默认的连接事务隔离级别。
- defaultNetworkTimeout – 等待数据库操作完成的默认网络超时时间(单位:毫秒)。
- POLLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:
- poolMaximumActiveConnections – 在任意时间可存在的活动(正在使用)连接数量,默认值:10
- poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
- poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
- poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
- poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与poolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
- poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。
- poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
- poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。
- JNDI:使用上下文中的数据源,这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:
- initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
- data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
- type:数据源类型,有 UNPOOLED|POLLED|JNDI 三种
properties
-
上面的数据库配置属性都写死了,但是其实我们是会写在 properties 中
-
resource 下创建 jdbc.properties
-
driver=com.mysql.jdbc.Driver //这里注意将 '&' 重新改为 '&' url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull username=root password=123456
-
但是这样键值对的形式,比如说 password,可能会重名别人怎么知道访哪个配置个文件的哪个 username,所以我们根据文件名加个前缀,这样就规范多了
-
jdbc.driver=com.mysql.jdbc.Driver" jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull jdbc.username=root jdbc.password=123456
-
那么 mybatis 的配置文件怎么访问你写的 properties 文件呢,当然是引入了
-
<?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"> ... </environments> <!--引入映射文件,等你创建了你的映射文件再来这修改成你的--> <mappers> ...... </mappers> </configuration>
-
再用 ${} 的形式来读取 properties 中的值
-
<?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"> <!--事务管理的类型是最原始的JDBC--> <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="mappers/UserMapper.xml"/> </mappers> </configuration>
typeAliases
-
你每个查询的方法的结果集都要写全类名的话,确实很累,所以你可以为结果类型起别名
-
修改 mybatis-config.xml
-
<?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" /> <!--设置类型别名--> <typeAliases> <typeAlias type="com.mybatis.pojo.User" alias="User"></typeAlias> </typeAliases> <!--设置连接数据库的环境--> <environments default="development"/> <!--引入映射文件,等你创建了你的映射文件再来这修改成你的--> <mappers/> </configuration>
-
我们习惯性把新加的标签放在最下面或最上面为什么我把 typeAliases 放在了中间呢,当我放在最上面时报错了:
-
The content of element type “configuration” must match “(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?)”.
-
什么意思?configuration 中的内容必须按照它括号内的顺序来填写,没有则跳过。对应到这里就是properties 之后才能放 typeAliases
-
此时把 UserMapper.xml 中的查询结果修改依旧能正常查询
-
<select id="getAllUser" resultType="User"> select * from t_user </select>
-
这里大小写的 User 都能找到,你写成 UsEr 都没事,小细节吧算是
-
而且 typeAlias 中的 type 必写,但是 alias 非必写,写了就只能按照你起的别名来找,而不写则默认根据类名来设置别名也就是和你设置的 User 相同,这里同样都不区分大小写
-
但是你是否会感觉还是麻烦,还得以类为单位设置别名,那么你可以以包为单位设置别名
-
<typeAliases> <!--<typeAlias type="com.mybatis.pojo.User" alias="User"></typeAlias>--> <!--以包为单位,将包下所有类型设置默认的类型别名--> <package name="com.mybatis.pojo"></package> </typeAliases>
Mapper
-
我们知道一张表对应一个实体类,一个实体类对应一个 Mapper ,一个 Mapper 对应一个 xml 映射文件,那么映射文件多的时候,MyBatis 配置文件岂不是要引入多次映射文件,所以你可以在 mybatis-config.xml 直接引入 映射文件所在包
-
<mappers> <package name="com.mybatis.mapper"> </mappers>
-
但是以包为单位引入映射文件需要符合两个条件:
- Mapper 接口所在包与映射文件所在包要一致,例如我的 Mapper 接口文件在 com.mybatis.mapper,我应该在 resource 下创建同名目录来填给 name,创建时写com/mybatis/mapper 因为你不是在包下创建二而是创建目录
- mapper 接口要和映射文件名字一致
五、MyBatis 获取参数值的两种方法
两种方式:${} 和 ${}
-
先新建一个模块 MyBatis_demo2
-
那么还是一样的步骤:pom.xml 导入依赖 -> 新建 mybatis-config.xml
-
那么其实每次都是一样的操作,所以你可以配置 mybatis-config.xml 模板,省得每次都复制粘贴
-
Settings–>Editor–>File and Code Templates:File 中新增 name 为 mybatis-config,Extension 为 xml 内容如下的模板文件
-
<?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"/> <typeAliases> <package name=""/> </typeAliases> <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> <package name=""/> </mappers> </configuration>
-
我们再创建 Mapper 接口,映射文件,映射文件也搞个代码模板吧
-
<?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=""> </mapper>
-
然后编写测试类,这时我们发现,我又要重复获取 SqlSession 然后获取 mapper 调用方法的操作,那就封装成自己的工作类吧:
-
新建 pojo 同级目录 utils,创建文件 SqlSessionUtils.java
-
public class SqlSessionUtils { public static SqlSession getSqlSession(){ SqlSession sqlSession = null; //这里直接 try/catch 省的到时候调用的地方还要处理异常 try { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); sqlSession = sqlSessionFactory.openSession(true); } catch (IOException e) { e.printStackTrace(); } return sqlSession; } }
-
测试类如下:
-
public class ParameterMapperTest { @Test public void testGetAllUser(){ SqlSession sqlSession = SqlSessionUtils.getSqlSession(); ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class); List<User> list = mapper.getAllUser(); list.forEach(n-> System.out.println(n)); } }
-
测试有警告,把 log4j 配置文件复制过来就 ok
个人尝试
-
尝试直接返回 mapper 接口:
-
public class SqlSessionUtils { public static <T> T getMapper(Class mapperClass){ T mapper = null; //这里直接 try/catch 省的到时候调用的地方还要处理异常 try { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); SqlSession sqlSession = sqlSessionFactory.openSession(true); mapper = (T) sqlSession.getMapper(mapperClass); } catch (IOException e) { e.printStackTrace(); } return mapper; } }
-
测试类:
-
public class ParameterMapperTest { @Test public void testGetAllUser(){ ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class); List<User> list = mapper.getAllUser(); list.forEach(n-> System.out.println(n)); } }
正式进入主题
- 我们之前的 sql 语句在映射文件中都是写死了,为了测试功能而已。那么现实开发中肯定是 mapper 接口中的方法会有传参,对应到映射文件又该如何获取?
先在测试类中回顾一下 JDBC 中的获取参数的方式
-
字符串拼接的方式:
-
@Test public void testJDBC() throws Exception { String userName = "admin"; Class.forName(""); Connection connection = DriverManager.getConnection("","",""); PreparedStatement ps = connection.prepareStatement("select * from t_user where username='"+userName+"'"); }
-
我们可以看到这样不仅拼接起来很麻烦,你得手动加单引号,还容易 SQL 注入
-
简单插一嘴 SQL 注入
-
例如你的接口中调用登录方法时拼接的 SQL 为:
-
String sql = "select * from user_table where username='" + userName + "' and password='"+ password +"'";
-
如果我们传入的 username 为:
' or 1 = 1 --
,sql 语句就变成了: -
select * from user_table where username=’ ’ or 1 = 1 – ‘and password=’ ’
-
--
意味着注释,他后面的语句就被注释了 -
所以你的 sql 实际上是:
-
select * from user_table where username=’ ’ or 1 = 1
-
这意味着你的登录时对用户名和密码的校验没有任何作用,谁都能登录
-
如果换个更加狠的传个删除库表的 sql:
-
select * from user_table where username=’ ’ ;DROP DATABASE (DB Name) --’ and password=’ ’
-
那你等着被开除吧
-
-
所以一般用另一种方式,占位符:
-
@Test public void testJDBC() throws Exception { String userName = "admin"; Class.forName(""); Connection connection = DriverManager.getConnection("","",""); PreparedStatement ps = connection.prepareStatement("select * from t_user where username = ?"); ps.setString(1,userName);//字符类型所以用 setString,将第 1 个占位符的内容替换为 username }
-
这种方式就无需手动写单引号,也不用担心 SQL 注入
此时来看 MyBatis 中获取参数的方式:
-
${ }:本质就是字符串拼接
-
#{ }:本质就是占位符赋值
-
那么我们肯定能用 # 就尽量不用 $,因为便捷且不用担心 SQL 注入,但是有些 SQL 必须使用 $
-
那我可能传入一个字面量,多个字面量,集合…,所以现在来考虑一下不同参数的情况下如何获取参数
- 传参为单个字面量:例如根据用户名查询用户信息
//ParametetMapper.java: User getUserByUsername(String username); //映射文件: <select id="getUserByUsername" resultType="User"> select * from t_user where username = #{username} </select> //测试类: @Test public void testGetUserByUsername(){ ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class); User user = mapper.getUserByUsername("张三"); System.out.println(user); }
- 如果报错,可能是查询到多个用户名为张三的
- #{ } 的大括号中可以写任意变量,毕竟传过来就一个字面量,占位符就一个,不给这个占位符给谁呢,但是肯定规范些比较好,写成 username
- 如果换用 ${} 就得注意单引号问题:
select * from t_user where username = '${username}'
- 传参为多个字面量:例如根据用户名和密码验证登录
-
//ParametetMapper.java: User checkLogin(String username,String password); //映射文件: <select id="checkLogin" resultType="User"> select * from t_user where username = #{username} and password = #{password} </select> //测试类: @Test public void testCheckLogin(){ ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class); User user = mapper.checkLogin("张三","123"); System.out.println(user); }
-
发现报错了,根本解析不了 sql ,传过去的参数都获取不到
-
根据报错时的解决方案可知可用参数有 {arg0, arg1, param1, param2}
-
MyBatis 在接收到多个参数时会自动放入 map 集合,以如上两种方式为键,参数为值,所以 sql 修改如下
-
select * from t_user where username = #{arg0} and password = #{arg1}
-
那么根据原理你还能这么写:
-
select * from t_user where username = #{arg0} and password = #{param2}
-
换用 ${} 方式还是一样,注意单引号就 ok
- 传参为 map
-
既然 MyBatis 在接收多个参数时自动放入 map,那索性我自己用 map 装起来参数,传参改为 map
-
//ParametetMapper.java: User checkLoginByMap(Map<String,Object> map); //映射文件: <select id="checkLoginByMap" resultType="User"> select * from t_user where username = #{username} and password = #{password} </select> //测试类: @Test public void testCheckLoginByMap(){ ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class); Map<String, Object> map = new HashMap<>(); map.put("username","张三"); map.put("password","123"); User user = mapper.checkLoginByMap(map); System.out.println(user); }
-
传参 map 我们自己设定了 key,所以映射文件中的 sql 获取参数方式就根据我们设定的 key 来获取
-
用 ${} 依旧一样注意单引号就 ok(还是那句话,如果你不怕 SQL 注入就用)
- 传参为实体类类型
-
其实 map 都知道根据 key 取值了,那么对象肯定也就是根据属性来获取属性值了
-
这里有个知识点:什么叫属性?你会认为是成员方法,你可以这么认为,但是属性其实是 get、set 方法中去掉 get、set 后获取的字符串的首字母大写改为小写。例如 setName,name 就是属性。因为有时候没有相对应的成员变量,却有相对应的 get,set 方法。
-
//ParametetMapper.java: int insertUser(User user); //映射文件: <insert id="insertUser"> insert into t_user values (null,#{username},#{password},#{age},#{sex},#{email}) </insert> //测试类: @Test public void testInsertUser(){ ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class); User user = new User(null,"王五","123",28,"男","123@qq.com"); int result = mapper.insertUser(user); System.out.println(result); }
-
最后还是一样的道理,${} 注意单引号
- 命名参数
-
第二种方式明显那个键名不符合我们的规范,但是第三种方式我们自己建 map 好像也很费力,所以我们一般都会采用命名参数的方式
-
//ParametetMapper.java: User checkLoginByParam(@Param("username") String username, @Param("password") String password); //映射文件: <select id="checkLoginByParam" resultType="com.mybatis.pojo.User"> select * from t_user where username = #{username} and password = #{password} </select> //测试类: @Test public void testCheckLoginByParam(){ ParameterMapper mapper = SqlSessionUtils.getMapper(ParameterMapper.class); User user = mapper.checkLoginByParam("张三","123"); System.out.println(user); }
-
加了 @Param 注解的参数,到时候存到 MyBatia 的 map 中时就会根据你写的 @Param 的值来存储对应的值。你也可以故意写错 sql 语句中 #{} 的值来看看可用的值有哪些,会发现有 password,param1,usernam,param2
总结:建议将以上的两种情况都统一成两种情况:@Param 或者实体类,除非你说你只能传 map。
@Param源码
- 首先断点打在 mapper 的方法调用行
- F7 进入 checkLoginByParam 实际的 MyBatis 为我们写好的 invoke 方法
try {
//Object的equals看你是不是同一个对象,一个是Object.class,是 class java.lang.Object,另一个是method.getDeclaringClass(),是 interface com.mybatis.mapper.ParameterMapper,怎么都不可能相等,所以进 this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession)
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
- 进 this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession),这里的 args 是你传入的参数也就是 username 和 password 对应的值,他放进了 Object 数组,也就是 args = {“张三”,“123”}
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
//没啥好说的直接进 excute(sqlSession, args)
return this.mapperMethod.execute(sqlSession, args);
}
- 进 this.mapperMethod.execute(sqlSession, args)
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
//commond 有 name、type 两个属性,name 为全类名,而类型就是映射文件中你写的 SELECT,所以进 case SELECT
switch(this.command.getType()) {
case INSERT:
...
case UPDATE:
...
case DELETE:
...
//来这
case SELECT:
//我们定义的方法也就是 method,返回结果是 User,所以以下的都不是,直接进 else,
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
//来这,重头戏来了,进 this.method.convertArgsToSqlCommandParam(args)
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
...
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
- 进 this.method.convertArgsToSqlCommandParam(args)
public Object convertArgsToSqlCommandParam(Object[] args) {
return this.paramNameResolver.getNamedParams(args);
}
- 看看 this.paramNameResolver.getNamedParams(args) 是啥
public Object getNamedParams(Object[] args) {
//name 是啥?怎么拿的 name 参考下面的代码解读
int paramCount = this.names.size();
if (args != null && paramCount != 0) {
//下面代码可知 this.hasParamAnnotation 为 true
if (!this.hasParamAnnotation && paramCount == 1) {
...
} else {
Map<String, Object> param = new ParamMap();
int i = 0;
for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {
//结合上下代码现在是两个数组
//args:{"张三","123"}
//names:{"username","password"}
Entry<Integer, String> entry = (Entry)var5.next();
//这可不就是 param.put("username","张三")
param.put(entry.getValue(), args[(Integer)entry.getKey()]);
//这里就是默认为我们设置的键值对 param1,param2......
String genericParamName = "param" + (i + 1);
//如果我们没用 @Param 注解设置 key 为 paramx,我们当然不会这么做
if (!this.names.containsValue(genericParamName)) {
//默认生成的键值对也放进去了
param.put(genericParamName, args[(Integer)entry.getKey()]);
}
}
//这不就是最后的{"username":"张三","param1":"张 三","password":"123",
// "param2":"123"}
return param;
}
} else {
return null;
}
}
-
name 的来源:
-
public ParamNameResolver(Configuration config, Method method) { this.useActualParamName = config.isUseActualParamName(); Class<?>[] paramTypes = method.getParameterTypes(); //获取参数注解,可能多个参数有多个注解所以这里使用二维数组 Annotation[][] paramAnnotations = method.getParameterAnnotations(); SortedMap<Integer, String> map = new TreeMap(); //这是 Annotation[] 的长度也就是列的长度也就是有注解的参数的个数 int paramCount = paramAnnotations.length; for(int paramIndex = 0; paramIndex < paramCount; ++paramIndex) { //如果不是特殊的参数类型注解,显然不是 if (!isSpecialParameter(paramTypes[paramIndex])) { String name = null; //获取第 paramIndex 个参数的所有注解 Annotation[] var9 = paramAnnotations[paramIndex]; int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { Annotation annotation = var9[var11]; //由于我们有 @Param 注解所以这里是 true if (annotation instanceof Param) { //这个判断有无 Param 注解的标志就变成 true this.hasParamAnnotation = true; //然后获取注解的值例如@param("username"),这个 value 就是 username name = ((Param)annotation).value(); break; } } if (name == null) { ... } //所以第一次循环就有了{0:"username"},以此类推 map.put(paramIndex, name); } } //最后 names 就是把我们得到的 map 转换一成集合{"username","password"} this.names = Collections.unmodifiableSortedMap(map); }
六、MyBatis 的各种查询功能
查询一个实体类对象
- 建个 SelectMapper 接口 编写一个 根据 id 查询用户的方法,创建对应映射文件,创建对应测试类
- 若查询出的数据只有一条,可以通过实体类对象或者 list、map 集合接收
查询一个 list 集合
- 同理创建查询所有用户的方法并测试
- 若查询出的数据有多条,可以通过 list 集合接收,但是不能通过实体类对象接收,会抛异常TooManyResultException
查询单个数据
-
例如查询用户数
-
<!--Integer getCount();--> <select id="getCount" resultType="java.lang.Integer"> select count(*) from t_user </select>
-
这里结果集设置成 Int,integer 发现都可以,这是因为 MyBatis 自动为我们设置了一些默认的类型别名,且不区分大小写
查询一条数据为 map 的集合
-
<!--Map<String,Object> getUserByIdToMap(@Param("id") Integer id);--> <select id="getUserByIdToMap" resultType="Map"> select * from t_user where id = #{id} </select> @Test public void testGetUserByIdToMap(){ SelectMapper mapper = SqlSessionUtils.getMapper(SelectMapper.class); Map<String, Object> map = mapper.getUserByIdToMap(4); System.out.println(map); }
-
我们知道 map 是以键值对的形式存储数据,表中数据正好以字段名为 key 以字段值为 value
查询多条数据为 map 的集合
- 既然上面用一个 map 存放一条数据,那么多条数据就用 List 的 map 集合来接收,其实实体类和 map
很像,多个用户用用户类型的 list 接收,所以用 map 类型接收也得用 list
-
<!--List<Map<String,Object>> getAllUserToMap();--> <select id="getAllUserToMap" resultType="map"> select * from t_user </select>
-
用 list 接收是因为查询到每条数据都被自动被转为 map,根据字段名和字段的值,我们形成不了一个完整的数据。但是其实我们的 map 是可以存储多条数据的,因为 Object 可以是一个 User 而不一定要是一个字符串类型的某个字段的值。这时你可以在 mapper 接口中的方法上加上 MapKey 注解,为查询到的每条数据设置 key,这样就可以用 map 根据你设置的 key 存储多条数据
-
总结:查询多条数据时
- 通过 Map 类型的 list 接收
- 通过实体类类型的 list 接收
- mapper 接口方法上添加 @MapKey(“value”) 将每条数据转为 map 集合的值,以 value 为键(value 为某个字段),放在同一个 map 集合中
六、特殊 SQL 的执行
模糊查询
-
老三样,Mapper、Mapperxml、Test
-
如果用 #{} 根据用户名模糊查询
-
<!--List<User> getUserByLike(@Param("username") String username);--> <select id="getUserByLike" resultType="User"> select * from t_user where username like '%#{username}%' </select>
-
那么测试时会发现报错了,因为你用比如 #{username} ,最后会被解析成 ‘xx’,那么你的 SQL 语句就变成了
-
select * from t_user where username like '%'xx'%'
,所以会报错
解决方案一:使用 ${}
- 直接修改 sql 把 # 改成 $ 即可
解决方案二:使用 sql 函数 concat
-
修改 sql 如下
-
select * from t_user where username like concat('%',#{username},'%')
-
concat 会将多个字符串拼接成一个字符串,其实就是字符串相加,所以如下
-
concat('%',#{username},'%')='%' + '张' + '%'='%张%'
-
最后的 sql 就拼接成:
select * from t_user where username like '%张%'
解决方案三:自己拼接 #{}(推荐使用)
-
既然 concat 函数可以,那么自己拼接字符串当然也行
-
这里注意字符串用双引号:
-
select * from t_user where username like "%"#{username}"%"
-
感觉貌似是会自动转换成像 concat 一样的字符串相加
批量删除
-
例如 delete * from t_user where id in {1,2,3}
-
那么我们的传参很明显应该是一个字符串,例如 “1,2,3”
-
<!--int deleteMore(@Param("ids") String ids);--> <delete id="deleteMore"> delete from t_user where id in (${ids}) </delete>
-
这里还想用 #{} 括号里面就变成了 ‘1,2,3’ ,谁的 id 会等于 ‘1,2,3’
动态设置表名
-
例如查询指定表中的数据,这时表名就需要动态设置
-
<!--List<User> getUserByTableName(@Param("tableName") String tableName);--> <select id="getUserByTableName" resultType="User"> select * from ${tableName} </select>
-
我们知道表名不能带有单引号,所以只能用 ${}
添加功能获取自增的主键
例如我们有一个操作界面,可以新建班级的同时把勾选的同学也添加进班级,也就是说完成这个操作会执行两步,第一步,新建班级插入班级表,第二步,把勾选的每个学生的班级 id 置为新增的班级的 id,那我刚生成的 id 我怎么获取到?
JDBC原生方式
@Test
public void testJDBC() throws Exception {
Class.forName("");
Connection connection = DriverManager.getConnection("", "", "");
//默认不返回,所以传了个参数为返回
PreparedStatement ps = connection.prepareStatement("insert into t_user values(null,?,?,?,?)",Statement.RETURN_GENERATED_KEYS);
ps.executeUpdate();
ResultSet resultSet = ps.getGeneratedKeys();
}
MyBatis
<!--
void insertUser(User user);
useGeneratedKeys:表明设置当前标签中的 sql 使用了自增的主键
keyProperty:将自增的主键生成的值赋给传输到映射文件中参数的某个属性,也就是赋给该方法传参中的某个属性
-->
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user values(null,#{username},#{password},#{age},#{sex},#{email})
</insert>
@Test
public void testInsertUser() throws Exception{
SQLMapper mapper = SqlSessionUtils.getMapper(SQLMapper.class);
User user = new User(null,"王六","123",20,"女","123@163.com");
mapper.insertUser(user);
System.out.println(user);
}
- 因为在 xml 已经设置了返回主键的值,所以测试中当执行插入操作后会返回生成的主键的值给 user 的 id,此时打印 user 发现有了 id
八、自定义映射 resultMap
实际开发中我们数据库表的字段不一定和实体类属性一致,例如 User 的 user_name 和 User 类 的userName,还有一对多等情况。我们在之前结果集设置都用 resultType,此时显然不适用,需要自定义映射 resultMap
-
建个新模块 mybatis_demo3,pom.xml 添加依赖,jdbc 属性文件,log4j 配置文件,mybatis 配置文件,建mapper、pojo 包,建对应 mapper 目录,创建实体类,映射文件,测试类
-
新建 t_emp 以及 t_dept,也就是员工表和部门表,对应实体类如下
-
@Data @AllArgsConstructor @NoArgsConstructor @ToString //员工类 public class Emp { private Integer eid; private String empName; private Integer age; private Dept dept; } @Data @AllArgsConstructor @NoArgsConstructor @ToString //部门类 public class Dept { private Integer did; private String deptName; private List<Emp> emps; }
-
新建查询所有员工的方法,如果还使用 restltType
-
<!--List<Emp> getAllEmp();--> <select id="getAllEmp" resultType="com.mybatis.pojo.Emp"> select * from t_emp </select>
-
测试打印发现因为 emp_name 和 empName 不同名所以无法获取到值,其他对的上的可以获取到
字段别名
既然名字对应不上,那我就让你对应上,当然我不可能改变字段名或者属性名
- 我们就把 sql 查询的 * 换成每个字段然后起别名就能和实体类的属性对应上
select eid,emp_name empName,age from t_emp
全局配置mapUnderscoreToCamelCase
既然数据库字段以及实体类属性名各有命名规范,那么 MyBatis 也意识到了这点,可以在 MyBatis 配置文件中设置
-
<settings> <!--将下划线映射为驼峰--> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>
resultMap 设置自定义映射关系
-
上面两种方法一种太麻烦,而且不易维护,另一种只能解决默认的下划线转驼峰的情况,还是有局限性,其实用 resultMap 解决当前问题也是大材小用,一般都是解决一对多,多对一的问题,以下只是演示用法
-
<select id="getAllEmp" resultMap=""> select * from t_emp </select>
-
那 resultMap 的值又是什么,既然是自定义映射关系,那么你肯定会定义一个 resultMap,而定义时取的名字就是我们要填入的值,这样我们就通过这个名字对应你定义的 resultMap
<!--我们自定义的 resultMap,id 为了让别人使用时指定,type 自然是对应的实体类-->
<resultMap id="empResult" type="emp">
<!--主键需要用 id 标签,其他属性用 result 标签,property 就是实体类属性,这个实体类就是上面 type 写的,column 必须是 sql 语句查询出的字段名-->
<id property="eid" column="eid"/>
<result property="age" column="age"/>
<result property="empName" column="emp_name"/>
</resultMap>
<select id="getAllEmp" resultMap="empResult">
select * from t_emp
</select>
多对一
-
例如多个员工对应一个部门,那么我们就在多的一方设置一的属性,也就是说,Emp 类中加入属性
-
private Dept dept;
-
尝试查询员工以及员工对应部门的信息
-
我们这里结果集用 resultType 肯定不行,因为你查出来的什么能对应 Dept 类型的字段呢,所以只能用 resultMap
-
测试查询:
select * from t_emp left join t_dept on t_emp.did = t_dept.did where t_emp.eid = 1
-
结果如下
-
eid emp_name age did did dept_name 1 张三 10 1 1 A -
那么其实我们就是要把查出来的 eid、emp_name、age 给 emp 的三个属性,把 did 和 dept_name 给 emp 的 dept 属性
1.使用级联属性赋值
<resultMap id="empAndDeptResultOne" type="emp">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
<result property="dept.did" column="did"/>
<result property="dept.deptName" column="dept_name"/>
</resultMap>
<!--Emp getEmpAndDept(@Param("eid") Integer eid);-->
<select id="getEmpAndDept" resultMap="empAndDeptResultOne">
select * from t_emp left join t_dept on t_emp.did = t_dept.did where t_emp.eid = #{eid}
</select>
2.使用 association 标签
<!--处理多对一映射关系方式二:association-->
<resultMap id="empAndDeptResultTwo" type="emp">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
<association property="dept" javaType="dept">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>
- 其实我们会发现,一个 resultMap 其实也是一个类似实体类的概念,那么我们最标准的定义方式应该就是 Emp 实体类有什么属性,resultMap 就有什么属性,但是我们的 Emp 实体类的 dept 属性本身又是一个实体类,所以最好有一个特殊的标签来映射实体类中的实体类属性的映射关系也就是 Emp 类中的 dept 属性,这个标签就是 association,因为它本身也是属性,所以当然也有 property,来指定需要处理的多对一的映射关系的属性名,那么它对应什么实体类呢,我们可以用 javaType 来指定,所以有
<association property="dept" javaType="dept">
,说实话这个标签的属性真的很见名知义,然后我们在其中继续按照一个实体类来处理,有 id、result - 个人感觉:这里的多对一还感觉像是查询到的多个字段对应该实体类的一个属性,也就是 did、dept_name 对应了 Emp 的 dept 属性,我们通过映射关系赋值给 dept,再把 dept 赋值给 emp
3.使用 association 标签分步查询
我们可以一步到位查出来,那我们可以分成多步来查询啊,我们查出 emp 也就得到了 did,我们再通过 did查出 dept 最后把结果赋值给 emp 不就好了
-
首先便是第一步:
-
<resultMap id="empAndDeptByStepOneResult" type="emp"> <id property="eid" column="eid"/> <result property="empName" column="emp_name"/> <result property="age" column="age"/> <association property="dept" select="com.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo" column="did"/> </resultMap> <!--Emp getEmpAndDeptByStepOne(@Param("eid") Integer eid);--> <select id="getEmpAndDeptByStepOne" resultMap="empAndDeptByStepOneResult"> select * from t_emp where eid = #{eid} </select>
-
我们这里依旧使用 association,但是里面不再是属性名 property 以及对应的 java 类 javaType,而是属性名 property 以及下一步的 sql 也就是 select,通过 select 的唯一标识,还有就是传给下一步的参数 column,这里的 column 不是映射的字段名而是下一步中所要使用的这一步传过去的条件,例如我们先查询到 did 然后以此为依据查询进行下一步查询,这里注意 select 的唯一标识是全类名加方法名,因为不同映射文件中的 select 也能有相同的 id,所以带上全类名就能确保唯一性
-
既然获得了 did ,那么就可以查询部门信息了
-
<!--Dept getEmpAndDeptByStepTwo(@Param("did") Integer did);--> <select id="getEmpAndDeptByStepTwo" resultType="dept"> select * from t_dept where did = #{did} </select>
-
这里的 did 就是第一步提供的 column 中的 did,而这里查询出的结果就根据 select 的唯一标识返回给第一步,这也就实现了查询出实体类赋值给某个实体类的实体类属性,也就是查询出 Dept 类数据赋值给 Emp 类的 dept 字段
延迟加载
分布查询的好处就是可以实现延迟加载,默认不开启。我们在第一步的 resultMap 中配置了第二步的 sql,也就是说默认当你调用 getEmpAndDeptByStepOne 时,getEmpAndDeptByStepTwo 也会随之调用,但是当你开启后就会按需调用了。
-
MyBatis 配置文件设置 lazyLoadingEnabled 为 true 即可
-
<setting name="lazyLoadingEnabled" value="true"/>
-
开启延迟加载后我们访问什么信息就执行相关的什么 sql,例如测试类如下
-
@Test public void testGetEmpAndDeptByStep(){ EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class); Emp emp = mapper.getEmpAndDeptByStepOne(1); //System.out.println(emp); System.out.println(emp.getEmpName); }
-
我们将打印内容换成员工名,我们会发现控制台打印的 sql 中只执行了查询员工的 sql 而没有执行查询部门信息的 sql 了,换成打印部门信息同理
-
但是开启延迟加载后会应用到全局,我们有些 sql 不希望延迟加载怎么办?用 fetchType
-
<association property="dept" select="com.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo" column="did" fetchTyper="eager"/> </resultMap>
-
第一步中的 resultMap 中配置的分步查询中设置 fetchType 属性为 eager 就可以开启立即加载,设置为 lazy 则还是延迟加载(注意要先开启延迟加载)
一对多
例如一个部门对应多个员工,那么我们就在“一”的一方定义多的集合属性
1.使用collection标签
-
Dept 类加入属性
private List<Emp> emps;
-
<!--Dept getDeptAndEmp(@Param("did") Integer did);--> <resultMap id="deptAndEmpResultMap" type="dept"> <id property="did" column="did"></id> <result property="deptName" column="dept_name"></result> <collection property="emps" ofType="emp"> <id property="eid" column="eid"/> <result property="empName" column="emp_name"/> <result property="age" column="age"/> </collection> </resultMap> <select id="getDeptAndEmp" resultMap="deptAndEmpResultMap"> select * from t_dept left join t_emp on t_dept.did = t_emp.did where t_dept.did = #{did} </select>
-
我们在多对一时使用的 resultMap 设置结果集时使用的 association 是对应 Emp 类的 dept 属性,是一个 java 类型所以是 javaType,但是我们在实现一对多的时候,“多”是集合,它对应的属性是一个集合,我们 sql查询的并不是对应集合类型数据而是属性所对应的集合中存储的数据类型,也就是 emps 中储存的 Emp 类型的数据,所以使用的是 ofType
2.使用 collection 标签分步查询
既然一对多可以分步查询,那么对应的多对一自然也可以
-
还是一样先考虑分成哪几个 sql,分成两步,其中 collection 标签的 property、select、colume 属性,包括延迟加载与多对一都完全一致,不过分赘述
-
个人感觉:其实一对多与多对一不用去想哪个对应什么。只要想比如一个部门拥有多个员工,你就给部门类 List<Emp> 属性,每个员工都有自己的部门,那就给员工类 Dept 属性,但是不至于,给他个 did 就行,毕竟你在数据库表里面只需要两张表能用某个字段关联起来就行。然后就看使用的标签,不要去管什么一对多多对一,你看你这个实体类查出来的是什么,比如 Dept 类的 emps 属性是一堆员工,这不就是集合,那就用 collection 标签。记得这个,那另一种情况比如多对一,多个员工对应一个部门,你要查某个员工的部门,你只会属于一个部门,所以很明显部门不是集合,不是集合就用 association 标签即可。
九、动态 SQL
本质就是实现 sql 拼接,但是拼接的是条件,比如查一本书可以根据年代、作者、书名,你选了几个条件,对应的 sql 就应该根据几个条件查询。如果你使用 java 语言去拼接 sql,你可以想象有多复杂:先定义字符串 select * from table,除非有条件不为 null 且不等于空你才能拼接上 where,如果有两个及以上条件,第二个条件开始就得是 and …
1.恒成立条件+ if 标签
-
新建 DynamicMapper 三件套
-
<!--List<Emp> getEmpByCondition(Emp emp);--> <select id="getEmpByCondition" resultType="Emp"> select * from t_emp where <if test="empName !=null and empName != ''"> emp_name = #{empName} </if> <if test="age !=null and age != ''"> and age = #{age} </if> </select> @Test public void testGetEmpByCondition(){ DynamicMapper mapper = SqlUtils.getMapper(DynamicMapper.class); List<Emp> emps = mapper.getEmpByCondition(new Emp( "张三", 10)); emps.forEach(System.out::println); }
-
测试会发现没有任何问题查询到了,但是如果将张三换成空字符串或者 null 呢?sql 语句就变成了
-
select * from t_emp where and age = 10
-
很明显 sql 语句就错误了
-
稍微修改 sql
-
<select id="getEmpByCondition" resultType="Emp"> select * from t_emp where 1=1 <if test="empName !=null and empName != ''"> and emp_name = #{empName} </if> <if test="age !=null and age != ''"> and age = #{age} </if> </select>
-
添加恒成立条件此时不影响结果,但是其他条件就都可以用 and 拼接上,并且如果所有条件都不成立时,我们的 where 也不会多出来
-
if:根据标签中的 test 属性所对应表达式决定标签中的内容是否需要拼接到 sql 中
2.where 标签
-
修改 sql 如下
-
<select id="getEmpByCondition" resultType="Emp"> select * from t_emp <where> <if test="empName !=null and empName != ''"> and emp_name = #{empName} </if> <if test="age !=null and age != ''"> and age = #{age} </if> </where> </select>
-
此时发现,就算 empName 为空也能根据 age 查询到数据,也就是说 MyBatis 帮我们自动去除了 and,同理 or 也能自动去除,如果条件都不成立,即 where 标签中无内容时 where 也会被自动去除
-
==注意:==where 标签只能去除内容前多余的 and/or,内容后的不能自动去除
3.trim 标签
那么标签中的内容后面有 and 怎么办
prefix|suffix 属性
- 将 trim 标签中的内容前面或后面添加指定内容
prefixOverrides|suffixOverrides
-
将 trim 标签中的内容前面或后面去掉指定内容
-
修改 sql 如下
-
<select id="getEmpByCondition" resultType="Emp"> select * from t_emp <trim prefix="where" suffixOverrides="and|or"> <if test="empName !=null and empName != ''"> emp_name = #{empName} and </if> <if test="age !=null and age != ''"> age = #{age} and </if> </trim> </select>
-
此时会自动拼接上 where,并且去除标签中内容的后面的 and 以及 or,如果 trim 标签中内容为空,trim 也直接不起作用,不会多出个 where
4.choose + when + otherwise
choose 为父标签,when 就相当于 if,else if,else if…,otherwise 相当于 else
-
新建方法映射 sql 以及测试
-
<!--List<Emp> getEmpByChoose(Emp emp);--> <select id="getEmpByChoose" resultType="Emp"> select * from t_emp <where> <choose> <when test="empName != null and empName != ''"> emp_name = #{empName} </when> <when test="age != null and age != ''"> age = #{age} </when> <otherwise> did = 1 </otherwise> </choose> </where> </select>
-
根据 if elseif else 的规则可知,when 标签你起码得有一个吧,但是 otherwise 就不一定要有了,而且根据 if 的规则只要有条件成立就拼接上去了,其他的就不判断了。
5.foreach 标签
当我们进行批量操作的时候,比如批量删除,我们会例如 delete from table where id in (1,2,3),那么这时我们要传入的就是一个数组,那么数组的每个值怎么放进去呢?使用 foreach 标签
-
首先肯定是写好框架
-
<!--int deleteMoreByArray(Integer[] eids) ;--> <delete id="deleteMoreByArray"> delete from t_emp where eid in () </delete>
-
那么我们就用 foreach 循环把参数放进去
-
既然循环,首先肯定有数组 collection,那么数组名我们很自然地会想到用我们传参的名字,有了要循环的数组肯定还要有循环的变量名,就像
for(Interger eid:eids)
,所以有 item 我们就起名为 eid,最后还有一个问题,如果我们自己拼接每个参数之间的逗号,最后的语句一定会多一个逗号,比如: -
delete from t_emp where eid in ( <foreach collection="eids" item="eid"> #{eid}, </foreach> )
-
那么最终结果会如下
-
delete from t_emp where eid in (1,2,)
-
所以会有分隔符属性 separator,写成逗号即可
-
delete from t_emp where eid in ( <foreach collection="eids" item="eid" separator=","> #{eid} </foreach> )
-
测试发现找不到说 eids,可用参数只有 array、arg0,这不禁让我想到之前传两个参数的时候,那么我们同样用 @Param 指定 key 不就好了
int deleteMoreByArray(@Param("eids") Integer[] eids);
-
测试,大功告成,这也就是之前说的传参除了实体类和 Map,其他都用 @Param
-
其实 foreach 标签还有两个属性 open、close,这样你连括号也不用写
-
delete from t_emp where eid in <foreach collection="eids" item="eid" separator="," open="(" close=")"> #{eid} </foreach>
虽然很笨,但是确实还有一种写法:delete from table where id = 1 or id = 2 or id = 3,但是用了 foreach 标签就不会这么冗长了
-
根据上面每个属性的讲解不难推测怎么写
-
delete from t_emp where <foreach collection="eids" item="eid" separator="or"> eid = #{eid} </foreach>
-
细心一点的话你可能会想,这里的 separator 的值应该写成 " or ",要注意空格问题,不然 sql 变成了
-
delete from table where id = 1orid = 2orid = 3
-
但是其实 MyBatis 会自动在每个参数前后加上空格,所以不用这么写
尝试插入List<Emp> 到表中,那么 sql 语句会为 insert into t_emp values(xx),(xx)
-
根据以上的 foreach 标签的使用规则可以推测
-
<!--int insertMoreByList(@Param("emps") List<Emp> emps);--> <insert id="insertMoreByList"> insert into t_emp values <foreach collection="emps" item="emp" separator=","> (null,#{emp.empName},#{emp.age},null) </foreach> </insert>
-
注意这里就不能用 open 和 close 了,因为这是为标签外全部内容的起始和结束符
sql 标签
我们经常用到一些代码片段,我们就会封装成一个方法,那么对应的,例如我们的查询 sql,最好其实是按需查询,比如 select * from t_emp,我们一般不用查出他的 did,所以可以写成 select eid,emp_name,age from t_emp,但是每次都要写一堆字段太过麻烦,所以我们可以定义成一个 sql 片段
-
我们随便挑选一个查询 sql
-
<!--先定义sql片段--> <sql id="empColumns">eid,emp_name,age</sql> <!--将要替换的部分改为 <include refid="xx"></include>--> select <include refid="empColumns"></include> from t_emp
-
refid 就是你定义的 sql 片段的唯一标识
十、MyBatis 的缓存
我们的 sql 进行查询后得到的结果可能会被缓存下来,这样你到时候进行相同的 sql 查询时不需要再访问数据库,直接读取缓存即可
一级缓存
一级缓存是 SqlSession 级别的,通过同一个 SqlSession 查询的数据会被缓存,默认开启
-
例如新建 CacheMapper 三件套
-
<!--public Emp getEmpByEid(@Param("eid") Integer eid);--> <select id="getEmpByEid" resultType="Emp"> select * from t_emp where eid = #{eid} </select>
-
再测试一下
-
@Test public void testCache(){ CacheMapper mapper = SqlUtils.getMapper(CacheMapper.class); Emp emp = mapper.getEmpByEid(1); System.out.println(emp); Emp emp2 = mapper.getEmpByEid(1); System.out.println(emp2); }
-
观察控制台会发现打印了一句 sql,也就是说只查询了一次,因为是从同一个 SqlSession 获取的同一个 Mapper 进行的查询
-
我们再测试用同一个 SqlSession 的不同 mapper 来查询
-
@Test public void testCache(){ SqlSession sqlSession = SqlUtils.getSqlSession(); CacheMapper mapper = sqlSession.getMapper(CacheMapper.class); CacheMapper mapper2 = sqlSession.getMapper(CacheMapper.class); Emp emp = mapper.getEmpByEid(1); System.out.println(emp); Emp emp2 = mapper2.getEmpByEid(1); System.out.println(emp2); }
-
观察控制台发现还是只打印了一句 sql
-
测试一下不同的 SqlSession
-
@Test public void testCache(){ SqlSession sqlSession = SqlUtils.getSqlSession(); SqlSession sqlSession2 = SqlUtils.getSqlSession(); CacheMapper mapper = sqlSession.getMapper(CacheMapper.class); CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class); Emp emp = mapper.getEmpByEid(1); System.out.println(emp); Emp emp2 = mapper2.getEmpByEid(1); System.out.println(emp2); }
-
结果证明默认的确是 SqlSession 级别的一级缓存
使一级缓存失效的四种情况
- 不同的 SqlSession 对应不同的一级缓存(上面测过了)
- 同一个 SqlSession 但是查询条件不同(感觉是废话,因为条件不同我都没有你这个数据我怎么缓存)
- 同一个 SqlSession 两次查询期间执行了一次任意的增删改操作(肯定啊,你删了一条数据再查询全部怎么可能还会是原来的全部数据)
- 同一个 SqlSession 两次查询期间手动删除了缓存(两个语句间添加
sqlSession.clearCache();
,自作孽当然不可活)
二级缓存
二级缓存是 SqlSesssionFactory 级别,通过同一个 SqlSessionFactory 创建的 SqlSession 查询的结果会被缓存
二级缓存开启条件
- 核心配置文件中设置全局配置属性 cacheEnabled = “true” ,但是其实默认就为 true,不需要设置
- 在映射文件中设置标签 <cache/>
- 二级缓存必须在 SqlSession 关闭或提交后有效(我们没有关闭或者提交的时候储存在一级缓存,关闭或提交后存储在二级缓存,有那么点局部变量和全局变量的感觉,一级缓存就是最里面的局部变量,二级缓存是最外面的全局变量,再外面一层都已经是 SqlSessionFactoryBuilder 了,你的 SqlSessionFactory 是根据你的 MBbatis 配置文件来创建的,也就是说它所对应的操作都在同一个数据库中执行,不同的 SqlSessionFactoryBuilder 都已经跳出数据库层面的对应了,这都缓存确实说不过去)
- 查询的数据所转换的实体类类型必须实现序列化的接口(
public class Emp implements Serializable
)
此时想要测试,你的工具类肯定要新增一个返回 SqlSessionFactory 的方法,参照之前的方法就行
-
映射文件添加 cache 标签
-
<mapper namespace="com.mybatis.mapper.CacheMapper"> <cache/> ... </mapper>
-
Emp 类实现序列化接口
-
测试类如下
-
@Test public void testTwoCache(){ SqlSessionFactory sqlSessionFactory = SqlUtils.getSqlSessionFactory(); SqlSession sqlSession1 = sqlSessionFactory.openSession(true); CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class); System.out.println(mapper1.getEmpByEid(1)); sqlSession1.commit(); SqlSession sqlSession2 = sqlSessionFactory.openSession(true); CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class); System.out.println(mapper2.getEmpByEid(1)); sqlSession2.commit(); }
-
控制台有一句
Cache Hit Ratio [com.mybatis.mapper.CacheMapper]: 0.5
就是缓存命中率有 0.5,就是有二级缓存 -
老实说
SqlSessionFactory.openSession(true)
不是会自动提交事务吗,不加sqlSession1.commit();
还不行
二级缓存失效
- 两次查询间任意的增删改操作
二级缓存相关配置(cache 标签的属性)
-
eviction:缓存回收策略,你不可能无限缓存,所以要进行回收,默认 LRU
-
LRU(Least Recently Used):最近最少使用原则,也就是移除最长时间不用的
-
FIFO(First in First out):先进先出,按对象进入缓存的顺序来移除
-
SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象,内存不足才回收
-
WEAK(弱引用):移除基于垃圾回收器状态和弱引用规则的对象,垃圾回收时,内存足不足都回收
-
-
flushInterval:刷新间隔,单位毫秒,多长时间刷新二级缓存,默认情况不设置,增删改时才刷新
-
size:引用数目,正整数,可以缓存多少个对象,太大容易内存溢出
-
readOnly:只读,true/false。
- true:二级缓存返回缓存对象的相同实例,但是对象不能修改,不然就等于直接修改数据库了,缓存都没意义了,反应会快些
- flase:返回缓存对象的拷贝,那随便你改了,但是速度肯定会慢一些
缓存查询的顺序
- 先查询二级缓存,因为他范围大
- 二级缓存没有命中再查询一级缓存
- 一级缓存也没有命中就只能查询数据库了
- SqlSession 关闭后要一级缓存中的数据写入二级缓存
整合第三方缓存 EHCache
MyBatis 毕竟只是持久层框架,所以缓存技术可能并没有那么好,所以允许调用其他缓存技术的接口,但是只能代替二级缓存
-
添加依赖
-
<!-- Mybatis EHCache整合包 --> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.1</version> </dependency> <!-- slf4j日志门面的一个具体实现 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
-
添加配置文件 ehcache.xml
-
<?xml version="1.0" encoding="utf-8" ?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"> <!-- 磁盘保存路径 --> <diskStore path="D:\atguigu\ehcache"/> <defaultCache maxElementsInMemory="1000" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="true" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"></defaultCache> </ehcache>
-
那么怎么让程序知道他该用你添加的 EHCache 呢,通过 cache 标签的 type 属性
-
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
-
存在 SLF4J 时,作为简易日志的 log4j 将失效,此时我们需要借助 SLF4J 的具体实现 logback 来打印日志。
-
创建 logback.xml
-
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="true"> <!-- 指定日志输出的位置 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <!-- 日志输出的格式 --> <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 --> <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern> </encoder> </appender> <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR --> <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 --> <root level="DEBUG"> <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender --> <appender-ref ref="STDOUT"/> </root> <!-- 根据特殊需求指定局部日志级别 --> <logger name="com.atguigu.crowd.mapper" level="DEBUG"/> </configuration>
十一、MyBatis的逆向工程
正向工程是框架根据实体类生成数据库表,Hibernate 就支持正向工程
逆向工程则是先创建数据库表,框架负责根据数据库表生成 Java 实体类,Mapper 接口,Mapper 映射文件
逆向工程的本质其实也就是代码生成器
-
新建一个模块 MyBatis_MBG(MyBatisGegerator)
-
添加依赖和插件
-
<!-- 依赖MyBatis核心包 --> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> <!-- junit测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.3</version> </dependency> <!-- log4j日志 --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> </dependencies> <!-- 控制Maven在构建过程中相关配置 --> <build> <!-- 构建过程中用到的插件 --> <plugins> <!-- 具体插件,逆向工程的操作是以构建过程中插件形式出现的 --> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.0</version> <!-- 插件的依赖 --> <dependencies> <!-- 逆向工程的核心依赖 --> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.2</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.8</version> </dependency> </dependencies> </plugin> </plugins> </build>
-
到时候可以双击这个插件来生成代码
-
jdbc.properties 和 log4j.xml 配置文件复制过来,mybatis-config.xml 也生成一下,暂时可以不配置别名包和 mapper 包,生成了再配置
-
接下来就是最重要的逆向工程的配置文件 generatorConfig.xml,先用一下清新简洁版
-
<?xml version="1.0" encoding="UTF-8"?> <!--如果是 dtd 约束,根标签必定为 DOCTYPE 后面的值也就是 generatorConfiguration--> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- targetRuntime: 执行生成的逆向工程的版本 MyBatis3Simple: 生成基本的CRUD(清新简洁版)功能为增删改,查全部,根据id查单条记录这5个功能 MyBatis3: 生成带条件的CRUD(奢华尊享版) --> <context id="DB2Tables" targetRuntime="MyBatis3Simple"> <!-- 数据库的连接信息 --> <!--想要根据数据库表生成自然要数据库连接信息--> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/mybatis" userId="root" password="123456"> </jdbcConnection> <!-- javaBean的生成策略--> <!--targetPackage:生成的包名--> <!--targetProject:生成的目录在哪--> <!--最后实体类生成在.\src\main\java\com\sky\model下--> <javaModelGenerator targetPackage="com.sky.model" targetProject=".\src\main\java"> <!--是否使用子包,也就是.是不是包的分隔符,如果是 false 的话 com.sky.model 整个就是一个包名了--> <property name="enableSubPackages" value="true"/> <!--根据字段名生成属性时去掉字段名前后的空格--> <property name="trimStrings" value="true"/> </javaModelGenerator> <!-- SQL映射文件的生成策略 --> <sqlMapGenerator targetPackage="com.sky.mapper" targetProject=".\src\main\resources"> <property name="enableSubPackages" value="true"/> </sqlMapGenerator> <!-- Mapper接口的生成策略 --> <javaClientGenerator type="XMLMAPPER" targetPackage="com.sky.mapper" targetProject=".\src\main\java"> <property name="enableSubPackages" value="true"/> </javaClientGenerator> <!-- 逆向分析的表 --> <!-- tableName设置为*号,可以对应所有表,此时不写domainObjectName --> <!-- domainObjectName属性指定生成出来的实体类的类名 --> <!--映射文件和 mapper 接口类也直接根据实体类名生成--> <table tableName="t_emp" domainObjectName="Emp"/> <table tableName="t_dept" domainObjectName="Dept"/> </context> </generatorConfiguration>
-
再试一下尊贵奢华版,其实也就是把 targetRuntime 修改一下
-
<context id="DB2Tables" targetRuntime="MyBatis3">
-
他生成的实体类会多一个 xxExample,有了这个实体类,就可以进行任意条件的操作了
-
别忘了 MyBatis 的核心配置 mapper 包目录,实体类重写一下 toString
-
创个测试类试一下员工姓名等于张三的
-
@Test public void testMBG(){ EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class); EmpExample example = new EmpExample(); example.createCriteria().andEmpNameEqualTo("张三"); List<Emp> list = mapper.selectByExample(example); list.forEach(System.out::println); }
-
这是 QBC (Query By Criteria)风格,也就是条件查询查的时候首先是创建一个条件
createCriteria()
,然后就看你的条件了比如and emp_name = '张三'
,那就是andEmpNameEqualTo("张三")
-
再试一下修改功能,其中有四种修改方式,根据主键或条件修改或选择修改,根据条件或者主键没什么好说的,选择修改又是什么意思
-
这是我的 t_emp 表的某个员工,id 为 1,姓名为张三,年龄为 10,部门 id 为 1
-
试一下根据主键修改
-
@Test public void testUpdate(){ EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class); Emp emp = mapper.selectByPrimaryKey(1); emp.setAge(null); mapper.updateByPrimaryKey(emp); }
-
此时 age 被更新为 null
-
根据主键选择修改则不同,先把年龄改回 10,再测试选择修改
-
@Test public void testUpdate(){ EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class); Emp emp = mapper.selectByPrimaryKey(1); emp.setAge(null); mapper.updateByPrimaryKey(emp); }
-
此时发现年龄未被修改,选择修改就是如果传过去的某个属性的值为 null 则跳过该字段不做修改,如果不是选择修改那就是你传什么我就改成什么
十二、分页插件
-
添加依赖
-
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.2</version> </dependency>a
-
然后在 MyBatis 核心配置文件配置分页插件
-
<plugins> <!--设置分页插件--> <plugin interceptor="com.github.pagehelper.PageInterceptor"/> </plugins>
-
先回顾一下之前的分页
-
public class PageHelperTest { /** * 分页是根据 limit 关键字,limit index,pageSize * index:当前页的起始索引,假设每页 5 条数据 * 比如第 1 页的第一条数据是数据库中的第 1 条数据,所以索引是 0, * 第 2 页的第一条数据是数据库中的第 6 条数据,索引就是 5 * 第 3 页的第一条数据是数据库中的第 11 条数据,索引就是 10 * pageSize:每页显示的条数 * pageNum:当前页的页码数 * 我们一般都是知道当前页和每页条数 * index 怎么计算呢?比如第 3 页的第一条数据的索引,他是第 3 页 * 他之前就有(3-1)页数据,也就是 (pageNum-1) * 数量是页数乘以每页条数也就是 (pageNum-1)*pageSize * 他之前有十条,它就是第十一条数据,也就是 (pageNum-1)*pageSize+1 * 他的索引是序号减一也就是 (pageNum-1)*pageSize+1-1 * 也就是 (pageNum-1)*pageSize */ @Test public void testPageHelper(){ EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class); } }
-
使用分页功能也非常简单,在查询前使用分页插件拦截器即可
-
@Test public void testPageHelper(){ EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class); //每页是 3 条数据,查询第 2 页的 PageHelper.startPage(2,3); List<Emp> list = mapper.selectByExample(null); list.forEach(System.out::println); }
-
查询后还能获取分页的相关信息
-
@Test public void testPageHelper(){ EmpMapper mapper = SqlUtils.getMapper(EmpMapper.class); PageHelper.startPage(2,3); List<Emp> list = mapper.selectByExample(null); PageInfo<Emp> page = new PageInfo<>(list,5); System.out.println(page); list.forEach(System.out::println); }
-
new PageInfo<>(list,5)
中的第一个参数就是分页后的数据,第二个参数则是导航分页页码数,比如 5,我们知道前端界面的分页功能中会有上一页,下一页,以及当前页所在的导航页比如你在第三页:1 2 3 4 5,如果传的是 3,比如你在第六页:5 6 7 -
PageInfo 有许多分页信息:
-
PageInfo{ pageNum=2, pageSize=3, size=3, startRow=4, endRow=6, total=7, pages=3, list=Page{count=true, pageNum=2, pageSize=3, startRow=3, endRow=6, total=7, pages=3, reasonable=false, pageSizeZero=false}, prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]}
常用信息:
pageNum:当前页的页码
pageSize:每页显示的条数
size:当前页显示的真实条数,比如最后一页可能只有一条
total:总记录数
pages:总页数
prePage:上一页的页码
nextPage:下一页的页码
isFirstPage/isLastPage:是否为第一页/最后一页
hasPreviousPage/hasNextPage:是否存在上一页/下一页
navigatePages:导航分页的页码数,你传的第二个参数
navigatepageNums:导航分页的页码,[1,2,3]
- 我的学习代码放码云:
https://gitee.com/sky759/my-batis.git