概述
- 目标:用最少代码完成单表 CRUD、分页查询,并给出 JOIN 查询的常见做法。
- 环境:Spring Boot 3.x + MyBatis-Plus 3.5+ + MySQL 8.x。
1. 依赖与连接配置
1.1 Maven 依赖(片段)
<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 连接
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 启动类与包扫描
@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 若在接口上使用 @Mapper,可不加 @MapperScan
public class DemoApplication {
public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }
}
1.4 分页插件配置
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2. 示例模型与 Mapper
2.1 表结构(示例)
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
@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 接口
public interface UserMapper extends BaseMapper<User> {}
3. 常见查询(QueryWrapper / Lambda)
3.1 基础查询
@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(更安全的字段引用)
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 按条件可选拼装
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)。
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 中直接返回分页结果:
@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)
表
CREATE TABLE `user_profile` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL UNIQUE, `phone` VARCHAR(32), `address` VARCHAR(255) );
DTO
@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 接口
public interface UserProfileQueryMapper { UserWithProfileDTO selectUserWithProfile(@Param("userId") Long userId); }
XML(resources/mapper/UserProfileQueryMapper.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 方式。
表
CREATE TABLE `order` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `amount` DECIMAL(6,2) );
DTO
@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 接口
public interface UserOrderNestedMapper { UserWithOrdersDTO selectUsersWithOrders(@Param("userId") Long userId); }
XML(resources/mapper/UserOrderNestedMapper.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 关联)
表
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
@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 接口
public interface UserRoleMapper { List<UserWithRolesDTO> selectUsersWithRoles(@Param("userIds") List<Long> userIds); }
XML(resources/mapper/UserRoleMapper.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 能力并封装业务逻辑。
public interface UserService extends IService<User> {
Page<User> pageUsers(int page, int size, Integer minAge);
}
@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,进行参数校验与异常转换。
@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)。
2 条评论
好像懂了
很棒