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;
    }
}
选型建议:如果项目以增删改查为主,业务逻辑不复杂,JPA更省事。如果SQL复杂、需要大量调优,MP更灵活。我个人的经验是——微服务内部用JPA,报表统计类服务用MP。

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默认只回滚RuntimeExceptionError。如果你抛的是受检异常(比如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"),清爽多了。

避坑指南:多数据源下事务要小心。Spring默认事务只绑定一个数据源。跨数据源的事务需要分布式事务方案,比如Seata。我曾经因为没注意这个,导致主库写成功、从库读不到,排查了半天。

4.5 总结与最佳实践

聊了这么多,最后给几个实在的建议:

  • JPA vs MP:没有绝对好坏。团队擅长什么就用什么。我见过用JPA写出几百行SQL的,也见过用MP写出面条式代码的。工具是死的,人是活的。
  • 事务粒度:能小就别大。一个方法里只放必要的数据库操作,别把远程调用、文件操作也包进去。事务时间越长,锁冲突概率越大。
  • 多数据源:能少就别多。每个数据源都意味着额外的连接池、事务管理成本。如果只是读写分离,用ShardingSphere中间件会更省心。
  • 监控:数据访问层一定要加慢SQL日志和监控。我习惯用p6spy打印完整SQL,配合Druid监控连接池状态。

数据访问这块,说白了就是「选对工具、配好事务、管好连接」。你把这些基础打牢了,后面业务逻辑怎么写都不会出大问题。下一章咱们聊聊缓存和消息队列,那又是另一番天地了。