从3天开发到10分钟交付,优雅应对复杂业务需求的架构实践。
开篇:被CRUD支配的恐惧
深夜的办公室里,实习生小张面对满屏的SQL映射文件几近崩溃:
“第17个模糊查询接口,字段名又写错了...”
你接过键盘,看着GoodsMapper.xml中重复的<select>、<insert>、<update>标签——
这些千篇一律的CRUD代码,正吞噬着团队70%的开发时间。
今天,你将用MyBatis Plus(MP)开启一场生产力革命。
第一章:传统MyBatis的七宗罪——为什么我们需要MP
1.1 CRUD代码的重复炼狱
<!-- 典型MyBatis映射文件片段 -->
<select id="selectById" resultType="Goods">
SELECT * FROM goods WHERE id=#{id}
</select>
<insert id="insert" parameterType="Goods">
INSERT INTO goods(name,price) VALUES(#{name},#{price})
</insert>
<!-- 20个类似方法后... -->
痛点清单:
- 字段硬编码:数据库变更需全局搜索替换。
- SQL错误:price拼成prcie导致生产事故。
- 分页复杂:每个分页查询重复编写count语句。
1.2 MP的降维打击能力
MP不是语法糖,而是将CRUD抽象为声明式编程,让开发者专注业务逻辑。
第二章:10分钟极速交付——商品模块实战
下面用厨房烹饪比喻,带你感受MP的高效魔法。
2.1 食材准备:依赖配置(2分钟)
// 关键依赖
dependencies {
implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.3'
implementation 'com.baomidou:mybatis-plus-generator:3.5.3'
implementation 'org.freemarker:freemarker:2.3.32' // 代码生成模板
}
# application.yml 核心配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发期SQL日志
global-config:
db-config:
id-type: assign_id # 分布式ID策略
logic-delete-field: deleted # 逻辑删除字段
2.2 切菜备料:实体类设计(3分钟)
@Data // Lombok自动生成getter/setter
@TableName("goods") // 绑定表名
public class Goods {
@TableId(type = IdType.ASSIGN_ID) // 分布式雪花ID
private Long id;
@TableField("goods_name") // 字段映射
@NotBlank(message = "商品名不能为空")
private String name;
@TableField
@DecimalMin(value = "0.01", message = "价格必须≥0.01")
private BigDecimal price;
@TableField(fill = FieldFill.INSERT) // 自动填充
private LocalDateTime createTime;
@TableLogic // 逻辑删除标记
private Integer deleted;
}
设计哲学:
- @TableField解耦字段与列名,避免硬编码。
- 校验注解直接嵌入实体,实现声明式数据验证。
- 自动填充减少业务无关代码。
2.3 烹饪核心:Mapper与Service(3分钟)
// ███ 只需声明接口 ███
public interface GoodsMapper extends BaseMapper<Goods> {
// 复杂查询后期扩展
@Select("SELECT * FROM goods WHERE price > #{minPrice} ORDER BY sales DESC")
List<Goods> selectHotGoods(BigDecimal minPrice);
}
// ███ 服务层继承增强接口 ███
@Service
public class GoodsService extends ServiceImpl<GoodsMapper, Goods> {
// 1. 自带全套CRUD方法
// save(), removeById(), updateById(), getById(), page()...
// 2. 自定义业务方法
public List<Goods> searchGoods(String keyword) {
return lambdaQuery()
.like(Goods::getName, keyword)
.or()
.apply("JSON_CONTAINS(keywords, '" + keyword + "')") // JSON字段查询
.list();
}
}
魔法解析:Service继承树提供150+开箱即用方法,覆盖90%业务场景。
2.4 出锅装盘:控制器暴露API(2分钟)
@RestController
@RequestMapping("/goods")
public class GoodsController {
@Autowired
private GoodsService goodsService;
// ███ 分页查询 ███
@GetMapping("/page")
public Page<Goods> page(
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String name) {
return goodsService.page(new Page<>(current, size),
Wrappers.<Goods>lambdaQuery()
.like(StringUtils.isNotBlank(name), Goods::getName, name)
);
}
// ███ 逻辑删除 ███
@DeleteMapping("/{id}")
public boolean delete(@PathVariable Long id) {
return goodsService.removeById(id); // 自动触发逻辑删除
}
}
关键进步:相比传统Controller减少70%代码量。
第三章:生产级精进——复杂业务的优雅应对
基础CRUD只是起点,真实业务需要更强大的武器库。
3.1 条件构造器:动态SQL的终极方案
public Page<Goods> complexQuery(GoodsQuery query) {
return goodsService.lambdaQuery()
.like(StringUtils.isNotBlank(query.getKeyword()), Goods::getName, query.getKeyword())
.gt(query.getMinPrice() != null, Goods::getPrice, query.getMinPrice())
.lt(query.getMaxPrice() != null, Goods::getPrice, query.getMaxPrice())
.in(!CollectionUtils.isEmpty(query.getCategoryIds()), Goods::getCategoryId, query.getCategoryIds())
.eq(Goods::getStatus, 1) // 上架商品
.orderByDesc(Goods::getSales) // 销量排序
.page(new Page<>(query.getPage(), query.getSize()));
}
优势对比:
方案 | 可读性 | 防SQL注入 | 代码量 |
XML动态SQL | 差 | 需手动防 | 多 |
MP条件构造器 | 优 | 自动防护 | 少60% |
3.2 自定义SQL:应对极端复杂场景
<!-- 位于GoodsMapper.xml -->
<select id="selectGoodsWithCategory" resultType="GoodsVO">
SELECT g.*, c.name as categoryName
FROM goods g
LEFT JOIN category c ON g.category_id = c.id
WHERE g.id = #{id}
</select>
// Mapper接口增强
public interface GoodsMapper extends BaseMapper<Goods> {
// ███ 注解方式 ███
@Select("SELECT * FROM goods WHERE category_id = #{categoryId}")
List<Goods> selectByCategory(@Param("categoryId") Long categoryId);
// ███ XML方式 ███
GoodsVO selectGoodsWithCategory(Long id);
}
设计平衡:80%简单操作用MP,20%复杂查询保留原生MyBatis灵活性。
3.3 多租户架构:SAAS系统必备
public class TenantInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms,
Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) {
// 自动注入租户ID
String sql = boundSql.getSql();
if (sql.contains("tenant_id")) return;
String newSql = "SELECT * FROM (" + sql + ") tmp WHERE tenant_id = " + getCurrentTenantId();
ReflectionUtils.setFieldValue(boundSql, "sql", newSql);
}
}
安全策略:在SQL解析层自动隔离租户数据,避免越权风险。
第四章:工程化实践——从玩具到生产级应用
4.1 分层架构规范
src/main/java
├── controller # 控制层
├── service # 业务层
│ ├── impl # 实现类
├── mapper # 数据层
├── entity # 实体类
├── dto # 数据传输对象
│ ├── request # 入参DTO
│ └── response # 出参DTO
└── config # 配置类
└── MybatisPlusConfig.java
4.2 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理数据校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
String message = result.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
return Result.fail(400, message);
}
// 处理MyBatisPlus异常
@ExceptionHandler(MybatisPlusException.class)
public Result handleMybatisPlusException(MybatisPlusException e) {
return Result.fail(500, "数据操作失败: " + e.getMessage());
}
}
4.3 性能优化四板斧
优化点 | 方案 | 效果 |
分页插件 | 启用PageOptimize模式 | 减少1次count查询 |
二级缓存 | 整合Redis+注解@CacheNamespace | 查询速度提升8倍 |
批量操作 | saveBatch()设置批次大小1000 | 插入速度提升15倍 |
SQL打印 | 生产环境关闭log-impl | 降低CPU消耗30% |
第五章:代码生成器——终结重复劳动的核武器
5.1 生成器配置(核心代码)
FastAutoGenerator.create(dataSourceConfig)
.globalConfig(builder -> builder.author("DevSquad") // 作者
.outputDir("D://gen-code") // 输出目录
.enableSwagger() // 开启swagger
)
.packageConfig(builder -> builder.parent("com.example.mall") // 包名
.moduleName("product") // 模块名
)
.strategyConfig(builder -> builder.addInclude("goods", "category") // 表名
.entityBuilder().enableLombok() // 实体策略
.controllerBuilder().enableRestStyle() // REST风格
.mapperBuilder().enableMapperAnnotation() // @Mapper
)
.templateEngine(new FreemarkerTemplateEngine()) // 模板引擎
.execute();
5.2 生成效果
D://gen-code
├── goods
│ ├── GoodsController.java
│ ├── GoodsService.java
│ ├── GoodsServiceImpl.java
│ ├── GoodsMapper.java
│ ├── Goods.java # 实体类
│ └── GoodsDTO.java # DTO对象
└── category # 相同结构
效率对比:手动创建1个实体模块需30分钟 → 生成器10秒完成。
终章:CRUD的涅槃重生
当小张看到自动生成的代码时,眼睛亮了起来:
“原来商品模块可以这么快完成!”
你指着屏幕上的分层代码说:
“记住,真正强大的不是框架本身,而是它让我们摆脱低级重复,
将创造力倾注在真正改变业务的复杂逻辑上”。
系列预告
下一篇:《SpringCloud alibaba:微服务架构生死局》。