文章目录
-
- 概述
- [1. 依赖与连接配置](#1. 依赖与连接配置)
-
- [1.1 Maven 依赖(片段)](#1.1 Maven 依赖(片段))
- [1.2 application.yml 连接](#1.2 application.yml 连接)
- [1.3 启动类与包扫描](#1.3 启动类与包扫描)
- [1.4 分页插件配置](#1.4 分页插件配置)
- [2. 示例模型与 Mapper](#2. 示例模型与 Mapper)
-
- [2.1 表结构(示例)](#2.1 表结构(示例))
- [2.2 实体 Entity](#2.2 实体 Entity)
- [2.3 Mapper 接口](#2.3 Mapper 接口)
- [3. 常见查询(QueryWrapper / Lambda)](#3. 常见查询(QueryWrapper / Lambda))
-
- [3.1 基础查询](#3.1 基础查询)
- [3.2 LambdaQueryWrapper(更安全的字段引用)](#3.2 LambdaQueryWrapper(更安全的字段引用))
- [3.3 按条件可选拼装](#3.3 按条件可选拼装)
- [4. 分页查询(Page)](#4. 分页查询(Page))
- [5. JOIN(连接)查询的常见做法](#5. JOIN(连接)查询的常见做法)
-
- [5.1 一对一(User ↔ UserProfile)](#5.1 一对一(User ↔ UserProfile))
- [5.2 一对多(User ↔ Orders)](#5.2 一对多(User ↔ Orders))
- [5.3 多对多(User ↔ Role,经 user_role 关联)](#5.3 多对多(User ↔ Role,经 user_role 关联))
- [6. Service 层(IService / ServiceImpl)](#6. Service 层(IService / ServiceImpl))
- [7. Controller 层(CRUD + 分页)](#7. Controller 层(CRUD + 分页))
- [8. 实用补充与优化建议](#8. 实用补充与优化建议)
- [9. 小结](#9. 小结)
概述
- 目标:用最少代码完成单表 CRUD、分页查询,并给出 JOIN 查询的常见做法。
- 环境:Spring Boot 3.x + MyBatis-Plus 3.5+ + MySQL 8.x。
1. 依赖与连接配置
1.1 Maven 依赖(片段)
xml
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
1.2 application.yml 连接
yaml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath*:mapper/**/*.xml
1.3 启动类与包扫描
java
@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 若在接口上使用 @Mapper,可不加 @MapperScan
public class DemoApplication {
public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }
}
1.4 分页插件配置
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2. 示例模型与 Mapper
2.1 表结构(示例)
sql
CREATE TABLE `t_user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL,
`age` INT NOT NULL,
`email` VARCHAR(128),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
2.2 实体 Entity
java
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
@TableField("created_at")
private LocalDateTime createdAt;
}
2.3 Mapper 接口
java
public interface UserMapper extends BaseMapper<User> {}
3. 常见查询(QueryWrapper / Lambda)
3.1 基础查询
java
@Autowired private UserMapper userMapper;
// 主键
User u1 = userMapper.selectById(1L);
// 批量主键
List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L));
// 条件查询(QueryWrapper)
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("id", "name", "age")
.eq("age", 20)
.likeRight("name", "A")
.orderByDesc("id");
List<User> list = userMapper.selectList(qw);
// 计数
Long count = userMapper.selectCount(new QueryWrapper<User>().ge("age", 18));
3.2 LambdaQueryWrapper(更安全的字段引用)
java
LambdaQueryWrapper<User> lw = Wrappers.lambdaQuery(User.class)
.select(User::getId, User::getName)
.between(User::getAge, 18, 30)
.like(User::getName, "Tom")
.orderByAsc(User::getCreatedAt);
List<User> list2 = userMapper.selectList(lw);
3.3 按条件可选拼装
java
String name = req.getName();
Integer minAge = req.getMinAge();
LambdaQueryWrapper<User> w = Wrappers.lambdaQuery(User.class)
.like(StringUtils.hasText(name), User::getName, name)
.ge(minAge != null, User::getAge, minAge);
List<User> result = userMapper.selectList(w);
4. 分页查询(Page)
需先注册分页插件(见 1.4)。
java
int pageNo = 1, pageSize = 10;
Page<User> page = new Page<>(pageNo, pageSize);
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class)
.ge(User::getAge, 18)
.orderByDesc(User::getId);
Page<User> resultPage = userMapper.selectPage(page, wrapper);
List<User> records = resultPage.getRecords();
long total = resultPage.getTotal();
long pages = resultPage.getPages();
可在 Controller 中直接返回分页结果:
java
@GetMapping("/users")
public Page<User> list(@RequestParam int page, @RequestParam int size,
@RequestParam(required = false) Integer minAge) {
Page<User> p = new Page<>(page, size);
LambdaQueryWrapper<User> w = Wrappers.lambdaQuery(User.class)
.ge(minAge != null, User::getAge, minAge)
.orderByDesc(User::getId);
return userMapper.selectPage(p, w);
}
5. JOIN(连接)查询的常见做法
MyBatis-Plus 聚焦单表 CRUD;多表 JOIN 推荐使用自定义 SQL(注解或 XML)。
5.1 一对一(User ↔ UserProfile)
- 表
sql
CREATE TABLE `user_profile` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL UNIQUE,
`phone` VARCHAR(32),
`address` VARCHAR(255)
);
- DTO
java
@Data
public class UserProfileDTO {
private Long id;
private Long userId;
private String phone;
private String address;
}
@Data
public class UserWithProfileDTO {
private Long id;
private String name;
private Integer age;
private String email;
private UserProfileDTO profile;
}
- Mapper 接口
java
public interface UserProfileQueryMapper {
UserWithProfileDTO selectUserWithProfile(@Param("userId") Long userId);
}
- XML(resources/mapper/UserProfileQueryMapper.xml)
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.example.demo.mapper.UserProfileQueryMapper">
<resultMap id="UserProfileMap" type="com.example.demo.dto.UserProfileDTO">
<id property="id" column="p_id"/>
<result property="userId" column="p_user_id"/>
<result property="phone" column="p_phone"/>
<result property="address" column="p_address"/>
</resultMap>
<resultMap id="UserWithProfileMap" type="com.example.demo.dto.UserWithProfileDTO">
<id property="id" column="u_id"/>
<result property="name" column="u_name"/>
<result property="age" column="u_age"/>
<result property="email" column="u_email"/>
<association property="profile" resultMap="UserProfileMap"/>
</resultMap>
<select id="selectUserWithProfile" resultMap="UserWithProfileMap">
SELECT
u.id AS u_id, u.name AS u_name, u.age AS u_age, u.email AS u_email,
p.id AS p_id, p.user_id AS p_user_id, p.phone AS p_phone, p.address AS p_address
FROM t_user u
LEFT JOIN user_profile p ON p.user_id = u.id
WHERE u.id = #{userId}
</select>
</mapper>
5.2 一对多(User ↔ Orders)
下例演示嵌套集合映射;若只需扁平结果,可参考 5.1/5.3 的 DTO 方式。
- 表
sql
CREATE TABLE `order` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`amount` DECIMAL(6,2)
);
- DTO
java
@Data
public class OrderDTO {
private Long id;
private Long userId;
private BigDecimal amount;
}
@Data
public class UserWithOrdersDTO {
private Long id;
private String name;
private List<OrderDTO> orders;
}
- Mapper 接口
java
public interface UserOrderNestedMapper {
UserWithOrdersDTO selectUsersWithOrders(@Param("userId") Long userId);
}
- XML(resources/mapper/UserOrderNestedMapper.xml)
xml
<mapper namespace="com.example.demo.mapper.UserOrderNestedMapper">
<resultMap id="OrderMap" type="com.example.demo.dto.OrderDTO">
<id property="id" column="o_id"/>
<result property="amount" column="o_amount"/>
</resultMap>
<resultMap id="UserWithOrdersMap" type="com.example.demo.dto.UserWithOrdersDTO">
<id property="id" column="u_id"/>
<result property="name" column="u_name"/>
<collection property="orders" ofType="com.example.demo.dto.OrderDTO" resultMap="OrderMap"/>
</resultMap>
<select id="selectUsersWithOrders" resultMap="UserWithOrdersMap">
SELECT
u.id AS u_id, u.name AS u_name,
o.id AS o_id, o.amount AS o_amount
FROM t_user u
LEFT JOIN `order` o ON o.user_id = u.id
<where>
u.id = #{userId}
</where>
ORDER BY u.id DESC, o.id DESC
</select>
</mapper>
5.3 多对多(User ↔ Role,经 user_role 关联)
- 表
sql
CREATE TABLE `role` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL
);
CREATE TABLE `user_role` (
`user_id` BIGINT NOT NULL,
`role_id` BIGINT NOT NULL,
PRIMARY KEY (`user_id`, `role_id`)
);
- DTO
java
@Data
public class RoleDTO {
private Long id;
private String name;
}
@Data
public class UserWithRolesDTO {
private Long id;
private String name;
private List<RoleDTO> roles;
}
- Mapper 接口
java
public interface UserRoleMapper {
List<UserWithRolesDTO> selectUsersWithRoles(@Param("userIds") List<Long> userIds);
}
- XML(resources/mapper/UserRoleMapper.xml)
xml
<mapper namespace="com.example.demo.mapper.UserRoleMapper">
<resultMap id="RoleMap" type="com.example.demo.dto.RoleDTO">
<id property="id" column="r_id"/>
<result property="name" column="r_name"/>
</resultMap>
<resultMap id="UserWithRolesMap" type="com.example.demo.dto.UserWithRolesDTO">
<id property="id" column="u_id"/>
<result property="name" column="u_name"/>
<collection property="roles" ofType="com.example.demo.dto.RoleDTO" resultMap="RoleMap"/>
</resultMap>
<select id="selectUsersWithRoles" resultMap="UserWithRolesMap">
SELECT
u.id AS u_id, u.name AS u_name,
r.id AS r_id, r.name AS r_name
FROM user u
LEFT JOIN user_role ur ON ur.user_id = u.id
LEFT JOIN role r ON r.id = ur.role_id
<where>
<if test="userIds != null and userIds.size > 0">
u.id IN
<foreach collection="userIds" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
</where>
ORDER BY u.id DESC, r.id DESC
</select>
</mapper>
小贴士:嵌套集合映射时,为每个表字段起唯一别名(如 u_id/o_id/r_id
),避免列名冲突导致映射失败。
6. Service 层(IService / ServiceImpl)
通过继承通用接口/实现类,快速获得 CRUD 能力并封装业务逻辑。
java
public interface UserService extends IService<User> {
Page<User> pageUsers(int page, int size, Integer minAge);
}
java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public Page<User> pageUsers(int page, int size, Integer minAge) {
Page<User> p = new Page<>(page, size);
LambdaQueryWrapper<User> w = Wrappers.lambdaQuery(User.class)
.ge(minAge != null, User::getAge, minAge)
.orderByDesc(User::getId);
return this.page(p, w);
}
}
7. Controller 层(CRUD + 分页)
控制器依赖 Service,进行参数校验与异常转换。
java
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) { this.userService = userService; }
@GetMapping
public Page<User> list(@RequestParam int page, @RequestParam int size,
@RequestParam(required = false) Integer minAge) {
return userService.pageUsers(page, size, minAge);
}
@GetMapping("/{id}")
public User detail(@PathVariable Long id) {
return userService.getById(id);
}
@PostMapping
public User create(@Valid @RequestBody User body) {
userService.save(body);
return body;
}
@PutMapping("/{id}")
public User update(@PathVariable Long id, @Valid @RequestBody User body) {
body.setId(id);
userService.updateById(body);
return body;
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
userService.removeById(id);
}
}
8. 实用补充与优化建议
- 参数校验(Jakarta Validation)
- 在实体/DTO 上使用
@NotBlank/@NotNull/@Email/@Min
等注解;控制器方法参数加@Valid
。
- 在实体/DTO 上使用
- 统一异常处理
- 通过
@RestControllerAdvice
+@ExceptionHandler
统一返回错误结构(包含code/message
)。
- 通过
- DTO 与实体分离
- 建议 Controller 使用请求/响应 DTO,Service 处理实体,避免直接暴露数据库结构。
- 分页与排序安全
- 对前端传入的排序字段做白名单;分页
size
设置上限。
- 对前端传入的排序字段做白名单;分页
- 事务管理
- 在需要的 Service 方法上使用
@Transactional
,避免在 Controller 层开启事务。
- 在需要的 Service 方法上使用
9. 小结
- 连接:配置数据源 +
@MapperScan
; - 分页:注册
MybatisPlusInterceptor
的PaginationInnerInterceptor
,使用Page<T>
; - Service:基于
IService/ServiceImpl
封装业务; - Controller:提供 RESTful CRUD 与分页,配合校验与异常处理;
- 查询:
QueryWrapper
/LambdaQueryWrapper
完成单表条件,JOIN 用自定义 SQL(注解/XML)。
作者:xuan
欢迎访问我的博客,获取更多技术文章和教程。