md_files/自学/Mybatis&-Plus.md

1263 lines
38 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Mybatis
### 快速创建
![image-20240307125505211](https://pic.bitday.top/i/2025/03/19/u6pfoj-2.png)
1. 创建springboot工程Spring Initializr并导入 mybatis的起步依赖、mysql的驱动包。创建用户表user并创建对应的实体类User
![image-20240307125820685](https://pic.bitday.top/i/2025/03/19/u6q96d-2.png)
2. 在springboot项目中可以编写main/resources/application.properties文件配置数据库连接信息。
```
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=1234
```
3. 在引导类所在包下,在创建一个包 mapper。在mapper包下创建一个接口 UserMapper
![image-20240307132356616](https://pic.bitday.top/i/2025/03/19/u6qtz4-2.png)
@Mapper注解表示是mybatis中的Mapper接口
-程序运行时:框架会自动生成接口的**实现类对象(代理对象)**并交给Spring的IOC容器管理
@Select注解代表的就是select查询用于书写select查询语句
```java
@Mapper
public interface UserMapper {
//查询所有用户数据
@Select("select * from user")
public List<User> list();
}
```
### 数据库连接池
数据库连接池是一个容器,负责管理和分配数据库连接(`Connection`)。
- 在程序启动时,连接池会创建一定数量的数据库连接。
- 客户端在执行 SQL 时,从连接池获取连接对象,执行完 SQL 后,将连接归还给连接池,以供其他客户端复用。
- 如果连接对象长时间空闲且超过预设的最大空闲时间,连接池会自动释放该连接。
**优势**:避免频繁创建和销毁连接,提高数据库访问效率。
Druid德鲁伊
* Druid连接池是阿里巴巴开源的数据库连接池项目
* 功能强大性能优秀是Java语言最好的数据库连接池之一
把默认的 Hikari 数据库连接池切换为 Druid 数据库连接池:
1. 在pom.xml文件中引入依赖
```xml
<dependency>
<!-- Druid连接池依赖 -->
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
```
2. 在application.properties中引入数据库连接配置
```properties
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.druid.username=root
spring.datasource.druid.password=123456
```
### SQL注入问题
SQL注入由于没有对用户输入进行充分检查而SQL又是拼接而成在用户输入参数时在参数中添加一些SQL关键字达到改变SQL运行结果的目的也可以完成恶意攻击。
在Mybatis中提供的参数占位符有两种${...} 、#{...}
- #{...}
- 执行SQL时会将#{…}替换为?生成预编译SQL会自动设置参数值
- 使用时机:参数传递,都使用#{…}
- ${...}
- 拼接SQL。直接将参数拼接在SQL语句中**存在SQL注入问题**
- 使用时机:如果对表名、列表进行动态设置时使用
### 日志输出
只建议开发环境使用在Mybatis当中我们可以借助日志查看到sql语句的执行、执行传递的参数以及执行结果
1. 打开application.properties文件
2. 开启mybatis的日志并指定输出到控制台
```java
#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
```
### 驼峰命名法
在 Java 项目中,数据库表字段名一般使用 **下划线命名法**snake_case而 Java 中的变量名使用 **驼峰命名法**camelCase
- [x] **小驼峰命名lowerCamelCase**
- 第一个单词的首字母小写,后续单词的首字母大写。
- **例子**`firstName`, `userName`, `myVariable`
**大驼峰命名UpperCamelCase**
- 每个单词的首字母都大写,通常用于类名或类型名。
- **例子**`MyClass`, `EmployeeData`, `OrderDetails`
表中查询的数据封装到实体类中
- 实体类属性名和数据库表查询返回的**字段名一致**mybatis会自动封装。
- 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。
![image-20221212103124490](https://pic.bitday.top/i/2025/03/19/u6o894-2.png)
解决方法:
1. 起别名
2. 结果映射
3. **开启驼峰命名**
4. **属性名和表中字段名保持一致**
**开启驼峰命名(推荐)**如果字段名与属性名符合驼峰命名规则mybatis会自动通过驼峰命名规则映射
> 驼峰命名规则: abc_xyz => abcXyz
>
> - 表中字段名abc_xyz
> - 类中属性名abcXyz
### 推荐的完整配置:
```yaml
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.sky.entity
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true
```
`type-aliases-package: com.sky.entity`把 `com.sky.entity` 包下的所有类都当作别名注册XML 里就可以直接写 `<resultType="Dish">` 而不用写全限定名。可以多添加几个包,用逗号隔开。
### 增删改
- **增删改通用返回值为int时表示影响的记录数一般不需要可以设置为void**
**作用于单个字段**
```java
@Mapper
public interface EmpMapper {
//SQL语句中的id值不能写成固定数值需要变为动态的数值
//解决方案在delete方法中添加一个参数(用户id)将方法中的参数传给SQL语句
/**
* 根据id删除数据
* @param id 用户id
*/
@Delete("delete from emp where id = #{id}")//使用#{key}方式获取方法中的参数值
public void delete(Integer id);
}
```
![image-20240312122323753](https://pic.bitday.top/i/2025/03/19/u6mu7z-2.png)
上图参数值分离有效防止SQL注入
**作用于多个字段**
```java
@Mapper
public interface EmpMapper {
//会自动将生成的主键值赋值给emp对象的id属性
@Options(useGeneratedKeys = true,keyProperty = "id")
@Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
public void insert(Emp emp);
}
```
在 **`@Insert`** 注解中使用 `#{}` 来引用 `Emp` 对象的属性MyBatis 会自动从 `Emp` 对象中提取相应的字段并绑定到 SQL 语句中的占位符。
`@Options(useGeneratedKeys = true, keyProperty = "id")` 这行配置表示,插入时自动生成的主键会赋值给 `Emp` 对象的 `id` 属性。
```
// 调用 mapper 执行插入操作
empMapper.insert(emp);
// 现在 emp 对象的 id 属性会被自动设置为数据库生成的主键值
System.out.println("Generated ID: " + emp.getId());
```
### 查
查询案例:
- **姓名:要求支持模糊匹配**
- 性别:要求精确匹配
- 入职时间:要求进行范围查询
- 根据最后修改时间进行降序排序
重点在于模糊查询时where name like '%#{name}%' 会报错。
解决方案:
使用MySQL提供的字符串拼接函数`concat('%' , '关键字' , '%')`
**`CONCAT()`** 如果其中任何一个参数为 **`NULL`**`CONCAT()` 返回 **`NULL`**`Like NULL`会导致查询不到任何结果!
`NULL`和`''`是完全不同的
```java
@Mapper
public interface EmpMapper {
@Select("select * from emp " +
"where name like concat('%',#{name},'%') " +
"and gender = #{gender} " +
"and entrydate between #{begin} and #{end} " +
"order by update_time desc")
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
}
```
### XML配置文件规范
使用Mybatis的注解方式主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能建议使用XML来配置映射语句也就是将SQL语句写在XML配置文件中。
在Mybatis中使用XML映射文件方式开发需要符合一定的规范
1. XML映射**文件的名称**与Mapper**接口名称**一致并且将XML映射文件和Mapper接口放置在相同包下同包同名
2. XML映射文件的**namespace属性**为Mapper接口**全限定名**一致
3. XML映射文件中sql语句的**id**与Mapper接口中的**方法名**一致,并保持返回类型一致。
![image-20221212153529732](https://pic.bitday.top/i/2025/03/19/u6su5s-2.png)
\<select>标签就是用于编写select查询语句的。
resultType属性指的是查询返回的单条记录所封装的类型(查询必须)。
parameterType属性可选MyBatis 会根据接口方法的入参类型(比如 `Dish` 或 `DishPageQueryDTO`自动推断POJO作为入参需要使用全类名或是`typealiasespackage: com.sky.entity` 下注册的别名。
```
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
<select id="pageQuery" resultType="com.sky.vo.DishVO">
<select id="list" resultType="com.sky.entity.Dish" parameterType="com.sky.entity.Dish">
```
**实现过程:**
1. resources下创与java下一样的包即edu/whut/mapper新建xx.xml文件
2. 配置Mapper文件
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="edu.whut.mapper.EmpMapper">
<!-- SQL 查询语句写在这里 -->
</mapper>
```
`namespace` 属性指定了 **Mapper 接口的全限定名**(即包名 + 类名)。
3. 编写查询语句
```xml
<select id="list" resultType="edu.whut.pojo.Emp">
select * from emp
where name like concat('%',#{name},'%')
and gender = #{gender}
and entrydate between #{begin} and #{end}
order by update_time desc
</select>
```
**`id="list"`**:指定查询方法的名称,应该与 Mapper 接口中的方法名称一致。
**`resultType="edu.whut.pojo.Emp"`**`resultType` 只在 **查询操作** 中需要指定。指定查询结果映射的对象类型,这里是 `Emp` 类。
这里有bug
`concat('%',#{name},'%')`这里应该用`<where>` `<if>`标签对name是否为`NULL`或`''`进行判断
### 动态SQL
#### SQL-if,where
`<if>`用于判断条件是否成立。使用test属性进行条件判断如果条件为true则拼接SQL。
~~~xml
<if test="条件表达式">
要拼接的sql语句
</if>
~~~
`<where>`只会在子元素有内容的情况下才插入where子句而且会自动去除子句的开头的AND或OR,**加了总比不加好**
```java
<select id="list" resultType="com.itheima.pojo.Emp">
select * from emp
<where>
<!-- if做为where标签的子元素 -->
<if test="name != null">
and name like concat('%',#{name},'%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
</where>
order by update_time desc
</select>
```
#### SQL-foreach
Mapper 接口
```java
@Mapper
public interface EmpMapper {
//批量删除
public void deleteByIds(List<Integer> ids);
}
```
XML 映射文件
`<foreach>` 标签用于遍历集合,常用于动态生成 SQL 语句中的 IN 子句、批量插入、批量更新等操作。
```java
<foreach collection="集合名称" item="集合遍历出来的元素/项" separator="每一次遍历使用的分隔符"
open="遍历开始前拼接的片段" close="遍历结束后拼接的片段">
</foreach>
```
`open="("`:这个属性表示,在*生成的 SQL 语句开始*时添加一个 左括号 `(`。
`close=")"`:这个属性表示,在生成的 SQL 语句结束时添加一个 右括号 `)`。
例:批量删除实现
```java
<delete id="deleteByIds">
DELETE FROM emp WHERE id IN
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
```
实现效果类似:`DELETE FROM emp WHERE id IN (1, 2, 3);`
## Mybatis-Plus
MyBatis-Plus 的使命就是——在保留 MyBatis 灵活性的同时,大幅减少模板化、重复的代码编写,让增删改查、分页等常见场景“开箱即用”,以更少的配置、更少的样板文件、更高的开发效率,帮助团队快速交付高质量的数据库访问层。
### 快速开始
#### **1.引入依赖**
```XML
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.mybatis.spring.boot</groupId>-->
<!-- <artifactId>mybatis-spring-boot-starter</artifactId>-->
<!-- <version>2.3.1</version>-->
<!-- </dependency>-->
```
由于这个starter包含对mybatis的自动装配因此完**全可以替换**掉Mybatis的starter。
#### **2.定义mapper**
为了简化单表CRUDMybatisPlus提供了一个基础的`BaseMapper`接口,其中已经实现了单表的**CRUD(增删查改)**
<img src="https://pic.bitday.top/i/2025/05/18/shk22b-0.png" alt="image-20250518172250325" style="zoom:67%;" />
仅需让自定义的`UserMapper`接口,继承`BaseMapper`接口:
```java
public interface UserMapper extends BaseMapper<User> {
}
```
测试:
```java
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testInsert() {
User user = new User();
user.setId(5L);
user.setUsername("Lucy");
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
userMapper.insert(user);
}
@Test
void testSelectById() {
User user = userMapper.selectById(5L);
System.out.println("user = " + user);
}
@Test
void testSelectByIds() {
List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L, 5L));
users.forEach(System.out::println);
}
@Test
void testUpdateById() {
User user = new User();
user.setId(5L);
user.setBalance(20000);
userMapper.updateById(user);
}
@Test
void testDelete() {
userMapper.deleteById(5L);
}
}
```
#### **3.常见注解**
MybatisPlus如何知道我们要查询的是哪张表表中有哪些字段呢
**约定大于配置**
**泛型中的User**就是与数据库对应的PO.
MybatisPlus就是根据PO实体的信息来推断出表的信息从而生成SQL的。默认情况下
- MybatisPlus会把PO实体的**类名**驼峰转下划线作为**表名** `UserRecord->user_record`
- MybatisPlus会把PO实体的所有**变量名**驼峰转下划线作为表的**字段名**,并根据变量类型推断字段类型
- MybatisPlus会把名为**id**的字段作为**主键**
但很多情况下默认的实现与实际场景不符因此MybatisPlus提供了一些注解便于我们声明表信息。
**@TableName**
- 描述:表名注解,标识实体类对应的表
**@TableId**
- 描述:主键注解,标识实体类中的主键字段
`TableId`注解支持两个属性:
| **属性** | **类型** | **必须指定** | **默认值** | **描述** |
| :------- | :------- | :----------- | :---------- | :----------- |
| value | String | 否 | "" | 主键字段名 |
| type | Enum | 否 | IdType.NONE | 指定主键类型 |
```java
@TableName("user_detail")
public class User {
@TableId(value="id_dd",type=IdType.AUTO)
private Long id;
private String name;
}
```
这个例子会映射到数据库中的user_detail表主键为id_dd并且插入时采用数据库自增能自动回写主键相当于开启`useGeneratedKeys=true`,执行完 `insert(user)` 后,`user.getId()` 就会是数据库分配的主键值否则默认获得null但不影响数据表中的内容。
`type=dType.ASSIGN_ID` 表示用雪花算法生成密码更加复杂而不是简单的AUTO自增。它也能自动回写主键。
**@TableField**
- 普通字段注解
一般情况下我们并不需要给字段添加`@TableField`注解,一些特殊情况除外:
- 成员变量名与数据库字段名不一致
- 成员变量是以`isXXX`命名,按照`JavaBean`的规范,`MybatisPlus`识别字段时会把`is`去除,这就导致与数据库不符。
```java
public class User {
private Long id;
private String name;
private Boolean isActive; // 按 JavaBean 习惯,这里用 isActive数据表是is_acitive但MybatisPlus会识别为active
}
```
- 成员变量名与数据库一致,但是与数据库的**关键字(如order)**冲突。
```java
public class Order {
private Long id;
private Integer order; // 名字和 SQL 关键字冲突
}
```
默认MP会生成`SELECT id, order FROM order;` 导致报错
- 一些字段不希望被映射到数据表中,不希望进行增删查改
解决办法:
```java
@TableField("is_active")
private Boolean isActive;
@TableField("`order`") //添加转义字符
private Integer order;
@TableField(exist=false) //exist默认是true
private String address;
```
#### **4.常用配置**
大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:
- 实体类的别名扫描包
- 全局id类型
要改也就改这两个即可
```YAML
mybatis-plus:
type-aliases-package: edu.whut.mp.domain.po
global-config:
db-config:
id-type: auto # 全局id类型为自增长
```
作用1.把`edu.whut.mp.domain.po `包下的所有 `PO` 类注册为 MyBatis 的 Type Alias。这样在你的 Mapper XML 里就可以直接写 `<resultType="User">`(或 `<parameterType="User">`)而不用写全限定类名 `edu.whut.mp.domain.po.User`
2.无需在每个 `@TableId` 上都写 `type = IdType.AUTO`,统一由全局配置管。
### 核心功能
前面的例子都是**根据主键id**更新、修改、查询无法支持复杂条件where。
#### 条件构造器Wrapper
除了新增以外修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法**除了以`id`作为`where`条件**以外,还支持**更加复杂的`where`条件**。
<img src="https://pic.bitday.top/i/2025/05/18/tyh7e3-0.png" alt="image-20250518181145318" style="zoom:67%;" />
`Wrapper`就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
<img src="https://pic.bitday.top/i/2025/03/19/u7fwe0-2.png" alt="image-20240813112049624" style="zoom: 67%;" />
<img src="https://pic.bitday.top/i/2025/03/19/u7f24w-2.png" alt="image-20240813134824946" style="zoom:67%;" />
**QueryWrapper**
在AbstractWrapper的基础上拓展了一个**select方法**,允许指定查询字段,无论是**修改、删除、查询**都可以使用QueryWrapper来构建查询条件。
select方法只需用于 **查询** 时指定所需的**列**完整查询不需要用于update和delete不需要。
`QueryWrapper` 里对 `like`、`eq`、`ge` 等方法都做了重载
```
QueryWrapper<User> qw = new QueryWrapper<>();
qw.like("name", name); //两参版本,第一个参数对应数据库中的列名,如果对应不上,就会报错!!!
qw.like(StrUtil.isNotBlank(name), "name", name); //三参多一个boolean condition 参数
```
**例1**查询出名字中带o的存款大于等于1000元的人的idusername,info,balance:
```Java
/**
* SELECT id,username,info,balance
* FROM user
* WHERE username LIKE ? AND balance >=?
*/
@Test
void testQueryWrapper(){
QueryWrapper<User> wrapper =new QueryWrapper<User>()
.select("id","username","info","balance")
.like("username","o")
.ge("balance",1000);
//查询
List<User> users=userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
```
**UpdateWrapper**
基于BaseMapper中的update方法更新时只能直接赋值对于一些复杂的需求就难以实现。
**例1** 例如更新id为`1,2,4`的用户的余额扣200对应的SQL应该是
```Java
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
```
```Java
@Test
void testUpdateWrapper() {
List<Long> ids = List.of(1L, 2L, 4L);
// 1.生成SQL
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200") // SET balance = balance - 200
.in("id", ids); // WHERE id in (1, 2, 4)
// 2.更新注意第一个参数可以给null告诉 MP不要从实体里取任何字段值
// 而是基于UpdateWrapper中的setSQL来更新
userMapper.update(null, wrapper);
}
```
**例2**
```java
// 用 UpdateWrapper 拼 WHERE + SET
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
// WHERE status = 'ACTIVE'
.eq("status", "ACTIVE")
// SET balance = 2000, name = 'Alice'
.set("balance", 2000)
.set("name", "Alice");
// 把 entity 参数传 nullMyBatis-Plus 会只用 wrapper 里的 set/where
userMapper.update(null, wrapper);
```
**LambdaQueryWrapper推荐**
是**QueryWrapper**和**UpdateWrapper**的上位选择!!!
传统的 `QueryWrapper`/`UpdateWrapper` 需要把数据库字段名写成**字符串常量**既容易拼写出错也无法在编译期校验。MyBatis-Plus 引入了两种基于 Lambda 的 Wrapper —— `LambdaQueryWrapper` 和 `LambdaUpdateWrapper` —— 通过传入实体类的 getter 方法引用,框架会自动解析并映射到对应的列,实现了类型安全和更高的可维护性。
```java
// ——— 传统 QueryWrapper ———
public User findByUsername(String username) {
QueryWrapper<User> qw = new QueryWrapper<>();
// 硬编码列名,拼写错了编译不过不了,会在运行时抛数据库异常
qw.eq("user_name", username);
return userMapper.selectOne(qw);
}
// ——— LambdaQueryWrapper ———
public User findByUsername(String username) {
// 内部已注入实体 Class 和元数据,方法引用自动解析列名
LambdaQueryWrapper<User> qw = Wrappers.lambdaQuery(User.class)
.eq(User::getUserName, username);
return userMapper.selectOne(qw);
}
```
#### 自定义sql
即自己编写Wrapper查询条件再结合Mapper.xml编写SQL
**例1**以 `UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)` 为例:
1先在**业务层**利用wrapper创建条件传递参数
```java
@Test
void testCustomWrapper() {
// 1.准备自定义查询条件
List<Long> ids = List.of(1L, 2L, 4L);
QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);
// 2.调用mapper的自定义方法直接传递Wrapper
userMapper.deductBalanceByIds(200, wrapper);
}
```
2自定义**mapper层**把wrapper和其他业务参数传进去自定义sql语句书写sql的前半部分后面拼接。
```java
public interface UserMapper extends BaseMapper<User> {
/**
* 注意:更新要用 @Update
* - #{money} 会被替换为方法第一个参数 200
* - ${ew.customSqlSegment} 会展开 wrapper 里的 WHERE 子句
*/
@Update("UPDATE user " +
"SET balance = balance - #{money} " +
"${ew.customSqlSegment}")
void deductBalanceByIds(@Param("money") int money,
@Param("ew") QueryWrapper<User> wrapper);
}
```
@Param("ew")就是给这个方法参数在 MyBatis 的 SQL 映射里起一个别名—— `ew ` Mapper 的注解或 XML 里MyBatis 想要拿到这个参数,就用它的 `@Param` 名称——也就是 **`ew`**
@Param("ew")中ew是 MP 约定的别名!
`${ew.customSqlSegment}` 可以自动拼接传入的条件语句
**例2**查询出所有收货地址在北京的并且用户id在1、2、4之中的用户
普通mybatis
```xml
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
SELECT *
FROM user u
INNER JOIN address a ON u.id = a.user_id
WHERE u.id
<foreach collection="ids" separator="," item="id" open="IN (" close=")">
#{id}
</foreach>
AND a.city = #{city}
</select>
```
mp方法
```java
@Test
void testCustomJoinWrapper() {
// 1.准备自定义查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.in("u.id", List.of(1L, 2L, 4L))
.eq("a.city", "北京");
// 2.调用mapper的自定义方法
List<User> users = userMapper.queryUserByWrapper(wrapper);
}
```
```xml
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);
```
#### Service层的常用方法
**查询:**
selectById根据主键 ID 查询单条记录。
selectBatchIds根据主键 ID 批量查询记录。
selectOne根据指定条件查询单条记录。
```java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User findByUsername(String username) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
return userMapper.selectOne(queryWrapper);
}
}
```
selectList根据指定条件查询多条记录。
```java
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.ge("age", 18);
List<User> users = userMapper.selectList(queryWrapper);
```
**插入:**
insert插入一条记录。
```java
User user = new User();
user.setUsername("alice");
user.setAge(20);
int rows = userMapper.insert(user);
```
**更新**
updateById根据主键 ID 更新记录。
```java
User user = new User();
user.setId(1L);
user.setAge(25);
int rows = userMapper.updateById(user);
```
update根据指定条件更新记录。
```java
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("username", "alice");
User user = new User();
user.setAge(30);
int rows = userMapper.update(user, updateWrapper);
```
**删除操作**
deleteById根据主键 ID 删除记录。
deleteBatchIds根据主键 ID 批量删除记录。
delete根据指定条件删除记录。
```java
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", "alice");
int rows = userMapper.delete(queryWrapper);
```
### IService
![image-20240815092311650](https://pic.bitday.top/i/2025/03/19/u7g3qb-2.png)
![image-20240815092324887](https://pic.bitday.top/i/2025/03/19/u7gmfr-2.png)
![image-20240815092338012](https://pic.bitday.top/i/2025/03/19/u7frqa-2.png)
![image-20240815092352179](https://pic.bitday.top/i/2025/03/19/u7gubw-2.png)
![image-20240815092420201](https://pic.bitday.top/i/2025/03/19/u7gbph-2.png)
![image-20240815092604848](https://pic.bitday.top/i/2025/03/19/u7f9pf-2.png)
#### 基本使用
由于`Service`中经常需要定义与业务有关的自定义方法,因此我们不能直接使用`IService`,而是自定义`Service`接口,然后继承`IService`以拓展方法。同时,让自定义的`Service实现类`继承`ServiceImpl`,这样就不用自己实现`IService`中的接口了。
<img src="https://pic.bitday.top/i/2025/05/19/o3lyzl-0.png" alt="image-20250519145722328" style="zoom:67%;" />
首先,定义`IUserService`,继承`IService`
```java
public interface IUserService extends IService<User> {
// 拓展自定义方法
}
```
然后,编写`UserServiceImpl`类,继承`ServiceImpl`,实现`UserService`
```java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
```
Controller层中写
```java
@RestController
@RequestMapping("/users")
@Slf4j
@Api(tags = "用户管理接口")
public class UserController {
@Autowired
private IUserService userService;
@PostMapping
@ApiOperation("新增用户接口")
public void saveUser(@RequestBody UserFormDTO userFormDTO){
User user=new User();
BeanUtils.copyProperties(userFormDTO, user);
userService.save(user);
}
@DeleteMapping("{id}")
@ApiOperation("删除用户接口")
public void deleteUserById(@PathVariable Long id){
userService.removeById(id);
}
@GetMapping("{id}")
@ApiOperation("根据id查询接口")
public UserVO queryUserById(@PathVariable Long id){
User user=userService.getById(id);
UserVO userVO=new UserVO();
BeanUtils.copyProperties(user,userVO);
return userVO;
}
@PutMapping("/{id}/deduction/{money}")
@ApiOperation("根据id扣减余额")
public void updateBalance(@PathVariable Long id,@PathVariable Long money){
userService.deductBalance(id,money);
}
}
```
service层
```java
@Service
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public void deductBalance(Long id, Long money) {
//1.查询用户
User user=getById(id);
if(user==null || user.getStatus()==2){
throw new RuntimeException("用户状态异常!");
}
//2.查验余额
if(user.getBalance()<money){
throw new RuntimeException("用户余额不足!");
}
//3.扣除余额 update User set balance=balance-money where id=id
userMapper.deductBalance(id,money);
}
}
```
mapper层
```java
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Update("update user set balance=balance-#{money} where id=#{id}")
void deductBalance(Long id, Long money);
}
```
总结如果是简单查询如用id来查询、删除可以直接在Controller层用Iservice方法否则自定义业务层Service实现具体任务。
#### Service层的lambdaQuery
IService中还提供了Lambda功能来简化我们的**复杂查询及更新功能**。
相当于「条件构造」和「执行方法」写在一起
`this.lambdaQuery()` = `LambdaQueryWrapper` + 内置的执行方法(如 `.list()`、`.one()`
| 特性 | `lambdaQuery()` | `lambdaUpdate()` |
| -------------- | --------------------------------------------------------- | --------------------------------------------- |
| **主要用途** | 构造查询条件,执行 `SELECT` 操作 | 构造更新条件,执行 `UPDATE`(或逻辑删除)操作 |
| **支持的方法** | `.eq()`, `.like()`, `.gt()`, `.orderBy()`, `.select()` 等 | `.eq()`, `.lt()`, `.set()`, `.setSql()` 等 |
| **执行方法** | `.list()`, `.one()`, `.page()` 等 | `.update()`, `.remove()`(逻辑删除 |
**案例一:**实现一个根据复杂条件查询用户的接口,查询条件如下:
- name用户名关键字可以为空
- status用户状态可以为空
- minBalance最小余额可以为空
- maxBalance最大余额可以为空
```java
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUsers(UserQuery query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
// 2.查询用户
List<User> users = userService.lambdaQuery()
.like(username != null, User::getUsername, username)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance)
.list();
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
```
`.eq(status != null, User::getStatus, status)`,使用`User::getStatus`方法引用并不直接把'Status'插入到 SQL而是在运行时会被 MyBatis-Plus 解析成实体属性 `Status`”对应的数据库列是 `status`。推荐!!!
可以发现lambdaQuery方法中除了可以构建条件还需要在链式编程的最后添加一个`list()`这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用`list()`,可选的方法有:
- `.one()`最多1个结果
- `.list()`:返回集合结果
- `.count()`:返回计数结果
MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。
这里不够规范业务写在controller层中了。
**案例二:**改造根据id修改用户余额的接口如果扣减后余额为0则将用户status修改为冻结状态2
```java
@Override
@Transactional
public void deductBalance(Long id, Integer money) {
// 1.查询用户
User user = getById(id);
// 2.校验用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
// 3.校验余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
// 4.扣减余额 update tb_user set balance = balance - ?
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance) // 更新余额
.set(remainBalance == 0, User::getStatus, 2) // 动态判断是否更新status
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance()) // 乐观锁
.update();
}
```
#### 批量新增
每 `batchSize` 条记录作为一个 JDBC batch 提交一次1000 条就一次)
```java
@Test
void testSaveBatch() {
// 准备10万条数据
List<User> list = new ArrayList<>(1000);
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
list.add(buildUser(i));
// 每1000条批量插入一次
if (i % 1000 == 0) {
userService.saveBatch(list);
list.clear();
}
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - b));
}
```
之所以把 100 000 条记录分成每 1 000 条一批来插,是为了兼顾 **性能**、**内存** 和 **数据库JDBC 限制**。
**JDBC 或数据库参数限制**
- 很多数据库MySQL、Oracle 等)对单条 SQL 里 `VALUES` 列表的长度有上限,一次性插入几十万行可能导致 SQL 过长、参数个数过多,被驱动或数据库拒绝。
- 即使驱动不直接报错也可能因为网络包packet过大而失败。
**内存占用和 GC 压力**
- JDBC 在执行 batch 时,会把所有要执行的 SQL 和参数暂存在客户端内存里。如果一次性缓存 100 000 条记录的参数(可能是几 MB 甚至十几 MB容易触发 OOM 或者频繁 GC。
**事务日志和回滚压力**
- 一次性插入大量数据,数据库需要在事务日志里记录相应条目,回滚时也要一次性回滚所有操作,性能开销巨大。分批能让每次写入都较为“轻量”,回滚范围也更小。
这种本质上是**多条单行 INSERT**
```mysql
Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01
```
而如果想要得到最佳性能最好是将多条SQL合并为一条像这样
```mysql
INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )
VALUES
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);
```
需要修改项目中的application.yml文件在jdbc的url后面添加参数`&rewriteBatchedStatements=true`
`url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true`
**但是会存在上述上事务的问题!!!**
### MQ分页
**快速入门**
1引入依赖
```xml
<!-- 数据库操作https://mp.baomidou.com/ -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!-- MyBatis Plus 分页插件 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>
```
2定义通用分页查询条件实体
```java
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Long pageNo;
@ApiModelProperty("页码")
private Long pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}
```
3新建一个 `UserQuery` 类,让它继承自你已有的 `PageQuery`
```java
@Data
@ApiModel(description = "用户分页查询实体")
public class UserQuery extends PageQuery {
@ApiModelProperty("用户名(模糊查询)")
private String name;
}
```
4Service里使用
```java
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
/**
* 用户分页查询(带用户名模糊 + 动态排序)
*
* @param query 包含 pageNo、pageSize、sortBy、isAsc、name 等字段
*/
public Page<User> pageByQuery(UserQuery query) {
// 1. 构造 Page 对象
Page<User> page = new Page<>(
query.getPageNo(),
query.getPageSize()
);
// 2. 构造查询条件
LambdaQueryWrapper<User> qw = Wrappers.<User>lambdaQuery()
// 当 name 非空时,加上 user_name LIKE '%name%'
.like(StrUtil.isNotBlank(query.getName()), User::getUserName, query.getName());
// 3. 动态排序
if (StrUtil.isNotBlank(query.getSortBy())) {
String column = StrUtil.toUnderlineCase(query.getSortBy());
boolean asc = Boolean.TRUE.equals(query.getIsAsc());
qw.last("ORDER BY " + column + (asc ? " ASC" : " DESC"));
}
// 4. 执行分页查询
return this.page(page, qw);
}
}
```