4、Spring Boot数据访问:Spring Data JPA、MyBatis-Plus集成、事务管理、多数据源配置
数据访问这块,说实在的,是每个企业级项目的核心命脉。我这些年带团队做项目,见过太多因为数据层没选好、配置不当导致线上翻车的情况。今天咱们就把Spring Boot里数据访问的几大块——JPA、MyBatis-Plus、事务、多数据源——一次性讲透。
4.1 Spring Data JPA:面向对象的持久化方案
先聊聊JPA。我个人习惯把JPA理解成「用Java对象思维操作数据库」。你不需要写SQL,只需要定义实体类,框架自动帮你生成CRUD语句。
4.1.1 快速集成
引入依赖很简单:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
配置一下数据源和JPA属性:
spring.datasource.url=jdbc:mysql://localhost:3306/demo
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
这里有个坑——ddl-auto在生产环境千万别用update。我曾经接手过一个项目,线上库表结构被自动改了,差点丢数据。生产环境请用validate或者手动管理。
4.1.2 实体与Repository
定义一个实体类:
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
// getter/setter 省略
}
再写个Repository接口:
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
List<User> findByAgeBetween(Integer min, Integer max);
}
你想想看,就这两步,CRUD全有了。JPA最爽的地方就是方法命名查询——方法名本身就是SQL。比如findByNameAndAge,框架自动解析成where name=? and age=?。
@Query注解写JPQL,可读性更好。比如多表关联时,JPQL比方法命名清晰得多。
4.2 MyBatis-Plus:SQL控的福音
如果你团队里DBA比较多,或者你本身喜欢精细控制SQL,那MyBatis-Plus会更顺手。说白了,JPA是「对象驱动」,MP是「SQL驱动」。
4.2.1 集成与配置
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
配置上几乎零成本,只要数据源配好就行。MP会自动扫描Mapper接口。
4.2.2 核心用法
实体类:
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
}
Mapper接口:
public interface UserMapper extends BaseMapper<User> {
// 继承BaseMapper后,CRUD方法全有了
}
MP最让我喜欢的是LambdaQueryWrapper,写条件查询特别优雅:
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三")
.ge(User::getAge, 18);
List<User> list = userMapper.selectList(wrapper);
嗯,这里要注意——MP的分页插件需要单独配置:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
4.3 事务管理:别让数据不一致
事务这块,说白了就是「要么全做,要么全不做」。Spring Boot里用@Transactional注解就能搞定。
4.3.1 基本用法
@Service
public class UserService {
@Transactional
public void createUserWithOrder(User user, Order order) {
userMapper.insert(user);
orderMapper.insert(order);
// 如果这里抛异常,两个插入都会回滚
}
}
为什么需要事务?我举个例子——转账操作。扣钱成功,加钱失败,这账就对不上了。事务就是保证这种场景下数据的一致性。
4.3.2 事务传播行为
这块容易搞混,我列个表:
| 传播行为 | 说明 |
|---|---|
| REQUIRED | 默认值。有事务就用,没有就新建 |
| REQUIRES_NEW | 每次都新建事务,挂起当前事务 |
| NESTED | 嵌套事务,子事务回滚不影响父事务 |
| SUPPORTS | 有事务就用,没有就算了 |
我曾经在项目中遇到过REQUIRES_NEW的坑——日志记录和业务操作放在同一个事务里,业务回滚了,日志也没了。后来改成REQUIRES_NEW,日志独立提交,问题解决。
@Transactional默认只回滚RuntimeException和Error。如果你抛的是受检异常(比如IOException),需要指定rollbackFor = Exception.class。
4.4 多数据源配置:读写分离与分库
企业级项目里,单数据源往往不够用。读写分离、多库分片是常态。Spring Boot支持多数据源,但配置起来需要点技巧。
4.4.1 静态多数据源
先配置两个数据源:
spring.datasource.primary.url=jdbc:mysql://master:3306/db
spring.datasource.primary.username=root
spring.datasource.secondary.url=jdbc:mysql://slave:3306/db
spring.datasource.secondary.username=root
然后定义配置类:
@Configuration
public class DataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
}
使用时通过@Qualifier指定:
@Service
public class UserService {
@Autowired
@Qualifier("primaryDataSource")
private DataSource primaryDataSource;
}
4.4.2 动态数据源(AOP方式)
静态方式太死板。我更喜欢用AOP+ThreadLocal实现动态切换:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
再配合注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default "master";
}
切面里根据注解值切换数据源。这样业务代码里只需要加个@DataSource("slave"),清爽多了。
4.5 总结与最佳实践
聊了这么多,最后给几个实在的建议:
- JPA vs MP:没有绝对好坏。团队擅长什么就用什么。我见过用JPA写出几百行SQL的,也见过用MP写出面条式代码的。工具是死的,人是活的。
- 事务粒度:能小就别大。一个方法里只放必要的数据库操作,别把远程调用、文件操作也包进去。事务时间越长,锁冲突概率越大。
- 多数据源:能少就别多。每个数据源都意味着额外的连接池、事务管理成本。如果只是读写分离,用ShardingSphere中间件会更省心。
- 监控:数据访问层一定要加慢SQL日志和监控。我习惯用
p6spy打印完整SQL,配合Druid监控连接池状态。
数据访问这块,说白了就是「选对工具、配好事务、管好连接」。你把这些基础打牢了,后面业务逻辑怎么写都不会出大问题。下一章咱们聊聊缓存和消息队列,那又是另一番天地了。