1. 为什么选择MyBatis-Plus作为ORM框架
第一次接触MyBatis-Plus是在2018年的一个电商项目中,当时团队还在使用原生MyBatis。每次新增一个简单的CRUD功能,都需要手动编写Mapper接口、XML映射文件,以及各种重复的SQL语句。直到有一天,项目组里来了一位新同事,他默默地在pom.xml里加了一个依赖,然后我们的开发效率突然提升了50%——这就是MyBatis-Plus给我的第一印象。
MyBatis-Plus(简称MP)是MyBatis的增强工具,在保留MyBatis所有特性的基础上,提供了诸多开箱即用的功能。它最核心的价值在于:让开发者能够用更少的代码实现更多的功能。根据我的项目经验,使用MP后,常规的CRUD操作代码量可以减少70%以上。
提示:虽然MP功能强大,但它并不是要替代MyBatis,而是在其基础上进行扩展。这意味着你可以随时回退到原生MyBatis的写法。
2. 环境搭建与基础配置
2.1 项目初始化
让我们从零开始搭建一个Spring Boot + MyBatis-Plus的项目。我推荐使用Spring Initializr(https://start.spring.io/)快速生成项目骨架:
- 选择Maven项目
- 语言选择Java
- Spring Boot版本建议选择2.7.x(目前最稳定的版本)
- 添加依赖:Spring Web、MyBatis Framework、MySQL Driver
生成项目后,在pom.xml中添加MyBatis-Plus的starter依赖:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
2.2 数据库配置
在application.yml中配置数据库连接(我习惯使用yml格式,因为它层次更清晰):
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mp_demo?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志
这里有个小技巧:在开发环境,我强烈建议开启SQL日志(如上配置)。这样你就能在控制台看到MP实际执行的SQL语句,对于调试和理解MP的工作原理非常有帮助。
2.3 实体类与Mapper
创建一个用户表对应的实体类:
java复制@Data
@TableName("user") // 指定表名
public class User {
@TableId(type = IdType.AUTO) // 主键自增
private Long id;
private String name;
private Integer age;
private String email;
}
创建对应的Mapper接口:
java复制public interface UserMapper extends BaseMapper<User> {
// 无需任何方法,BaseMapper已经提供了基础的CRUD方法
}
最后,别忘了在启动类上添加@MapperScan注解:
java复制@SpringBootApplication
@MapperScan("com.example.demo.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3. 核心功能实战
3.1 基础CRUD操作
MP最强大的特性之一就是内置了通用的CRUD方法。让我们看看如何用一行代码实现各种操作:
java复制@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
// 插入
public void addUser(User user) {
userMapper.insert(user); // 返回影响行数
}
// 根据ID查询
public User getById(Long id) {
return userMapper.selectById(id);
}
// 更新
public void updateUser(User user) {
userMapper.updateById(user);
}
// 删除
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
// 查询所有
public List<User> getAll() {
return userMapper.selectList(null); // null表示无查询条件
}
}
3.2 条件构造器QueryWrapper
QueryWrapper是MP的灵魂之一,它允许你以面向对象的方式构建查询条件:
java复制// 查询年龄大于18且名字包含"张"的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.gt("age", 18)
.like("name", "张");
List<User> users = userMapper.selectList(wrapper);
更推荐使用lambda方式,避免硬编码字段名:
java复制LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.gt(User::getAge, 18)
.like(User::getName, "张");
List<User> users = userMapper.selectList(lambdaWrapper);
3.3 分页查询
MP的分页功能需要先配置分页插件:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
然后就可以愉快地使用分页了:
java复制// 第一页,每页10条
Page<User> page = new Page<>(1, 10);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.gt(User::getAge, 18);
Page<User> result = userMapper.selectPage(page, wrapper);
System.out.println("总记录数:" + result.getTotal());
System.out.println("总页数:" + result.getPages());
System.out.println("当前页数据:" + result.getRecords());
4. 高级特性与最佳实践
4.1 自动填充功能
MP提供了自动填充功能,可以自动处理创建时间、更新时间等字段。首先在实体类中添加字段和注解:
java复制@Data
public class User {
// ...其他字段
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
然后实现MetaObjectHandler接口:
java复制@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
4.2 逻辑删除
在实际项目中,我们通常不会真正删除数据,而是使用逻辑删除。配置步骤如下:
- 在application.yml中配置:
yaml复制mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
- 在实体类中添加deleted字段:
java复制@TableLogic
private Integer deleted;
配置完成后,调用deleteById方法时,MP会自动改为执行UPDATE语句设置deleted=1,而查询时会自动加上deleted=0的条件。
4.3 性能优化建议
-
避免N+1查询问题:使用@TableField(exist = false)标注非数据库字段,然后手动处理关联查询。
-
批量操作:对于批量插入,使用saveBatch方法而不是循环调用insert:
java复制List<User> userList = ...;
userService.saveBatch(userList, 1000); // 每批1000条
- SQL优化:对于复杂查询,仍然推荐使用XML或注解方式编写自定义SQL。
5. 常见问题排查
5.1 字段映射问题
如果遇到"Unknown column 'xxx' in 'field list'"错误,通常是因为:
- 数据库字段名与实体类属性名不一致,可以使用@TableField("db_column")指定
- 使用了MP的驼峰转下划线功能但数据库字段不符合规范
5.2 分页不生效
确保:
- 正确配置了分页插件
- Page对象是第一个参数
- 返回值是Page类型而不是List
5.3 事务问题
MP本身不处理事务,需要结合Spring的@Transactional使用。特别注意:在同一个事务中,先插入再查询可能看不到刚插入的数据(与数据库隔离级别有关)。
6. 项目实战建议
经过多个项目的实践,我总结出以下经验:
-
适度使用原则:MP很强大,但不要强迫所有SQL都用它的方式实现。复杂查询还是应该用原生MyBatis的方式。
-
代码生成器:MP提供了代码生成器,可以快速生成Entity、Mapper、Service、Controller等代码。对于标准CRUD功能可以节省大量时间。
-
多数据源支持:如果需要连接多个数据库,可以使用dynamic-datasource-spring-boot-starter,它与MP完美兼容。
-
租户数据隔离:对于SaaS系统,可以通过实现TenantLineInnerInterceptor来实现租户数据自动隔离。
最后提醒一点:虽然MP大大简化了开发,但作为开发者,我们仍然需要理解它背后的SQL原理。只有这样,当遇到性能问题时才能快速定位和优化。
