MyBatis-Plus简介
官网:https://baomidou.com/
GitHub:https://github.com/baomidou/mybatis-plus
Gitee:https://gitee.com/baomidou/mybatis-plus
简介
MyBatis-Plus (简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
愿景
我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗中的 1P、2P,基友搭配,效率翻倍。
特性
-
无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
-
损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
-
强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
-
支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
-
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
-
支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
-
支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
-
内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
-
内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
-
分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
-
内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
-
内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
支持数据库
任何 mybatis
支持标准 SQL 的数据库,MyBatis-Plus也支持
- mysql,oracle,db2,h2,hsql,sqlite,postgresql,sqlserver,Phoenix,Gauss ,clickhouse,Sybase,OceanBase,Firebird,cubrid,goldilocks,csiidb
- 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库
框架结构
MyBatis与MyBatis-Plus区别
MyBatis
优点:
1>SQL语句自由控制,较为灵活
2>SQL与业务代码分离,易于阅读与维护
3>提供动态SQL语句,可以根据需求灵活控制
缺点:
1>简单的crud操作也必须提供对应SQL语句
2>必须维护大量的xml文件
3>自身功能有限,要拓展只能依赖第三方插件
MyBatis-Plus 是在Mybatis的基础上进行二次开发的具有MyBatis所有功能, 也添加了不少好用的功能
比如:
1>提供无sql 的crud操作
2>内置代码生成器,分页插件, 性能分析插件等
3>提供功能丰富的条件构造器快速进行无sql开发
4>提供统一全局处理比如:乐观锁、逻辑删除等
…
MyBatis-Plus入门案例-Spring版
步骤1:创建数据库mybatis-plus,并创建employee表,并导入数据
CREATE TABLE `employee` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`admin` bit(1) DEFAULT NULL,
`dept_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
INSERT INTO `employee` VALUES (1, 'admin', '1', 'admin@abc.com', 40, b'1', 6);
INSERT INTO `employee` VALUES (2, '赵总', '1', 'zhaoz@langfeiyes.com', 35, b'0', 1);
INSERT INTO `employee` VALUES (3, '赵一明', '1', 'zhaoym@langfeiyes.com', 25, b'0', 1);
INSERT INTO `employee` VALUES (4, '钱总', '1', 'qianz@langfeiyes.com', 31, b'0', 2);
INSERT INTO `employee` VALUES (5, '钱二明', '1', 'qianem@langfeiyes.com', 25, b'0', 2);
INSERT INTO `employee` VALUES (6, '孙总', '1', 'sunz@langfeiyes.com', 35, b'0', 3);
INSERT INTO `employee` VALUES (7, '孙三明', '1', 'sunsm@langfeiyes.com', 25, b'0', 3);
INSERT INTO `employee` VALUES (8, '李总', '1', 'liz@langfeiyes.com', 35, b'0', 4);
INSERT INTO `employee` VALUES (9, '李四明', '1', 'lism@langfeiyes.com', 25, b'0', 4);
INSERT INTO `employee` VALUES (10, '周总', '1', 'zhouz@langfeiyes.com', 19, b'0', 5);
INSERT INTO `employee` VALUES (11, '周五明', '1', 'zhouwm@langfeiyes.com', 25, b'0', 5);
INSERT INTO `employee` VALUES (12, '吴总', '1', 'wuz@langfeiyes.com', 41, b'0', 5);
INSERT INTO `employee` VALUES (13, '吴六明', '1', 'wulm@langfeiyes.com', 33, b'0', 5);
INSERT INTO `employee` VALUES (14, '郑总', '1', 'zhengz@langfeiyes.com', 35, b'0', 3);
INSERT INTO `employee` VALUES (15, '郑七明', '1', 'zhengqm@langfeiyes.com', 25, b'0', 2);
INSERT INTO `employee` VALUES (16, '孙四明', '1', 'sunsim@langfeiyes.com', 25, b'0', 3);
INSERT INTO `employee` VALUES (17, '孙五明', '1', 'sunwm@langfeiyes.com', 25, b'0', 3);
INSERT INTO `employee` VALUES (18, '李五明', '1', 'liwm@langfeiyes.com', 25, b'0', 4);
INSERT INTO `employee` VALUES (19, '李六明', '1', 'lilm@langfeiyes.com', 25, b'0', 4);
INSERT INTO `employee` VALUES (20, '叶子', '1', 'yezi@langfeiyes.com', 0, b'0', 1);
2>创建maven项目-mybatis-plus-spring
3>pom.xml文件中导入相关依赖
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.4.3.4</version>
</dependency>
<!--数据连接池依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!--自动生成getter/setter方法依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<scope>provided</scope>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--spring测试-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
<!--spring环境依赖包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
<!--spring事务依赖包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
<!--spring jdbc依赖包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
<!--spring 日志依赖包-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
4>配置数据db.properties、日志文件log4j.properties、spring容器文件spring-context-mybatis.xml
jdbc.url=jdbc:mysql://localhost:3306/mybatis-plus?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
jdbc.username=root
jdbc.password=root
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
# Global logging configuration
log4j.rootLogger=ERROR, stdout
log4j.logger.com.langfeiyes.mp.mapper=TRACE
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd">
<!-- 加载配置属性文件 -->
<context:property-placeholder location="classpath:db.properties" ignore-unresolvable="true" />
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="url" value="${jdbc.url}"></property>
</bean>
<!-- 配置SessionFactory -->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- MyBatis 动态扫描 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.langfeiyes.mp.mapper"/>
</bean>
</beans>
5>编写Employee实体类与mapper接口
@Setter
@Getter
@ToString
public class Employee {
private Long id;
private String name;
private String password;
private String email;
private int age;
private int admin;
private Long deptId;
}
/**
* 持久层映射接口:mybatis-plus
* mybatis-plus mapper接口自定义
* 1:自定义一个接口, 继承BaseMapper
* 2:指定接口泛型:当前接口操作实体对象:Employee
*/
public interface EmployeeMapper extends BaseMapper<Employee> {
}
6>编写测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring-context-mybatis.xml"})
public class MapperTest {
@Autowired
private EmployeeMapper employeeMapper;
@Test
public void testSave(){
Employee employee = new Employee();
employee.setAdmin(1);
employee.setAge(18);
employee.setDeptId(1L);
employee.setEmail("yezi@langfeiyes.com");
employee.setName("yezi");
employee.setPassword("111");
employeeMapper.insert(employee);
}
@Test
public void testUpdate(){
Employee employee = new Employee();
employee.setId(1L);
employee.setAdmin(1);
employee.setAge(18);
employee.setDeptId(1L);
employee.setEmail("yezi@langfeiyes.com");
employee.setName("yezi");
employee.setPassword("111");
employeeMapper.updateById(employee);
}
@Test
public void testDelete(){
employeeMapper.deleteById(1L);
}
@Test
public void testGet(){
System.out.println(employeeMapper.selectById(1L));
}
@Test
public void testList(){
System.out.println(employeeMapper.selectList(null));
}
}
MyBatis-Plus入门案例-SpringBoot版
相对上面的spring版本的MyBatis-Plus,SpringBoot版本就简化多了。
1>创建数据库mybatis-plus,并创建employee表,并导入数据
此处跟上面的Spring版一样,不重复操作了
2>创建springboot项目-mybatis-plus-demo
3>pom.xml文件中导入springboot依赖、mybatis-plus依赖、其他依赖等
<!--springboot项目导入依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
<dependencies>
<!--springboot项目基本组件依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!--数据连接池依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--自动生成getter/setter方法依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<scope>provided</scope>
</dependency>
</dependencies>
4>在application.properties文件中配置数据库连接四要素与日志
#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-plus?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 配置sql打印日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
5>编写Employee实体类与mapper接口
@Setter
@Getter
@ToString
public class Employee {
private Long id;
private String name;
private String password;
private String email;
private int age;
private int admin;
private Long deptId;
}
/**
* 持久层映射接口:mybatis-plus
* mybatis-plus mapper接口自定义
* 1:自定义一个接口, 继承BaseMapper
* 2:指定接口泛型:当前接口操作实体对象:Employee
*/
public interface EmployeeMapper extends BaseMapper<Employee> {
}
6>编写启动类,指定mapper接口扫描路径
@SpringBootApplication
//mapper接口所在的包路径
@MapperScan(basePackages = "com.langfeiyes.mp.mapper")
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
7>编写测试类
@SpringBootTest
public class MapperTest {
@Autowired
private EmployeeMapper employeeMapper;
@Test
public void testSave(){
Employee employee = new Employee();
employee.setAdmin(1);
employee.setAge(18);
employee.setDeptId(1L);
employee.setEmail("yezi@langfeiyes.com");
employee.setName("yezi");
employee.setPassword("111");
employeeMapper.insert(employee);
}
@Test
public void testUpdate(){
Employee employee = new Employee();
employee.setId(1L);
employee.setAdmin(1);
employee.setAge(18);
employee.setDeptId(1L);
employee.setEmail("yezi@langfeiyes.com");
employee.setName("yezi");
employee.setPassword("111");
employeeMapper.updateById(employee);
}
@Test
public void testDelete(){
employeeMapper.deleteById(1L);
}
@Test
public void testGet(){
System.out.println(employeeMapper.selectById(1L));
}
@Test
public void testList(){
System.out.println(employeeMapper.selectList(null));
}
}
8>运行testList方法
观察SQL
MyBatis-Plus入门案例解析
原理分析
提出问题
问题1:EmployeeMapper 接口没有定义crud方法,为什么在CRUDTest测试类中可以调用crud方法?
**答:**EmployeeMapper接口继承了BaseMapper接口,BaseMapper接口自定义了很多crud方法,所以即使EmployeeMapper接口没写,它也可以从BaseMapper接口继承而来。
问题2:整个项目没有编写SQL语句,为什么可以实现CRUD操作,比如上面查询可以打印出查询sql?
**答:**项目中没有,但是又能执行,说明了啥?说明了Mybatis-Plus框架帮我们实现了。
问题3:Mybatis-Plus框架如何实现SQL拼接的?
答:
以selectList为例子,实现员工列表的SQL:
SELECT id,name,password,email,age,admin,dept_id FROM employee
这条SQL核心点是啥?
表: employee ,
**列:**id,name,password,email,age,admin,dept_id
设想,那如果mybatis-plus能通过某种手段拼接出表跟列,那么执行sql不就有了么?但这又引出一个问题:mybatis-plus是怎么找到要操作的表跟列呢?
观察下面接口:
public interface EmployeeMapper extends BaseMapper<Employee> {
}
继承的BaseMapper接口中指定一个Employee泛型,那么是否可以这么推测:mybatis-plus通过获取指定的泛型,并解析该类型得到Employee类字节码对象,然后通过反射操作获取类名与字段名。并将类名跟表名映射,字段名跟列名映射,那么上面说的表, 列不就齐了么?
对的,mybatis-plus底层就是这么实现的。我们再看回mybatis-plus架构图
单看左边,也能大体推测出mybatis-plus操作原理:
1>扫描实体对象,通过反射获取实体对象对应的表,对应的列
2>解析表, 列名,做各种适配,然后根据调用的方法拼接出各种CRUD SQL语句
3>将拼接好的SQL语句交给mybatis容器执行。
到这Mybatis-Plus的helloword就介绍啦。
源码分析
上面讲清楚原理,那么我们从源码角度去分析,MyBatis-Plus拼接SQL核心类
1>ISqlInjector SQL 自动注入器 完成sql拼接与注入
2>AbstractMethod 抽象的注入方法类,BaseMapper接口中所有方法的父类
子类有:SelectById, SelectList, DeleteById, UpdateById, Insert 等
3>MappedStatement mapper接口方法与sql的映射对象,里面包含每个sql传入参数,返回值等。
4>SqlMethod MyBatis-Plus 定义枚举类,里面包含BaseMapper接口 主干sql语句
查看步骤:
1>查看mybatis-plus-boot-starter 启动器配置文件-Spring.factories
原因:自定义springboot启动器必须明确指定自动启动配置类,约定放置在spring.factories类中
2>查找mybatis-plus自动启动配置类然后解析
按着ctrl键,点击MybatisPlusAutoConfiguration, 里面有个@Bean 实例方法,springboot容器加载时会自动创建对象,并交给容器管理
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
Resource[] mapperLocations = this.properties.resolveMapperLocations();
if (!ObjectUtils.isEmpty(mapperLocations)) {
factory.setMapperLocations(mapperLocations);
}
// TODO 修改源码支持定义 TransactionFactory
this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);
// TODO 对源码做了一定的修改(因为源码适配了老旧的mybatis版本,但我们不需要适配)
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (!ObjectUtils.isEmpty(this.languageDrivers)) {
factory.setScriptingLanguageDrivers(this.languageDrivers);
}
Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver);
// TODO 自定义枚举包
if (StringUtils.hasLength(this.properties.getTypeEnumsPackage())) {
factory.setTypeEnumsPackage(this.properties.getTypeEnumsPackage());
}
// TODO 此处必为非 NULL
GlobalConfig globalConfig = this.properties.getGlobalConfig();
// TODO 注入填充器
this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
// TODO 注入主键生成器
this.getBeanThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerator(i));
// TODO 注入sql注入器
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
// TODO 注入ID生成器
this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
// TODO 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
factory.setGlobalConfig(globalConfig);
return factory.getObject();
}
上面的diam中, 下面代码是当前重点关注的了,
// TODO 注入sql注入器
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
按着ctrl键,点击进入ISqlInjector 类,它是一个借口,借口里面有唯一方法inspectInject
/**
* SQL 自动注入器接口
*
* @author hubin
* @since 2016-07-24
*/
public interface ISqlInjector {
/**
* 检查SQL是否注入(已经注入过不再注入)
*
* @param builderAssistant mapper 信息
* @param mapperClass mapper 接口的 class 对象
*/
void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}
进入它的实现类,AbstractSqlInjector,核心方法
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
//通过Mapper接口获取泛型对象,比如:Employee
Class<?> modelClass = extractModelClass(mapperClass);
if (modelClass != null) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
//获取Mapper接口中所有方法名
List<AbstractMethod> methodList = this.getMethodList(mapperClass);
if (CollectionUtils.isNotEmpty(methodList)) {
//解析Mapper接口获取方法对应的数据库,表,列等相关信息
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
// 循环注入自定义方法(包括默认的)
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
mapperRegistryCache.add(className);
}
}
}
拼接sql最核心的方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
按着ctrl键,点击进入(AbstractMethod)类的inject 方法,
/**
* 注入自定义方法
*/
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
this.configuration = builderAssistant.getConfiguration();
this.builderAssistant = builderAssistant;
this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
/* 注入自定义方法 */
injectMappedStatement(mapperClass, modelClass, tableInfo);
}
里面的injectMappedStatement 方法是核心,
/**
* 注入自定义 MappedStatement
*
* @param mapperClass mapper 接口
* @param modelClass mapper 泛型
* @param tableInfo 数据库表反射信息
* @return MappedStatement
*/
public abstract MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo);
injectMappedStatement 它是AbstractMethod的抽象方法,需要找对应的子类,这里以SelectList为例子展开讲
进入SelectList类,查看
/**
* 查询满足条件所有数据
*
* @author hubin
* @since 2018-04-06
*/
public class SelectList extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
//获取主干sql模本
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
//最终拼接出来的SQL
String sql = String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo), sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
}
}
其中下面代码就是我们想要的
SqlMethod 枚举类的sql模本
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
SELECT_LIST("selectList", "查询满足条件所有数据", "<script>%s SELECT %s FROM %s %s %s\n</script>"),
通过模板拼接出的sql语句
//最终拼接出来的SQL
String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
<script><choose>
<when test="ew != null and ew.sqlFirst != null">
${ew.sqlFirst}
</when>
<otherwise></otherwise>
</choose> SELECT <choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
</when>
<otherwise>id,name,password,email,age,admin,dept_id</otherwise>
</choose> FROM employee
<if test="ew != null">
<where>
<if test="ew.entity != null">
<if test="ew.entity.id != null">id=#{ew.entity.id}</if>
<if test="ew.entity['name'] != null"> AND name=#{ew.entity.name}</if>
<if test="ew.entity['password'] != null"> AND password=#{ew.entity.password}</if>
<if test="ew.entity['email'] != null"> AND email=#{ew.entity.email}</if>
AND age=#{ew.entity.age}
AND admin=#{ew.entity.admin}
<if test="ew.entity['deptId'] != null"> AND dept_id=#{ew.entity.deptId}</if>
</if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal"> AND</if> ${ew.sqlSegment}
</if>
</where>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
${ew.sqlSegment}
</if>
</if> <choose>
<when test="ew != null and ew.sqlComment != null">
${ew.sqlComment}
</when>
<otherwise></otherwise>
</choose>
</script>
到这,sql拼接源码查看就结束啦
MyBatis-Plus常用注解
上面讲到MyBatis-Plus会解析实体类的类名跟属性名映射数据库的表名跟列名,如果类名与表名,属性名与列名不一致呢?这会怎样?
答案很明显,直接报错
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a9gSKvfs-1693130976149)(/images/image-20211215160158923.png)]
从图上标记错误信息看:name 列找不到。很正常,按照上一章节的分析,列名与属性名必须一一对应,这里不一致,报错就是必然的啦。
思考:该如果处理类名/属性名与表名/列名不一致的问题呢?
MyBatis-Plus的解决方案是:使用注解(文档:https://baomidou.com/pages/223848/)
MyBatis-Plus官方提供10个注解,这里我们先讲其他常用的3个,剩下的等用到后续用到对应功能时再详细展开讲。
@TableName
作用:默认情况下,实体名与表名一致,当不一致时,使用该注解显示指定表名
核心属性:value
案例:
@TableName("t_employee") //t_employee 是表名
public class Employee {
//.....
}
@TableField
作用:默认情况下,实体属性名与列名一致,当不一致时,使用该注解显示指定列名
核心属性:value, exist
public class Employee {
@TableField(value="employename") //员工表列名:employename
private String name;
}
如果实体类中有多余的属性,且没有跟表中某一列映射,此时需要使用exist属性进行排除,这样mybatis-plus在拼接SQL时,放弃这个属性的解析
public class Employee {
@TableField(exist = false)
private Department dept;
}
@TableId
作用:默认情况下,没有明确指定表的主键,使用雪花算法生成一个唯一的long类型的id
核心属性:value, type
案例:
public class Employee {
@TableId(value="id", type=IdType.AUTO)
private Long id;
}
IdType的类型有:
IdType.AUTO :数据库ID自增
IdType.NONE :无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
IdType.INPUT :insert前自行set主键值
IdType.ASSIGN_ID :分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)
IdType.ASSIGN_UUID :分配UUID,主键类型为String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认default方法)
雪花算法计算出的long类型id—IdType.ASSIGN_ID
使用数据库自增----IdType.AUTO
@Version
拓展,后续讲乐观锁时候再讲
@TableLogic
拓展,后续讲逻辑删除时候再讲
MyBatis-Plus通用Mapper接口
BaseMapper介绍
回归到代码,MyBatis-Plus 编写实体对象mapper接口时,需要继承一个通用Mapper接口:BaseMapper
还是以员工实体对象做为操作例子
@Setter
@Getter
@ToString
public class Employee {
private Long id;
private String name;
private String password;
private String email;
private int age;
private int admin;
private Long deptId;
}
BaseMapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
MyBatis-Plus核心功能来自BaseMapepr<实体类> 接口的实现
BaseMapper方法集
insert:1个 update:2个 delete:4个 select:10个
添加-insert
mybatis-plus中添加方法有1个:
/**
* 插入一条记录
*
* @param entity 实体对象
*/
int insert(T entity);
用法:传入要添加的实体对象
需求:往employee表中添加一条记录
@Test
public void testSave(){
Employee employee = new Employee();
employee.setAdmin(1);
employee.setAge(18);
employee.setDeptId(1L);
employee.setEmail("zhangsan@163.com");
employee.setName("zhangsan");
employee.setPassword("111");
employeeMapper.insert(employee);
}
执行后SQL
INSERT INTO employee ( id, name, password, email, age, admin, dept_id ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
更新-update
mybatis-plus中更新方法有2个:
/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
int updateById(@Param(Constants.ENTITY) T entity);
/**
* 根据 whereEntity 条件,更新记录
*
* @param entity 实体对象 (set 条件值,可以为 null)
* @param updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
*/
int update(T entity, Wrapper<T> updateWrapper);
updateById
用法:以id作为更新条件更新指定实体对象
需求:更新id为1员工姓名为:zhangdasan
@Test
public void testUpdateById2(){
Employee employee = new Employee();
employee.setId(1L);
employee.setName("zhangdasan");
employeeMapper.updateById(employee);
}
执行后SQL
UPDATE employee SET name=?, age=?, admin=? WHERE id=?
改前:
改后:
执行之后就发现一个问题,employee表中id为1的员工数据age跟 admin这列数据丢失了,再看打印的SQL,本来只改name字段按理SQL中会拼接set name = ?但是SQL也拼接了age 跟admin列的更新 ,怎么回事呢?
这里就涉及到MyBatis-Plus拼接 SQL的规则啦:
MyBatis-Plus拼接SQL规则:
1:实体对象作为方法参数,如果属性值为null,该属性不参与方法SQL拼接, 反之, 则参与拼接。
2:实体对象作为方法参数,如果属性为基本类型,有默认值,该属性参与方法SQL拼接
上面的updateById方法操作中,name, password, email属性值都是null, 所以update sql语句set中并没有拼接name,password, email 列的更新。而 age, admin 2个属性属于基本类型,不显示设置属性值,那默认就是0,0。 MyBatis-Plus 认为该属性有值,便在update sql语句set 中拼接 age = ?, name = ?。这操作最终导致了表中数据丢失。
那怎么解决上述问题呢
方案1:使用包装类型
private Integer age;
private Integer admin;
包装类型默认值为null, 不参与set拼接
方案2:使用先查询,再替换,后更新
@Test
public void testUpdate(){
//先查询
Employee employee = employeeMapper.selectById(1L);
//再替换
employee.setName("zhangxiaosan");
//后更新
employeeMapper.updateById(employee);
}
先查询,可将表的所有字段查询出来,那么employee所有属性便有值,替换要更新属性,最后update时可以保证set 的列都要值,最终表数据能还原。
方案3:使用update(null, wrapper)方法。
需求:更新id为1员工姓名为:zhangdasan
@Test
public void testUpdate2(){
//Wrapper :暂时认为是sql 语句中where
UpdateWrapper<Employee> wrapper = new UpdateWrapper<>();
wrapper.eq("id", 21L); //等价于: 拼接 where id = 21
wrapper.set("name", "zhangzhongsan");
employeeMapper.update(null, wrapper);
}
执行后SQL
UPDATE employee SET name=? WHERE (id = ?)
这里有一个注意要点,update的第一参数为null,如果传了employee,sql 拼接跟上面的updateById一样,起不到想要的效果。
这种方案,准确的讲应该叫:使用wrapper方式。
思考:updateById 跟update 2个方法该如何选用
/**
* update跟updateById方法的选用
*
* 使用updateById 场景
* 1>where条件是id时候update场景
* 2>进行全量(所有字段)更新时候
*
* 使用update场景
* 1>where条件是不确定,或者多条件的update场景
* 2>进行部分字段更新时候
*/
删除-delete
mybatis-plus中删除方法有4个
/**
* 根据 ID 删除
*
* @param id 主键ID
*/
int deleteById(Serializable id);
/**
* 根据 columnMap 条件,删除记录
*
* @param columnMap 表字段 map 对象
*/
int deleteByMap(Map<String, Object> columnMap);
/**
* 根据 entity 条件,删除记录
*
* @param wrapper 实体对象封装操作类(可以为 null)
*/
int delete(Wrapper<T> wrapper);
/**
* 删除(根据ID 批量删除)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
*/
int deleteBatchIds(Collection<? extends Serializable> idList);
deleteById
用法:删除指定id的实体信息
需求:删除id为1的员工信息
@Test
public void testDeleteById(){
employeeMapper.deleteById(1L);
}
执行后SQL
DELETE FROM employee WHERE id=?
deleteBatchIds
用法:批量删除指定多个id对象信息
需求:删除id为1,2,3的员工信息
@Test
public void testDeleteBatchIds(){
employeeMapper.deleteBatchIds(Arrays.asList(1L, 2L, 3L));
}
执行后SQL
DELETE FROM employee WHERE id IN ( ? , ? , ? )
deleteByMap
用法:按条件删除,具体条件放置在map集合中
需求:删除name=zhangsan并且age=18的员工信息
@Test
public void testDeleteByMap(){
//key: 列, value:要匹配的条件值
Map<String, Object> map = new HashMap<>();
map.put("name", "zhangsan");
map.put("age", 18);
//多条件删除, map里面装的都是where 条件
employeeMapper.deleteByMap(map);
}
执行后SQL
DELETE FROM employee WHERE name = ? AND age = ?
delete
用法:按条件删除,具体条件通过条件构造器拼接
需求:删除name=zhangsan并且age=18的员工信息
@Test
public void testDelete(){
//如果更新条件操作使用: UpdateWrapper
//其他条件操作使用: QueryWrapper
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("age", 18);
wrapper.eq("name", "zhangsan"); //条件是and拼接
employeeMapper.delete(wrapper);
}
执行完的SQL
DELETE FROM employee WHERE (age = ? AND name = ?)
查询-select
mybatis-plus中查询方法有10个
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
T selectById(Serializable id);
/**
* 查询(根据ID 批量查询)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
*/
List<T> selectBatchIds(Collection<? extends Serializable> idList);
/**
* 查询(根据 columnMap 条件)
*
* @param columnMap 表字段 map 对象
*/
List<T> selectByMap( Map<String, Object> columnMap);
/**
* 根据 entity 条件,查询一条记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
T selectOne(Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询总记录数
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
Integer selectCount(Wrapper<T> queryWrapper);
/**
* 根据 entity 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<T> selectList( Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<Map<String, Object>> selectMaps(Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录
* <p>注意: 只返回第一个字段的值</p>
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<Object> selectObjs(Wrapper<T> queryWrapper);
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
<E extends IPage<T>> E selectPage(E page, Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类
*/
<E extends IPage<Map<String, Object>>> E selectMapsPage(E page, Wrapper<T> queryWrapper);
selectById
用法:查询指定id 对象信息
需求:查询id=1的员工信息
@Test
public void testselectById(){
Employee employee = employeeMapper.selectById(1L);
}
执行完SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE id=?
selectBatchIds
用法:批量查询指定的id对象信息
需求:查询id=1,2,3的员工信息
@Test
public void testselectBatchIds(){
employeeMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L));
}
执行完SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE id IN ( ? , ? , ? )
selectByMap
用法:查询满足条件实体信息,条件封装到map集合中
需求:查询age=18并且name=zhangsan的员工信息
@Test
public void testselectByMap(){
Map<String, Object> map = new HashMap<>();
map.put("age", 18);
map.put("name", "zhangsan");
List<Employee> employees = employeeMapper.selectByMap(map);
}
执行完SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE name = ? AND age = ?
selectCount
用法:查询满足条件实体信息记录总条数
需求:查询员工表记录条数
@Test
public void testselectCount(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
//如果统计所有数据:selectCount(null)
//Integer count = employeeMapper.selectCount(wrapper);
Integer count = employeeMapper.selectCount(wrapper);
}
执行完SQL
SELECT COUNT( 1 ) FROM employee
selectList
用法:查询满足条件实体信息记录, 返回List
需求:查询满足条件的所有的员工信息, 返回List
@Test
public void testselectList(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
List<Employee> list = employeeMapper.selectList(wrapper);
for (Employee employee : list) {
System.out.println(employee);
}
}
执行完SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee
selectMaps
用法:查询满足条件实体信息记录, 返回List<Map<String, Object>>
需求:查询满足条件的所有的员工信息, 返回List
@Test
public void testselectMaps(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
List<Map<String, Object>> mapList = employeeMapper.selectMaps(wrapper);
for (Map<String, Object> map : mapList) {
System.out.println(map);
}
}
执行完SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee
比较selectList 跟 selectMap 2个方法,一个返回值:List<实体> 一个是List
思考:开发中什么时候使用selectList,什么时候使用selectMaps呢?
/**
* 如果sql语句执行完之后,得到的数据能封装成实体对象时使用:selectList
* 如果sql语句执行完之后,得到的数据不能封装成实体对象时使用:selectMaps
*
* 比如:按部门id分组,统计每个部门员工的个数。
*
* select dest_id, count(id) count from employe group by dest_id
* 此时返回的2列无法封装成employee对象(原因:没有对应的属性与列对应),此时要么封装成vo值对象,要么就是map对象。
*/
selectPage
用法:分页查询满足条件实体信息记录
需求:查询第二页员工数据, 每页显示3条, (分页返回的数据是员工对象)
mybatis-plus分页查询步骤分2步:
1>在配置类中添加分页插件(springboot项目)
//分页
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setOverflow(true); //合理化
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
2>编写分页代码
@Test
public void testselectPage(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
//等价于:PageResult PageInfo
//参数1:当前页, 参数2:每页显示条数
Page<Employee> page = new Page<>(2,3);
Page<Employee> p = employeeMapper.selectPage(page, wrapper);
System.out.println(p == page); //true
System.out.println("当前页:" + page.getCurrent());
System.out.println("每页显示条数:" + page.getSize());
System.out.println("总页数:" + page.getPages());
System.out.println("总数:" + page.getTotal());
System.out.println("当前页数据:" + page.getRecords());
}
执行完SQL
SELECT COUNT(1) FROM employee
SELECT id,name,password,email,age,admin,dept_id FROM employee LIMIT ?,?
selectMapsPage
用法:跟selectPage一样,区别在与selectMapsPage返回分页数据集合泛型是Map, selectPage是实体对象。
selectOne
用法:查询满足条件实体对象,如果有多条数据返回,抛异常。
需求:查询name=zhangsan,password=1的员工数据
@Test
public void testselectOne(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("name", "zhangsan");
wrapper.eq("password", "1");
Employee employee = employeeMapper.selectOne(wrapper);
System.out.println(employee);
}
执行完SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (name = ? AND password = ?)
selectObjs
用法:查询满足条件实体对象,返回指定列的集合,如果没有指定列,默认返回第一列
需求:查询员工表所有员工名称
@Test
public void testselectObjs(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select("name");
List<Object> list = employeeMapper.selectObjs(wrapper);
list.forEach(System.out::println);
}
执行完SQL
SELECT name FROM employee
到这,通用的Mapper接口就介绍完了。
MyBatis-Plus条件构造器-Wrapper
Wrapper介绍
https://baomidou.com/pages/10c804/
条件构造器-Wrapper按照官方的给出的解释是用于生成 sql 的 where 条件,但在使用的过程中,发现Wrapper不仅仅是用于拼接where条件,可以用于拼接set 语法, select语法。所以,wrapper准确的来说,更像mybatis中的动态标签。在sql任意位置完成sql片段组装。
类体系结构
按功能分
修改型:UpdateWrapper LambdaUpdateWrapper
查询型:QueryWrapper LambdaQueryWrapper
按操作分
传统型:QueryWrapper UpdateWrapper
Lambda:LambdaQueryWrapper LambdaUpdateWrapper
不管怎么分,它们都有一个共同的父类:AbstractWrapper, 父类里面定义wrapper核心的条件方法比如:
allEqeq ne gt ge lt le between notBetween like notLike likeLeft likeRight isNull isNotNull in notIn inSql notInSql groupBy orderByAsc orderByDesc orderBy having func or and nested apply last exists notExists 等
Update类型的Wrapper独有操作方法:set setSql
Query类型的Wrapper独有操作方法:select
更新-Wrapper
更新类型的Wrapper有2个
UpdateWrapper 与 LambdaUpdateWrapper 这2个Wrapper都是用于对数据修改的条件构造器。
需求:将id=1的员工name修改为zhangsanfeng
UpdateWrapper
@Test
public void testUpdate(){
UpdateWrapper<Employee> wrapper = new UpdateWrapper<>();
wrapper.eq("id", 1L);
wrapper.set("name", "zhangsanfeng");
employeeMapper.update(null, wrapper);
}
独有方法操作
需求1其实还是一种写法,使用到:setSql
@Test
public void testUpdate(){
UpdateWrapper<Employee> wrapper = new UpdateWrapper<>();
wrapper.eq("id", 1L);
//wrapper.set("name", "zhangsanfeng");
wrapper.setSql("name='zhangsanfeng'");
employeeMapper.update(null, wrapper);
}
setSql表示使用拼接SQL片段方式进行操作,区别从打印的SQL可以看出
-- set方式:
UPDATE employee SET name=? WHERE (id = ?)
-- 参数:
zhangsanfeng(String), 1(Long)
-- setSql方式:
UPDATE employee SET name='zhangsanfeng' WHERE (id = ?)
-- 参数:
1(Long)
2个对比,可以很明显看出set方式使用预编译操作方法,而setSql 直接进行拼接,可能存sql注入风险,不建议使用,操作上更推荐set方式。
问题
从上面更新代码上看,开发存在一定隐患,设置条件/更新的列都是直接使用字符串,如果手误写错,编译期是无法检查的,需要等执行之后才会出现异常。存在一定瑕疵。怎办,改进解决方案就是:LambdaUpdateWrapper
LambdaUpdateWrapper
@Test
public void testUpdateLambda(){
LambdaUpdateWrapper<Employee> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Employee::getId, 1L);
wrapper.set(Employee::getName, "zhangsanfeng");
employeeMapper.update(null, wrapper);
}
LambdaUpdateWrapper 的update 跟 UpdateWrapper 的update操作一样,唯一区别点在于UpdateWrapper的操作列都是以字符串的形式存在,LambdaUpdateWrapper 的操作列使用lambda表达式。
咋一看,感觉高大山,点开查看源码,你发现:Employee::getName其实最终还是会解析出name属性来。简单的理解就是通过Employee类中的getName方法,将name属性解析出来。这么折腾好处是比直接写字符串方式多了一种操作前检查(避免出现手误)
@Override
public LambdaUpdateWrapper<T> set(boolean condition, SFunction<T, ?> column, Object val) {
if (condition) {
sqlSet.add(String.format("%s=%s", columnToString(column), formatSql("{0}", val)));
}
return typedThis;
}
protected String columnToString(SFunction<T, ?> column, boolean onlyColumn) {
return getColumn(LambdaUtils.resolve(column), onlyColumn);
}
private String getColumn(SerializedLambda lambda, boolean onlyColumn) {
Class<?> aClass = lambda.getInstantiatedType();
tryInitCache(aClass);
String fieldName = PropertyNamer.methodToProperty(lambda.getImplMethodName());
ColumnCache columnCache = getColumnCache(fieldName, aClass);
return onlyColumn ? columnCache.getColumn() : columnCache.getColumnSelect();
}
查询-Wrapper
查询类型的Wrapper有2个
**QueryWrapper ** 与 LambdaQueryWrapper 这2个Wrapper都是用于对数据修改的条件构造器。
需求:查询name=zhangsan, age=18 的员工信息
QueryWrapper
@Test
public void testQuery(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("name", "zhangsan");
wrapper.eq("age", 18);
List<Employee> list = employeeMapper.selectList(wrapper);
}
独有方法操作
query类型wrapper也有自己的独有的方法:select
需求:查询name=zhangsan, age=18 的员工信息,只需要查询id, name 2列
@Test
public void testQuery(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select("id", "name");
wrapper.eq("name", "zhangsan");
wrapper.eq("age", 18);
List<Employee> list = employeeMapper.selectList(wrapper);
}
里面的wrapper.select(“id”, “name”); 方法表示查询结果返回id,name 2列,等价于:
SELECT id,name FROM employee WHERE (name = ? AND age = ?)
-- 如果不加select 方法,默认查询所有列,等价于:
SELECT * FROM employee WHERE (name = ? AND age = ?)
问题
同UpdateWrapper
LambdaQueryWrapper
@Test
public void testQueryLambda(){
LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Employee::getName, "zhangsan");
wrapper.eq(Employee::getAge, 18);
List<Employee> list = employeeMapper.selectList(wrapper);
}
2个Wrapper操作跟上面update操作一个样,都是将字符串的操作列转换成lambda方式,其实没有本质上的区别。
构建Wrapper实例
@Test
public void testWrapper(){
//wrapper对象的创建
//query
QueryWrapper<Employee> queryWrapper1 = new QueryWrapper<>();
QueryWrapper<Employee> queryWrapper2 = Wrappers.<Employee>query();
LambdaQueryWrapper<Employee> queryWrapper3 = new LambdaQueryWrapper<>();
LambdaQueryWrapper<Employee> queryWrapper4 = Wrappers.<Employee>lambdaQuery();
LambdaQueryWrapper<Employee> queryWrapper5 = queryWrapper1.lambda();
//update
UpdateWrapper<Employee> updateWrapper1 = new UpdateWrapper<>();
UpdateWrapper<Employee> updateWrapper2 = Wrappers.<Employee>update();
LambdaUpdateWrapper<Employee> updateWrapper3 = new LambdaUpdateWrapper<>();
LambdaUpdateWrapper<Employee> updateWrapper4 = Wrappers.<Employee>lambdaUpdate();
LambdaUpdateWrapper<Employee> updateWrapper5 = updateWrapper1.lambda();
}
其中的Wrappers 是mybatis-plus官方提供的构建Wrapper实例的工具类。
Wrapper选择
单从可读性来看,建议使用传统的Wrapper,从预防手误来看,建议使用LambadWrapper
实际开发以公司规定为准则。
Wrapper练习
将之前写的CRUD操作转换成LambadWrapper方法
MyBatis-Plus条件查询
概要
1> 前一章节重点介绍了MyBatis-Plus中的wrapper体系与操作方式,本章节重点讲的wrapper中的query操作,以QueryWrapper 实例为切入点,讲解常用的条件查询,UpdateWrapper涉及到的条件同理可得即可。
2>一样沿用前几篇使用的employee 表/实体/mapper等代码
@Setter
@Getter
@ToString
@TableName("employee")
public class Employee {
@TableId(value = "id", type= IdType.AUTO)
private Long id;
private String name;
private String password;
private String email;
private int age;
private int admin;
private Long deptId;
}
列投影-select
select重载的方法有3个,其实就2个,一个功能重复了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CuSCOb5a-1693130976151)(images/image-20211216173125662.png)]
用法:从查询结果集中挑选指定列
需求:查询所有员工信息,返回员工的age跟name属性
select(String…)
@Test
public void testQuery1() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select("name", "age"); //列的投影, 挑选哪一些列, 参数是列名
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
SELECT name,age FROM employee
使用注意,如果列使用别名,那就按照sql语法编写别名,换句话讲select里面参数其实就是sql中的select语句,语法一样
wrapper.select("name as ename", "age as eage");
sql效果
SELECT name as ename,age as eage FROM employee
还有简便的写法:sql片段方式
@Test
public void testQuery1() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
//wrapper.select("name", "age"); //列的投影, 挑选哪一些列, 参数是列名
wrapper.select("name, age"); //参数是sql 片段
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
注意,这种写法参数为sql片接,容易出现sql注入风险
select(Class entityClass, Predicate predicate)
这个方法理解起来相对麻烦
enittyClass: 表示指定查询实体对象,比如:当前操作员工表,那么指定Employee.class
predicate:判断型函数接口,接口有个test方法,参数是TableFieldInfo
TableFieldInfo:表字段信息对象,将表的列抽象成java对象
方法意思:指定查询对象,使用predicate定义条件列的规则,满足条件的列,挑选出来。
需求:查询列名以 字母 “e” 结尾的列
@Test
public void testQuery1_1() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select(Employee.class, tableFieldInfo -> tableFieldInfo.getColumn().endsWith("e"));
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
SELECT id,name,age FROM employee
查询出来的列有id,name, age。 id是默认查询,name 跟 age 列都有e字母,所以能查询出来
需求2:查询列名长度大于 5的列
@Test
public void testQuery1_1() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select(Employee.class, tableFieldInfo -> tableFieldInfo.getColumn().length() > 5);
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
SELECT id,password,dept_id FROM employee
排序
MyBatis-Plus的排序有8个,具体分为3种:orderByAsc / orderByDesc / orderBy
orderByAsc : 正序排 3个
orderByDesc : 倒序排 3个
orderBy:1个
用法:对查询结果集排序,可以单列排,可以多列排
orderByAsc(String column)/orderByDesc(String column) : 单列正排序/倒排序
需求:查询所有员工信息,按age正序排/倒序排
@Test
public void testQuery2() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.orderByAsc("age"); //正序
//wrapper.orderByDesc("age"); //倒序
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
-- 正序
SELECT id,name,password,email,age,admin,dept_id FROM employee ORDER BY age ASC
-- 倒序
SELECT id,name,password,email,age,admin,dept_id FROM employee ORDER BY age DESC
需求:查询所有员工信息,按age正序排, 如果age 一样,按id正序排
需求:查询所有员工信息,按age正序排, 如果age 一样,按id倒序排
@Test
public void testQuery2() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.orderByAsc("age");
wrapper.orderByAsc("id"); //正序
//wrapper.orderByDesc("id"); //倒序
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
-- 先正序后正序
SELECT id,name,password,email,age,admin,dept_id FROM employee ORDER BY age ASC,id ASC
-- 先正序后倒序
SELECT id,name,password,email,age,admin,dept_id FROM employee ORDER BY age ASC,id DESC
这里注意,排序列谁先谁后是根据orderBy方法调用顺序决定的,操作时务必小心
orderByAsc(String… column)/orderByDesc(String… column) : 多列正排序/倒排序
需求:查询所有员工信息,按age正序排, 如果age 一样,按id正序排
需求:查询所有员工信息,按age倒序排, 如果age 一样,按id倒序排
@Test
public void testQuery2() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.orderByAsc("age", "id"); //都正序
//wrapper.orderByDesc("age", "id"); //都倒序
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
-- 都是正序
SELECT id,name,password,email,age,admin,dept_id FROM employee ORDER BY age ASC,id ASC
-- 都是倒序
SELECT id,name,password,email,age,admin,dept_id FROM employee ORDER BY age DESC,id DESC
从上面也可以看出多列排序其实跟单列排序本质一样没啥区别
orderByAsc(boolean condition, String… column)/orderByDesc(boolean condition, String… column) : 带条件判断多列正排序/倒排序
condition:排序控制开关,当condition这个参数为true时,才会拼接sql排序语句
需求:查询所有员工信息,按age正序排
@Test
public void testQuery2_1() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.orderByAsc(true, "age"); //true
//wrapper.orderByAsc(false, "age"); //false
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行完SQL
-- true
SELECT id,name,password,email,age,admin,dept_id FROM employee ORDER BY age ASC
-- false
SELECT id,name,password,email,age,admin,dept_id FROM employee
orderBy(boolean condition, boolean isAsc,String… columns) : 带条件判断多列排序
condition: 当condition这个参数为true时,才对sql语句进行排序操作
isAsc:是否为正序排, true:表示正序, false:表示倒序
columns:排序的列,可以多列,可单列
需求:查询所有员工信息,按age正序排
需求:查询所有员工信息,按age倒序排
@Test
public void testQuery2_2() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.orderBy(true, true, orderBy); //正序
wrapper.orderBy(true, false, orderBy); //倒序
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行完SQL
-- 正序
SELECT id,name,password,email,age,admin,dept_id FROM employee ORDER BY age ASC
-- 倒序
SELECT id,name,password,email,age,admin,dept_id FROM employee ORDER BY age DESC
比较运算
allEq
全等比较符,它重载方法有6个,功能由传入的参数决定
用法:全等,所有条件必须相等,条件使用Map<String, Object>封装, key:条件列, value:条件值
需求:查询name=dafei,age=18的员工信息
allEq(Map<String, V> params)
@Test
public void testQuery3() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
Map<String, Object> map = new HashMap<>();
map.put("name", "dafei");
map.put("age", 18);
wrapper.allEq(map);
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (name = ? AND age = ?)
allEq(Map<String, V> params, boolean null2IsNull)
null2IsNull:如果params参数中,如果某个key对应的value值为null时,
true:表示使用is null 替换, false:表示忽略该条件
@Test
public void testQuery3_1() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
Map<String, Object> map = new HashMap<>();
map.put("name", null);
map.put("age", 18); //value值为null
//wrapper.allEq(map, true); //拼接
wrapper.allEq(map, false); //不拼接
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
-- true 拼接
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (name IS NULL AND age = ?)
-- false 不拼接
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (age = ?)
allEq(BiPredicate<String, V> filter, Map<String, V> params)
filter:判断型函数接口,用于过滤params中的条件,接口有个test方法
需求:查询满足指定params条件的员工数据,附加条件, 如果params的key长度小于4,不参与sql条件查询
@Test
public void testQuery3_2() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
Map<String, Object> map = new HashMap<>();
map.put("name", "dafei");
map.put("age", 18);
wrapper.allEq((key, value)->{
return key.length() > 4;
}, map);
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (name = ?)
params参数key值有age, 跟name, 函数接口做了限制,key长度必须大于4, 所以age被排除,符合要求只有name这个条件。sql值拼接name条件
allEq(boolean condition, Map<String, V> params, boolean null2IsNull)
condition:allEq控制开关, 当condition为true,执行allEq语法,拼接查询条件, 为false, 不执行。
allEq(boolean condition, BiPredicate<String, V> filter, Map<String, V> params, boolean null2IsNull);
跟上面介绍重复了,同理可得即可。
eq
重载方法有2个
用法:等值条件过滤, sql:where 列 = 值
eq(String column, Object value):等值匹配
需求:查询name=dafei的员工信息
@Test
public void testQuery4_1() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("name", "dafei");
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (name = ?)
eq(boolean condition, String column, Object value):带开关的等值匹配
带开关的eq 操作, 使用跟上面操作一样
ne
重载方法有2个
用法:不等条件过滤, sql: where 列 <> 值
ne(String column, Object value):不等值匹配
需求: 查询name != dafei的用户信息
@Test
public void testQuery4_2() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.ne("name", "dafei");
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (name <> ?)
ne(boolean condition, String column, Object value):带开关的不等值匹配
带开关的ne 操作, 使用跟上面操作一样
gt/ge
gt有2个重载方法, ge也有2个重载方法
用法gt : great than 大于, sql: where 列 > 值
用法ge:greate than and equals, 大于等于, sql: where 列 >= 值
gt(String column, Obejct value) / ge(String column, Obejct value):大于/大于等于比较
需求:查询 age 大于 18 的员工信息
需求:查询age 大于等于 18 的员工信息
@Test
public void testQuery4_3() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.gt("age", 18); //大于 18
//wrapper.ge("age", 18); //大于等于18
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
-- gt
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (age > ?)
-- ge
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (age >= ?)
gt(boolean condition, String column, Obejct value) / ge(boolean condition, String column, Obejct value):带开关的大于/大于等于比较
带开关的gt/ ge 操作, 使用跟上面操作一样
lt/le
lt有2个重载方法, le也有2个重载方法
用法lt : less than 小于, sql: where 列 < 值
用法le:less than and equals, 小于等于, sql: where 列 <= 值
lt(String column, Obejct value) / le(String column, Obejct value):小于/小于等于比较
需求:查询 age 小于 18 的员工信息
需求:查询age 小于等于 18 的员工信息
@Test
public void testQuery4_4() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.lt("age", 18); //小于 18
//wrapper.le("age", 18); //小于等于18
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
-- lt
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (age < ?)
-- le
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (age <= ?)
lt(boolean condition, String column, Obejct value) / le(boolean condition, String column, Obejct value):带开关的小于/小于等于比较
带开关的lt/ le 操作, 使用跟上面操作一样
isNull/isNotNull
isNull 重载2个方法, isNotNull重载2个方法
用法isNull : 列判null条件, sql: where 列 is null
用法isNotNull:列判定不为null条件, sql: where 列 is not null
isNull(String column, Obejct value) / isNotNull(String column, Obejct value):列是否为null/不为null判断
需求:查询 dept_id 为null 的员工信息
需求:查询 dept_id 不为null 的员工信息
@Test
public void testQuery4() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.isNull("dept_id"); // 为null
//wrapper.isNotNull("dept_id"); // 不为null
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
执行后SQL
-- is null
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (dept_id is null)
-- is not null
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (dept_id is not null)
isNull(boolean condition, String column, Obejct value) / isNotNull(Boolean condition, String column, Obejct value):带开关的列是否为null/不为null判断
带开关的null/ isNotNull 操作, 使用跟上面操作一样
in/notIn
in重载4个方法, notIn重载4个方法
用法in: 列在指定列表数据中, sql: where 列 in (值1,值2,值3…)
用法notIn: 列不在指定列表数据中, sql: where 列 not in (值1,值2,值3…)
in(String column, Obejct…value) / notIn(String column, Obejct… value):可变参数方式
需求:查询 id = 1 或者 id = 2 或者 id = 3 的员工信息
需求:查询 id != 1 或者 id != 2 或者 id != 3 的员工信息
@Test
public void testQuery5() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.in("id",1L, 2L, 3L); // in
//wrapper.notIn("id",1L, 2L, 3L); //not in
List<Employee> list = employeeMapper.selectList(wrapper);
}
执行后SQL
-- in
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (id IN (?,?,?))
-- not in
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (id NOT IN (?,?,?))
in(String column, Collection<?> coll) / notIn(String column, Collection<!> coll):集合方式
@Test
public void testQuery5() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.in("id", Arrays.asList(1L, 2L, 3L)); //in
//wrapper.notIn("id",Arrays.asList(1L, 2L, 3L)); //not in
List<Employee> list = employeeMapper.selectList(wrapper);
}
执行后SQL
跟上面操作一模一样
in(boolean condition, String column, Obejct…value) / notIn(boolean condition, String column, Obejct… value):可变参数方式
in(boolean condition, String column, Collection<!> coll) / notIn(boolean condition, String column, Collection<!> coll):数组的方式
带开关的in/ notIn 操作, 使用跟上面操作一样
inSql/notInSql
inSql重载2个方法, notInSql重载2个方法
用法inSql: 列在指定列表数据中, sql: where 列 in (值1,值2,值3…)
用法notInSql: 列不在指定列表数据中, sql: where 列 not in (值1,值2,值3…)
inSql(String column,String value) / notInSql(String column, String value)
跟之前in /notIn 方法的区别是,方法参数value是一个sql片段
需求:查询 id = 1 或者 id = 2 或者 id = 3 的员工信息
需求:查询 id != 1 或者 id != 2 或者 id != 3 的员工信息
@Test
public void testQuery5() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.inSql("id","1, 2, 3"); // in,value是sql片段
//wrapper.notInSql("id","1, 2, 3"); //not in
List<Employee> list = employeeMapper.selectList(wrapper);
}
执行后SQL
-- in sql
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (id IN (1,2,3))
-- not in sql
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (id NOT IN (1,2,3))
inSql(boolean condition, String column, String value) / notInSql(boolean condition, String column, String value)
带开关的inSql/ notInSql 操作, 使用跟上面操作一样
模糊查询
like/notLike/likeLeft/likeRight
like 相关的方法有8个
用法:模糊查询,sql:
like: where 列 like “%值%”
notLike:where 列 not like “%值%”
likeLeft:where lie like “%值”
likeRight:where lie like “值%”
需求:查询名字中含有 “ye” 字样的员工信息 like
需求:查询名字中不含有 “ye” 字样的员工信息 notLikie
需求:查询名字中以 “ye” 字样结尾员工信息 likeLeft
需求:查询名字中以 “ye” 字样开头员工信息 likeRight
@Test
public void testQuery6() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.like("name", "ye"); // like "%ye%"
//wrapper.notLike("name", "ye"); // not like "%ye%"
//wrapper.likeLeft("name", "ye"); // like "%ye"
//wrapper.likeRight("name", "ye"); // like "fei%"
List<Employee> list = employeeMapper.selectList(wrapper);
}
执行后SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (name LIKE ?)
-- "%ye%" -- like
-- "%ye" -- likeLeft
-- "ye%" -- likeRight
%%
where name like _a; //代表name列满足2个字符串,第二字符是a字样
逻辑运算
and/or/nested
and
用法:逻辑与, sql: where 条件1 and 条件2
需求:查询年龄介于18~30岁的员工信息
@Test
public void testQuery8() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
//多个条件默认使用and
wrapper.ge("age", 18).le("age", 30);
List<Employee> list = employeeMapper.selectList(wrapper);
}
执行后SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (age >= ? AND age <= ?)
条件的链式连接,默认是使用and进行连接
or
用法:逻辑或, sql: where 条件1 or 条件2
需求:查询年龄小于18或大于30岁的员工信息
@Test
public void testQuery8() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
//多个条件使用or
wrapper.lt("age", 18)
.or()
.gt("age", 30);
List<Employee> list = employeeMapper.selectList(wrapper);
}
执行后SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (age < ? OR age > ?)
多个条件连接默认使用and拼接,如果是or操作需要明确调用or方法。
逻辑条件嵌套
需求:查询名字带有"ye"字样,或者年龄介于18~30岁的员工信息
@Test
public void testQuery10() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.like("name", "ye")
.or(wr -> wr.le("age", 30).ge("age", 18));
List<Employee> list = employeeMapper.selectList(wrapper);
}
执行后SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (name LIKE ? OR (age <= ? AND age >= ?))
需求:查询名字带有"ye"字样,并且年龄小于18或大于30岁的员工信息
@Test
public void testQuery11() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.like("name", "ye")
.and(wr -> wr.lt("age", 18).or().gt("age", 30));
List<Employee> list = employeeMapper.selectList(wrapper);
}
执行后SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE (name LIKE ? AND (age < ? OR age > ?))
nested
逻辑条件嵌套,等价于sqlwhere添加中的小括号,可以改变条件优先级。
需求:查询名字带有"ye"字样,或者年龄介于18~30岁的员工信息
@Test
public void testQuery10() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.like("name", "ye").or()
.nested(wr -> wr.le("age", 30).ge("age", 18));
List<Employee> list = employeeMapper.selectList(wrapper);
}
需求:查询名字带有"ye"字样,并且年龄小于18或大于30岁的员工信息
@Test
public void testQuery11() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.like("name", "ye")
.nested(wr -> wr.lt("age", 18).or().gt("age", 30));
List<Employee> list = employeeMapper.selectList(wrapper);
}
分组查询
group by/having
group by
重载的方法有3个
分组函数,指定列进行封装,可以多个也可以1个
having
重置方法2个
用于筛选分组之后条件数据
*
用法:分组查询, sql: group by 列 having 条件
需求:查询每个部门员工个数
@Test
public void testQuery12() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select("dept_id", "count(id) count");
wrapper.groupBy("dept_id");
List<Map<String, Object>> mapList = employeeMapper.selectMaps(wrapper);
mapList.forEach(System.out::println);
}
执行后SQL
SELECT dept_id,count(id) count FROM employee GROUP BY dept_id
需求:查询每个部门员工个数,筛选出个数大于3的部门
@Test
public void testQuery13() {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select("dept_id", "count(id) count");
wrapper.groupBy("dept_id");
//wrapper.having("count > 3");
wrapper.having("count > {0}", 3);
List<Map<String, Object>> mapList = employeeMapper.selectMaps(wrapper);
mapList.forEach(System.out::println);
}
执行后SQL
-- count > 3
SELECT dept_id,count(id) count FROM employee GROUP BY dept_id HAVING count > 3
-- count > {0}
SELECT dept_id,count(id) count FROM employee GROUP BY dept_id HAVING count > ?
自定义SQL
上面的wrapper查询,针对简单sql场景非常简便,但是如果业务相对复杂,需要要求更灵活的SQL时(比如多表关联查询,动态sql等),wrapper有点力不从心了,此时可以使用自定义sql方式。这种方式不是啥新鲜货,其实就是还原之前的Mybatis的XxxxMapper.xml写法。
Mapper接口
public interface EmployeeMapper extends BaseMapper<Employee> {
List<Employee> listByXmlSingle(); //自定义方法
}
Mapper.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" >
<mapper namespace="com.langfeiyes.mp.mapper.EmployeeMapper" >
<resultMap id="BaseResultMap" type="com.langfeiyes.mp.domain.Employee" >
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="email" jdbcType="VARCHAR" property="email" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="admin" jdbcType="BIT" property="admin" />
<result column="dept_id" property="deptId" />
</resultMap>
<select id="listByXmlSingle" resultMap="BaseResultMap">
select id, name, password, email, age, admin, dept_id
from employee
</select>
</mapper>
测试
@Test
public void testQuery14() {
List<Employee> list = employeeMapper.listByXmlSingle();
}
到这常用的条件方法就介绍到这,剩下的条件方法同理即可,技巧就是sql的关键语法就是Wrapper对象的方法。
MyBatis-Plus关联关系
关系型数据对象与对象间以下几种关联关系
一对一关系
A表一条数据只能映射到B表唯一一条数据
以QQ号与QQ空间为例子:一个QQ号对应一个QQ空间
表设计:
关系维护:同id原则,公用id
对象设计:
@Data
public class QQ {
private Long id;
private String num;
private Zone zone; //维护关系由主表维护
}
@Data
class Zone {
private Long id;
private String url;
}
MyBatis
在做查询时,mybatis的写法
<resultMap id="BaseResultMap" type="com.langfeiyes.mp.domain.QQ" >
<id column="id" jdbcType="BIGINT" property="id" />
<result column="num" jdbcType="VARCHAR" property="num" />
<result column="z_id" property="zone.id" />
<result column="z_url" property="zone.url" />
</resultMap>
<select id="selectSingle" resultMap="BaseResultMap">
select q.id, q.num, z.id z_id, z.url z_url from qq q left join zone z on q.id = z.id
</select>
MyBatis-Plus
MyBatis-Plus使用额外sql方式进行进行查询
@Test
public void testQuery10() {
QueryWrapper<QQ> wrapper = new QueryWrapper<>();
List<QQ> list = QQMapper.selectList(wrapper); //先查素有QQ对象
for(QQ q : list){
q.setZone(zoneMapper.selectById(q.getId())); //通过QQ对象带有的zoneid查询zone对象(id一样)
}
}
一对多关系
A表一条数据可以映射B表多条数据
以部门与员工为例子:一个部门可以对应多个员工, 此时主角是部门
表设计:
关系维护:在多方(员工)设置外键,引致部门主键
对象设计:
@Data
class Department {
private Long id;
private String dname;
//1对多关系,一方为主导,对象中关系维护交给一方
private List<Employee> es = new ArrayList<>();
}
@Data
class Employee {
private Long id;
private String ename;
}
MyBatis
一堆多关系查询,需要使用Collection进行额外配置
<resultMap id="BaseResultMap" type="com.langfeiyes.mp.domain.Department" >
<id column="id" jdbcType="BIGINT" property="id" />
<result column="dname" jdbcType="VARCHAR" property="dname" />
<collection property="es" ofType="com.langfeiyes.mp.domain.Employee">
<id column="e_id" property="id"/>
<result column="e_name" property="ename"/>
</collection>
</resultMap>
<select id="selectList" resultMap="BaseResultMap">
select d.id, d.dname, e.id e_id, e.ename e_name
from department d left join employee e on d.id = e.deptid
</select>
MyBatis-Plus
MyBatis-Plus使用额外sql方式进行进行查询
@Test
public void testQuery10() {
QueryWrapper<Department> wrapper = new QueryWrapper<>();
List<Department> list = departmentMapper.selectList(wrapper); //先查部门对象集合
for(Department d : list){
d.setEs(employeeMapper.selectByDeptId(d.getId())); //通过部门id查询员工集合
}
}
多对一关系
跟一对多关系一样,区分在所站的角度。
以部门与员工为例子:多个员工属于一个部门, 此时主角是员工
表设计:
关系维护:在多方(员工)设置外键,引致部门主键
对象设计:
@Data
class Department {
private Long id;
private String dname;
}
@Data
class Employee {
private Long id;
private String ename;
//多对1关系,多方为主导,对象中关系维护交给多方
private Department dept;
}
多对多关系
A表的一条数据可以映射B表多条数据,B表的一条数据也可以映射A表多条数据
以老师与学生为例子:一个老师可以教多个学生,一个学生可以给多个老师教
表设计:
关系维护:设计中间表,使用额外的表存放多对多关系映射id
对象设计:
@Data
class Student {
private Long id;
private String sname;
//多对多关系,核心对象为主导,对象中关系维护交它维护
private List<Teacher> ts = new ArrayList<>();
}
@Data
class Teacher {
private Long id;
private String tname;
}
上面面多对一,多对多操作跟之前的一对一,一堆多操作,大同小异,这里不展开讲了。
总之,MyBatis-Plus 对多表操作并不是很友好,只支持额外SQL查询方式,开发中如果复杂的SQL建议使用xml的方式。MyBatis-Plus无缝兼容Mybatis,因为:MyBatis-Plus只做增强, 不做修改。
MyBatis-PlusAR模式
概念
百度百科
Active Record(活动记录),是一种领域模型模式,特点是一个实体类对应关系型数据库中的一张表,而实体类实例对应表中的数据。ActiveRecord 严格遵循标准的 ORM 模型:表映射到实体对象,列映射到实体对象属性。
Active Record规定
一张表对应一个实体类,类的每一个对象实例对应于表中一行记录;
ActiveRecord 本身是一个实体类,同时实现持久化api,可自己实现CRUD逻辑
实现
MyBatis-Plus支持Active Record操作模式, 这里以部门对象为了例子讲解。
步骤1:创建部门数据库表
CREATE TABLE `department` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`sn` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
步骤2:编写部门实体对象
@Setter
@Getter
@TableName("department")
public class Department extends Model<Department> {
@TableId(value = "id", type= IdType.AUTO)
private Long id;
private String name;
private String sn;
}
这里必须强调, 实现AR模式需要2个前提之一:
实体对象必须继承核心类:Model, 并且明确指定操作的实体类泛型
步骤3:编写部门实体对象对应的mapper接口
public interface DepartmentMapper extends BaseMapper<Department> {
}
实现AR模式需要2个前提之二:
必须提供实体对象对应的Mapper接口
步骤4:编写测试类
@SpringBootTest
public class ARTest {
@Test
public void testInsert(){
Department department = new Department();
department.setName("小卖部");
department.setSn("xmb");
department.insert();
}
@Test
public void testDelete(){
Department department = new Department();
department.deleteById(1L);
}
@Test
public void testUpdate(){
Department department = new Department();
department.setName("小卖部2");
department.setSn("xmb2");
department.setId(1L);
department.updateById();
}
@Test
public void testGet(){
Department department = new Department();
System.out.println(department.selectById(1L));
}
@Test
public void testList(){
Department department = new Department();
department.selectAll();
}
}
常用API
打开Model类,查看源码,发现AR执行以下CRUD方法
添加:2个, 删除:3个,更新:2个,查询:7个
操作上跟BaseMapper接口的api一样,这里就不展开讲了。
原理
debug 下面代码
@Test
public void testGet(){
Department department = new Department();
System.out.println(department.selectById(1L));
}
点进去selectById方法
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
public T selectById(Serializable id) {
//获取操作sql的SqlSession对象
SqlSession sqlSession = sqlSession();
try {
//使用sqlsession执行对应sql,此处的sqlSession是Mybatis的sqlSession
//只需要拼接sql即可执行sql语句, 这里的sqlStatement就是拼接sql用的内部方法
return sqlSession.selectOne(sqlStatement(SqlMethod.SELECT_BY_ID), id);
} finally {
closeSqlSession(sqlSession);
}
}
/**
* 获取SqlStatement
*
* @param sqlMethod sqlMethod
*/
protected String sqlStatement(SqlMethod sqlMethod) {
return sqlStatement(sqlMethod.getMethod());
}
/**
* 获取SqlStatement
*
* @param sqlMethod sqlMethod
*/
protected String sqlStatement(String sqlMethod) {
//无法确定对应的mapper,只能用注入时候绑定的了。
return SqlHelper.table(getClass()).getSqlStatement(sqlMethod);
}
结合上面的源码, AR模式其实很简单,理由调用的方法取拼接对应的sql,然后通过sql直接调用的Mybatis中sqlSession执行即可。
MyBatis-Plus逻辑删除
概念
实际开发中,数据删除一般有2种选择:
1:物理删除
物理删除,也称为硬删除,指的是数据直接从数据库中移除,对应的SQL语句:DELETE FROM 表 where 条件,这种删除成功后,数据就无法再恢复啦。
2:逻辑删除
逻辑删除,也称为软删除,数据并没有真正删除,而是通过设置数据状态是否可显示,后续查询进行状态过滤,从而隐藏数据显示以达到删除对应的效果。比如:设置is_delete 数据状态, 0表示正常, 1表示删除。 后续的查询sql 加上 where is_delete = 0 就可以过滤删除的数据。
一般开发选用的是逻辑删除,原因有2方面,一是项目数据非常重要不能随意删除,一是项目运行产生数据一般不会是独立,它可能会通过外键形式与其他数据产生关联,如果贸然删除该数据会引起系统不稳定,甚至异常。
举个例子:比如一个用户注册之后,进行发帖,留言,点评,收藏点赞等一些操作,会在系统相关表留下该用户相关信息,如果用户销户,使用物理删除方式删除该用户数据,那么之前与用户产生关联数据表都出现脏数据啦,这很可能导致系统bug。
局部逻辑删除
MyBatis-Plus也支持逻辑删除,分别是:局部逻辑删除,全局逻辑删除
具体实现步骤如下:
步骤1:在employee表添加一个del列,类型是int
步骤2:修改Employee实体类
public class Employee {
//省略其他字段
@TableLogic(value = "1", delval = "0")
private int del;
}
@TableLogic
作用:逻辑删除注解,一般开发不建议硬删除数据(从数据库删除),建议使用软删除(数据不删除仅仅改数据状态,列表时做数据过滤)
核心属性:value, delval
value:表示未删除的数据状态
delval:表示删除之后的数据状态
步骤3:测试
@Test
public void testDelete(){
employeeMapper.deleteById(1L);
}
原先删除语法被转行成更新语法:
UPDATE employee SET del=0 WHERE id=? AND del=1
普通列表查询
@Test
public void testselectById(){
Employee employee = employeeMapper.selectById(1L);
}
执行SQL
SELECT id,name,password,email,age,admin,dept_id FROM employee WHERE id=? and del = 1
上面操作属于局部逻辑删除,针对是Employee对象,如果其他表也需要逻辑删除怎办?可以全局逻辑删除
全局逻辑删除
步骤1:在employee表添加一个del列,类型是int
步骤2:修改Employee实体类
public class Employee {
//省略其他字段
//@TableLogic(value = "1", delval = "0")
//此处不需要配置注解,在全局配置文件直接指定
private int del;
}
步骤3:配置主配置文件:application.yml
mybatis-plus:
global-config:
db-config:
logic-delete-field: del # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
如果是application.properties文件
# 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
mybatis-plus.global-config.db-config.logic-delete-field=del
# 逻辑已删除值(默认为 1)
mybatis-plus.global-config.db-config.logic-delete-value=0
# 逻辑未删除值(默认为 0)
mybatis-plus.global-config.db-config.logic-not-delete-value=1
步骤4:测试
跟之前操作一样。
全局逻辑删除需要注意,必须在所有表上统一加上del字段,要不无法实现。
MyBatis-Plus乐观锁
概念
多事务环境下如何保证数据库操作安全呢?常用的一种解决方案就是对操作数据表进行加锁处理。根据实现思路不同分:悲观锁与乐观锁2种。
悲观锁:悲观的认为多事务操作同一数据是及其不安全的,所以A事务在操作数据时,其他任何事务不允许对该数据进行修改,只能等待A事务操作结束后才可以执行。
乐观锁:乐观的认为A事务在操作数据时,期间不会有其他事务进行干扰,能顺利完成事务操作。
实现
悲观锁
悲观锁解决方案非常简单,直接在操作sql中加上for update 语句即可
select * from 表 where 条件列=值 for update
使用for update 操作,可以认为是给每次操作都加上表级别悲观锁,在事务没结束前,其他事务必须等待。
事务1
步骤1:启动事务
步骤2:使用悲观锁方式查询部门表(此时锁表)
步骤3:提交事务
事务2
在事务执行到步骤2时,执行下面逻辑,发现事务停滞,等待事务1commit之后才继续往下执行。
注意:并发环境下都不建议使用悲观锁,因为悲观锁容易锁表,导致事务等待,性能低下。
乐观锁
乐观锁操作跟悲观锁区别在于不对操作的数据表进行加锁,而是使用version字段去规避。
以部门表为例子
步骤1:给部门表添加version列,默认值为0
步骤2:实现乐观锁操作
需求: 修改id=1数据sn字段改为:kfb
1>先查询
select id, name, sn, version from department where id = 1;
得到该数据列version = 0
2>再修改, 注意version版本更新
update department set sn = "kfb", version = version + 1 where id = 1 and version = 0
这里有2个注意要点:
1:更新 sn字段的同时,version列数据也跟随变动
2:更新sql 除了基本的id=1条件外,多了version = 0 条件。
为什么要这么设计? 原因:乐观锁操作没有加锁,任意事务都可以同步操作,多事务同时操作,总有一个先成功,一成功则修改了version字段值,其他事务version 条件自然就不上啦,update失败。
步骤3:判断更新是否成功
乐观锁判断是否更新成功,就看执行update语句之后,返回的影响数据行数,如果行数大于0表示修改成功,如果行数等于0表示修改失败,放弃这次修改,一切重来。直到修改成功为止。
MyBatis-Plus版乐观锁
MyBatis-Plus 使用**@Version**注解实现乐观锁
还是以部门表为例
步骤1:在部门表添加version字段
步骤2:部门实体对象添加version字段
public class Department {
//省略其他字段
@Version
private int version;
}
步骤3:配置类中配置支持拦截器乐观锁
@Bean
public MybatisPlusInterceptor optimisticLockerInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
步骤4:正常执行更新方法
@Test
public void testUpdate(){
//先查询
Department dept = departmentMapper.selectById(1L);
//再替换
dept.setId(1L);
dept.setName("小卖部");
dept.setSn("sell");
//最后更新
departmentMapper.updateById(dept);
}
执行后SQL
UPDATE department SET name=?, sn=?, version=? WHERE id=? AND version=?
MyBatis-Plus会自动讲乐观锁逻辑加载到sql中
使用Mybatis-Plus注意:
乐观锁支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
仅支持 updateById(id) 与 update(entity, wrapper) 方法
另外,每次操作前都是先查询,替换,再更新,否则乐观锁无效
MyBatis-Plus通用枚举
平常开发中,某些实体对象涉及到状态的变化,一般使用方式有以下2种:
以员工实体举例子
1>使用常量的方式
public class Employee {
public static final int STATUS_NORMAL = 0; //在职
public static final int STATUS_LEAVE = 1; //离职
//员工状态
private int status;
//省略其他属性
}
2>使用枚举方式
//员工状态枚举
@Getter
public enum EmployeeStatus {
NORMAL(0, "在职"), LEAVE(1,"离职");
private int status;
private String label;
private EmployeeStatus(int status, String label){
this.status = status;
this.label = label;
}
}
public class Employee {
//员工状态
private EmployeeStatus status = EmployeeStatus.NORMAL;
//省略其他属性
}
使用常理操作方式与枚举操作方式其实大同小异,枚举赋予状态更多的操作空间,比如对应的方法,对应的属性,灵活性更高。
MyBatis-Plus 也支持枚举字段操作
还是以员工操作为例子
步骤1:修改员工表,添加status列,类型为int
步骤2:创建EmployeeStatus 枚举类与添加status字段
方案1:实现IEnum接口:并提供getvalue()方法:
@Getter
public enum EmployeeStatus implements IEnum<Integer> {
NORMAL(0, "在职"), LEAVE(1,"离职");
private int status;
private String label;
private EmployeeStatus(int status, String label){
this.status = status;
this.label = label;
}
@Override
public Integer getValue() {
//在表status列中存什么值
return this.status;
}
}
方案2: 使用@EnumValue注解
@Getter
public enum EmployeeStatus{
NORMAL(0, "在职"), LEAVE(1,"离职");
@EnumValue //代表者执行SQL语句的时候,以这个字段的值作为添加到数据库中参数值
private int status;
private String label;
private EmployeeStatus(int status, String label){
this.status = status;
this.label = label;
}
}
使用注解方式替换接口的实现。
步骤3:在Employee对象添加status字段
public class Employee {
//员工状态
private EmployeeStatus status = EmployeeStatus.NORMAL;
//省略其他属性
}
步骤4:在application.properties 文件配置操作类
明确指定枚举类所有包路径
mybatis-plus.type-enums-package=com.langfeiyes.mp.enums
步骤5:测试
@Test
public void testSave(){
Employee employee = new Employee();
employee.setAdmin(1);
employee.setAge(18);
employee.setDeptId(1L);
employee.setEmail("zhangsan@163.com");
employee.setName("zhangsan");
employee.setPassword("111");
//执行状态
employee.setStatus(EmployeeStatus.LEAVE);
employeeMapper.insert(employee);
}
执行SQL
==> Preparing: INSERT INTO employee ( id, status, name, password, email, age, admin, dept_id, del, version ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: 1(Long), 1(Integer), zhangsan(String), 111(String), zhangsan@163.com(String), 18(Integer), 1(Integer), 1(Long), 0(Integer), 0(Integer)
MyBatis-Plus类型处理器
概念
类型处理器,用于 JavaType 与 JdbcType 之间的转换,简单的理解就是对象属性与列间的映射处理。
举个例子:员工对象在Mapper.xml中对象属性与列间映射
<resultMap id="BaseResultMap" type="com.langfeiyes.mp.Employee" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="name" property="name" jdbcType="VARCHAR" typeHandler="xxxTypeHanler"/>
<result column="phone" property="phone" jdbcType="VARCHAR" />
<result column="email" property="email" jdbcType="VARCHAR" />
<result column="status" property="status" jdbcType="CHAR" />
<result column="del" property="del" jdbcType="CHAR" />
</resultMap>
在没有显示指定具体类型处理器,那就是使用默认处理器,默认处理器可以处理简单类型映射:比如8大基本类型,时间,字符串等。但是,如果是相对复杂类型映射呢?
要实现复杂的类型映射,那么就需要使用自定义类型处理器,自定义映射规则。
实现
需求:员工表中有手机号码列,格式如下
转成员工对象contact集合
public class Employee {
private List<String> contact = new ArrayList<>();
}
很明显,varchar类型的contact 列是无法直接转换list集合,此时需要使用自定义类型处理器啦。
步骤1:自定义类型转换处理器
/**
* 将字符串转换成字符串数组
* eg: 13700000000,13700000001,13700000002, ---> List<String>
* 将字符串数组元素拼接字符串
* eg: List<String> ---> 13700000000,13700000001,13700000002,
*/
public class ListTypeHandler extends BaseTypeHandler<List<String>> {
//将list集合字符串元素使用,方式拼接,然后存到列中
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, List<String> list, JdbcType jdbcType) throws SQLException {
if(list != null && list.size() > 0){
StringBuilder sb = new StringBuilder(100);
for (String s : list) {
sb.append(s).append(",");
}
preparedStatement.setString(i, sb.toString());
}
}
//从数据库查询出列值,然后拆分,转换成字符串集合
@Override
public List<String> getNullableResult(ResultSet resultSet, String column) throws SQLException {
String value = resultSet.getString(column);
return this.parseToList(value);
}
//从数据库查询出列值,然后拆分,转换成字符串集合
@Override
public List<String> getNullableResult(ResultSet resultSet, int i) throws SQLException {
String value = resultSet.getString(i);
return this.parseToList(value);
}
//从数据库查询出列值,然后拆分,转换成字符串集合
@Override
public List<String> getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
String value = callableStatement.getString(i);
return this.parseToList(value);
}
//字符串截取转换成list集合
private List<String> parseToList(String value){
List<String> list = new ArrayList<>();
if(StringUtils.hasText(value)){
String[] sArr = value.split(",");
if(sArr.length > 0){
for (String s : sArr) {
if(StringUtils.hasText(s)){
list.add(s.trim());
}
}
}
}
return list;
}
}
步骤2:给员工表添加contact列,类型为varchar
步骤3:给Employee对象添加contact属性,类型是List
public class Employee {
//指定类型转换处理器
@TableField(typeHandler = ListTypeHandler.class)
private List<String> contact = new ArrayList<>();
}
步骤4:在application.properties配置文件中配置处理器所在包
mybatis-plus.type-handlers-package=com.langfeiyes.mp.handler
步骤5:添加与查询测试
@Test
public void testSave(){
Employee employee = new Employee();
employee.setAdmin(1);
employee.setAge(18);
employee.setDeptId(1L);
employee.setEmail("zhangsan@163.com");
employee.setName("zhangsan");
employee.setPassword("111");
//执行状态
employee.setStatus(EmployeeStatus.LEAVE);
//联系电话
employee.setContact(Arrays.asList("13700000000", "13700000001","13700000002"));
employeeMapper.insert(employee);
}
@Test
public void testGet(){
Employee employee = employeeMapper.selectById(1L);
System.out.println(employee.getContact());
}
MyBatis-Plus通用Service接口
概念
Java项目一般使用三层结构开发:
表现层:接收请求,调用业务方法处理请求,响应请求
业务层:也叫服务层,实现业务逻辑,调用持久层实现数据组合操作
持久层:完成数据的CRUD操作
前面讲的Mapper接口操作属于持久层,如果项目加入服务层,那代码该如何构建呢?
传统的业务层
使用MyBatis-Plus之前,传统业务层构建方式:以员工操作为例子
步骤1:构建员工业务层服务接口
public interface IEmployeeService {
void save(Employee employee);
void update(Employee employee);
void delete(Long id);
Employee get(Long id);
List<Employee> list();
}
步骤2:实现员工业务层接口
@Service
public class EmployeeServiceImpl implements IEmployeeService {
@Autowired
private EmployeeMapper mapper;
@Override
public void save(Employee employee) {
mapper.insert(employee);
}
@Override
public void update(Employee employee) {
mapper.updateById(employee); //必须全量更新
}
@Override
public void delete(Long id) {
mapper.deleteById(id);
}
@Override
public Employee get(Long id) {
return mapper.selectById(id);
}
@Override
public List<Employee> list() {
return mapper.selectList(null);
}
}
步骤3:实现服务方法测试
传统业务层服务方法需要自己调用Mapper接口方法去实现,相对麻烦。再看MyBatis-Plus提供简化操作
MyBatis-Plus业务层
步骤1:构建员工业务层服务接口
/**
*1>自定义一个业务服务接口:IEmployeeService,继承父接口:IService
*2>明确指定父接口泛型:当前接口操作实体对象:Employee
*/
public interface IEmployeeService extends IService<Employee> {
}
步骤2:实现员工业务层接口
/**
*1>定义服务接口实现类,实现IEmployeeService接口
*2>继承通用的父接口实现类:ServiceImpl
*3>明确指定通用的父接口实现类2个泛型:
* 1:当前服务类操作的实体对象对应的Mapper接口:EmployeeMapper
* 2:当前服务类操作的实体对象:Employee
*/
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {
}
步骤3:实现服务方法测试
@SpringBootTest
public class ServiceTest {
@Autowired
private IEmployeeService employeeService;
@Test
public void testSave(){
Employee employee = new Employee();
employee.setAdmin(1);
employee.setAge(18);
employee.setDeptId(1L);
employee.setEmail("zhangsan@163.com");
employee.setName("zhangsan");
employee.setPassword("111");
employeeService.save(employee);
}
@Test
public void testUpdate(){
Employee employee = new Employee();
employee.setId(1327139013313564673L);
employee.setAdmin(1);
employee.setAge(18);
employee.setDeptId(1L);
employee.setEmail("zhangsan@163.com");
employee.setName("zhangxiaosan");
employee.setPassword("111");
employeeService.updateById(employee);
}
@Test
public void testDelete(){
employeeService.removeById(11L);
}
@Test
public void testGet(){
System.out.println(employeeService.getById(11L));
}
@Test
public void testList(){
List<Employee> employees = employeeService.list();
employees.forEach(System.err::println);
}
}
常用服务层api
上面就是IService接口提供的实现方法,几乎涵盖了数据库常规操作。
方法解析
思考一个问题,IService接口是怎么实现的?
以IEmployeeService 接口中的getById() 方法为例子
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
default T getById(Serializable id) {
return getBaseMapper().selectById(id);
}
源码上,getById是一个接口默认方法,默认实现是调用getBaseMapper()对象去执行selectById方法
/**
* 获取对应 entity 的 BaseMapper
*
* @return BaseMapper
*/
BaseMapper<T> getBaseMapper();
getBaseMapper方法也是IService接口定义的方法,继续追踪其接口实现:
ServiceImpl类的getBaseMapper方法
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
@Autowired
protected M baseMapper;
@Override
public M getBaseMapper() {
return baseMapper;
}
}
从上面代码可以看到baseMapper是使用@Autowired 方式从spring容器中注入的,具体类型是泛型对象M, 而在定义EmployeeServiceImpl类时,具体代码:
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {
}
可以确认,以EmployeeServiceImpl为例子, 那么ServiceImpl中M的泛型就是EmployeeMapper类型,那么ServiceImpl可以等价
public class ServiceImpl<EmployeeMapper extends BaseMapper<T>, T> implements IService<T> {
@Autowired
protected EmployeeMapper baseMapper;
@Override
public EmployeeMapper getBaseMapper() {
return baseMapper;
}
}
最后IService方法中getById
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
default T getById(Serializable id) {
return getBaseMapper().selectById(id);
}
等价于:
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
default T getById(Serializable id) {
//其中的baseMapper就是employeeMapper
return baseMapper.selectById(id);
}
到这,可以确定IService接口中方法底层都是使用Mapper接口方法实现,那么IService 接口方法操作就可以同理可得啦。
MyBatis-Plus代码生成器
概念
代码生成器,顾名思义就是通过程序生成想要的代码。其原理非常简单,可以简单理解为:模板 + 数据 = 输出。
比如:
模板–Mapper.ftl
package ${basepackage}.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import ${basepackage}.${domain};
public interface ${domain}Mapper extends BaseMapper<${domain}> {
}
数据
{
basepackage:"com.langfeiyes.mp",
domain:"Department"
}
输入:数据 + 模板
将模板中**${占位符}** 替换成数据,并将渲染好的结果输出到DepartmentMapper.java文件中
package com.langfeiyes.mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.langfeiyes.mp.domain.Department;
public interface DepartmentMapper extends BaseMapper<Department> {
}
那么此时的DepartmentMapper.java类就编写好了,后续如果将Department替换成Employee,那么EmployeeMapper.xml也就能快速编写成功。
package com.langfeiyes.mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.langfeiyes.mp.domainEmployee;
public interface EmployeeMapper extends BaseMapper<Employee> {
}
MyBatis-Plus 也提供一套代码生成器-AutoGenerator,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
旧版
旧版代码生成器指的是mybatis-plus-generator 3.5.1 以下版本
以User用户为例子
步骤1:创建User数据表
CREATE TABLE `mybatis-plus`.`user` (
`id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(255) NULL COMMENT '用户名',
`password` varchar(255) NULL COMMENT '密码',
PRIMARY KEY (`id`)
);
步骤2:导入相关依赖
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.0</version>
</dependency>
<!--模板依赖-freemarker-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
<!--模板依赖-Beetl-->
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>beetl</artifactId>
<version>3.9.3.RELEASE</version>
</dependency>
<!--模板依赖-Velocity-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
注意上面3个模板只需要导入一个即可,默认是Velocity
步骤3:编写代码生成器
package com.langfeiyes.mp.generator;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.engine.BeetlTemplateEngine;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
/**
* 代码生成器执行逻辑:
* 1>配置数据源-直接数据库加载表,通过解析表自动创建 domain, mapper, service ,controller 这些类
* 2>配置全局参数-核心:创建文件的文件摆放的位置(路径)
* 3>配置包参数-明确指定包路径,模块名,各种类包名称
* 4>配置模板参数-明确指出模板所在位置,模板路径
* 5>配置策略参数-用过配置可以定制哪些表需要进行代码生成,哪些不需要,表一些通用前缀配置,列前缀配置等。
* 6>配置注入参数-配置与模板较互的自定义参数,作为拓展
*/
@Component
public class MyCodeGenerator {
@Autowired
private DataSource dataSource; //数据源
//全局配置
private GlobalConfig getGlobalConfig(){
GlobalConfig config = new GlobalConfig.Builder()
//如果之前已经执行过代码生成,后续再执行是覆盖之前代码
//.fileOverride()
//执行代码生成之后,要不打开文件, true是打开,false:不打开
.openDir(true)
//执行代码生成之后,代码文件输出到哪个位置
.outputDir(System.getProperty("user.dir") + "/src/main/java")
//代码上面注释:作者
.author("dafei")
//是否启动swagger2配置模式, 该方法表示启动
//.enableSwagger()
.build();
return config;
}
//包配置
private PackageConfig getPackageConfig(){
PackageConfig config = new PackageConfig.Builder()
//指定包路径(倒写域名)
.parent("com.langfeiyes")
//模板名(项目名)
.moduleName("mp")
//实体类所在包名
.entity("domain")
//服务层接口所在包名
.service("service")
//服务层接口实现类所在包名
.serviceImpl("service.impl")
//mapper接口所在包名
.mapper("mapper")
//mapper.xml文件所在包名
.xml("mapper")
//表现层类锁在包名
.controller("web.controller")
.build();
return config;
}
//模板配置
private TemplateConfig getTemplateConfig(){
TemplateConfig config = new TemplateConfig.Builder()
//指定实体模板路径
//.entity("不是自定义模板,不建议修改,使用即可")
//.service("不是自定义模板,不建议修改,使用即可")
//.mapper("不是自定义模板,不建议修改,使用即可")
//.mapperXml("不是自定义模板,不建议修改,使用即可")
//.controller("不是自定义模板,不建议修改,使用即可")
.build();
return config;
}
//策略配置-配置是表相关
private StrategyConfig getStrategyConfig(){
StrategyConfig config = new StrategyConfig.Builder()
//需要进行代码生成的表(不写,默认是数据库所有表)
.addInclude("user")
//排除哪些不进行代码生成的表
//.addExclude()
//统一指定表的前缀
//.addTablePrefix("t_")
.build();
return config;
}
//注入配置-
private InjectionConfig getInjectionConfig(){
InjectionConfig config = new InjectionConfig.Builder()
.build();
return config;
}
//执行代码构造
public void excute(){
//构造代码生成器实例对象
AutoGenerator autoGenerator = new AutoGenerator( new DataSourceConfig.Builder(dataSource).build())
//全局配置
.global(getGlobalConfig())
//包配置
.packageInfo(getPackageConfig())
//模板配置
.template(getTemplateConfig())
//策略配置
.strategy(getStrategyConfig())
//注入配置-自定义模板使用
.injection(getInjectionConfig());
//autoGenerator.execute(); //默认使用Velocity模板引擎
//autoGenerator.execute(new VelocityTemplateEngine());
autoGenerator.execute(new FreemarkerTemplateEngine());
//autoGenerator.execute(new BeetlTemplateEngine());
}
}
步骤4:测试
@SpringBootTest
public class CodeBuilderTest {
@Autowired
MyCodeGenerator myCodeGenerator;
@Test
public void testCode() throws SQLException {
myCodeGenerator.excute();
}
}
注意:如果是低版本的druid,会包错误:SQLFeatureNotSupportedException
主要是druid数据源中dataSource.getConnection().getSchema() 没有实现,直接丢异常,换新版本即可
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
新版
新版代码生成器指的是mybatis-plus-generator 3.5.1 以上版本,操作最大的变化是使用了lambda语法
以User用户为例子
前面操作操作步骤都一样
步骤1:创建user用户表
步骤2:导入相关依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
步骤3:编写代码生成器代码
package com.langfeiyes.mp.generator;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
@Component
public class MyCodeGenerator {
@Autowired
private DataSource dataSource;
public void excute() {
FastAutoGenerator.create(new DataSourceConfig.Builder(dataSource))
.globalConfig(builder -> builder
.outputDir(System.getProperty("user.dir") + "/src/main/java") //输出目录
.author("dafei") //作者
//.enableSwagger() //是否开启swagger模式
//.fileOverride() //如果文件已经存在则覆盖
)
.packageConfig(builder -> builder
.parent("com.langfeiyes") //父包名,根包
.moduleName("mp") //模块名
.entity("domain") //指定实体对象包名,默认:entity
.service("service") //指定服务接口包名,默认:service
.serviceImpl("service.impl") //指定服务接口实现类包名,默认:service.impl
.mapper("mapper") //指定映射接口包名,默认:mapper
.xml("xml") //指定映射xml文件包名,默认:mapper.xml
.controller("controller") //指定controller包名,默认:controller
)
/*.templateConfig(builder -> builder
//.entity("entity") //指定实体对象模板路径
//.service("service", //指定服务接口模板路径
// "serviceImpl") //指定服务接口模板路径
//.mapper("mapper") //指定映射接口模板路径
//.mapperXml("xml") //指定映射xml文件模板路径
//.controller("controller") //指定controller模板路径
)*/
.strategyConfig(builder -> builder
//.addFieldPrefix("字段前缀")
//.addExclude("排除哪些表")
//.addInclude("指定哪些表")
.addInclude("user")
//.addTablePrefix("添加表前缀")
)
.templateEngine(new FreemarkerTemplateEngine())
.execute();
}
}
步骤4:测试
@SpringBootTest
public class CodeBuilderTest {
@Autowired
MyCodeGenerator myCodeGenerator;
@Test
public void testCode() throws SQLException {
myCodeGenerator.excute();
}
}