springboot

数据访问:JPA 与 MyBatis

By AI-Writer 15 min read

数据访问:JPA 与 MyBatis

数据持久层是任何业务系统的核心。Spring Boot 支持多种持久化方案,其中 Spring Data JPAMyBatis-Plus 是最流行的两种选择。本文将系统讲解两者的用法、优劣对比,以及事务管理的精髓。

数据源配置

Spring Boot 自动配置 DataSource,只需在 application.yml 中声明连接信息:

yaml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/blog?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: secret
    driver-class-name: com.mysql.cj.jdbc.Driver
    # HikariCP 连接池(默认已自动配置,性能最优)
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000

Spring Boot 3.x 默认使用 HikariCP 连接池(性能优于 Tomcat JDBC 和 DBCP2),无需额外引入依赖。

Spring Data JPA

Spring Data JPA 是 Spring Data 家族的一员,它通过接口方法名推导机制大幅简化了 DAO 层的代码量。

依赖引入

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Boot 3 使用 Jakarta Persistence API -->
<!-- 不再使用 javax.persistence.*,而是 jakarta.persistence.* -->

实体映射

java
package com.example.demo.entity;

import jakarta.persistence.*;  // 注意:Spring Boot 3 使用 jakarta.* 而非 javax.*

@Entity
@Table(name = "t_user", indexes = {
        @Index(name = "idx_email", columnList = "email"),
        @Index(name = "idx_status", columnList = "status")
})
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(length = 100)
    private String email;

    @Column(length = 20)
    private String phone;

    @Enumerated(EnumType.STRING)  // 存储为字符串而非数字
    @Column(nullable = false)
    private UserStatus status = UserStatus.ACTIVE;

    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(nullable = false)
    private LocalDateTime updatedAt;

    @Version  // 乐观锁版本字段,防止并发更新冲突
    private Long version;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

    // getters and setters (或使用 record)
}

public enum UserStatus {
    ACTIVE, INACTIVE, BANNED
}

Repository 接口

Spring Data JPA 的核心魔力在于只需要声明方法签名,框架自动实现

java
package com.example.demo.repository;

import com.example.demo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {

    // ==================== 方法名推导(自动实现)====================

    // SELECT * FROM t_user WHERE email = ?
    Optional<User> findByEmail(String email);

    // SELECT * FROM t_user WHERE username LIKE ? AND status = ?
    List<User> findByUsernameContainingAndStatus(String username, UserStatus status);

    // SELECT * FROM t_user WHERE status = ? ORDER BY created_at DESC
    List<User> findByStatusOrderByCreatedAtDesc(UserStatus status);

    // SELECT * FROM t_user WHERE status IN (?, ?)
    List<User> findByStatusIn(List<UserStatus> statuses);

    // SELECT COUNT(*) FROM t_user WHERE status = ?
    long countByStatus(UserStatus status);

    // SELECT EXISTS(SELECT 1 FROM t_user WHERE email = ?)
    boolean existsByEmail(String email);

    // SELECT * FROM t_user WHERE age BETWEEN ? AND ?
    List<User> findByAgeBetween(int minAge, int maxAge);

    // DELETE FROM t_user WHERE status = ?
    @Modifying
    @Query("DELETE FROM User u WHERE u.status = :status")
    int deleteByStatus(@Param("status") UserStatus status);

    // ==================== @Query 自定义查询 ====================

    // JPQL(操作实体而非表)
    @Query("SELECT u FROM User u WHERE u.email = :email AND u.status = :status")
    Optional<User> findByEmailAndStatus(
            @Param("email") String email,
            @Param("status") UserStatus status);

    // 原生 SQL
    @Query(value = """
            SELECT u.* FROM t_user u
            LEFT JOIN t_order o ON u.id = o.user_id
            WHERE o.amount > :amount
            """,
            countQuery = "SELECT COUNT(*) FROM t_user u LEFT JOIN t_order o ON u.id = o.user_id WHERE o.amount > :amount",
            nativeQuery = true)
    Page<User> findActiveBuyers(@Param("amount") BigDecimal amount, Pageable pageable);

    // @Query + @Modifying 执行更新操作
    @Modifying
    @Query("UPDATE User u SET u.status = :status WHERE u.lastLoginAt < :cutoff")
    int deactivateStaleUsers(
            @Param("status") UserStatus status,
            @Param("cutoff") LocalDateTime cutoff);

    // 分页查询(自动 COUNT)
    @Query("SELECT u FROM User u WHERE u.username LIKE %:keyword%")
    Page<User> search(@Param("keyword") String keyword, Pageable pageable);

    // 投影查询:只查部分字段,减少数据传输
    @Query("SELECT new com.example.demo.dto.UserBrief(u.id, u.username, u.email) FROM User u WHERE u.status = :status")
    List<UserBrief> findBriefByStatus(@Param("status") UserStatus status);
}

JpaSpecificationExecutor 动态查询

当查询条件需要动态组合时,JpaSpecificationExecutor 提供了构建 Specification 的能力:

java
@Service
public class UserSearchService {

    @Autowired
    private UserRepository userRepository;

    public Page<User> search(UserSearchCriteria criteria, Pageable pageable) {
        Specification<User> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            if (criteria.getUsername() != null) {
                predicates.add(cb.like(root.get("username"), "%" + criteria.getUsername() + "%"));
            }
            if (criteria.getEmail() != null) {
                predicates.add(cb.equal(root.get("email"), criteria.getEmail()));
            }
            if (criteria.getStatus() != null) {
                predicates.add(cb.equal(root.get("status"), criteria.getStatus()));
            }
            if (criteria.getCreatedAfter() != null) {
                predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), criteria.getCreatedAfter()));
            }

            query.where(predicates.toArray(new Predicate[0]));
            query.orderBy(cb.desc(root.get("createdAt")));
            return query.getRestriction();
        };

        return userRepository.findAll(spec, pageable);
    }
}

MyBatis-Plus

MyBatis-Plus 是 MyBatis 的增强工具,提供无 XML 的 CRUD 接口和强大的条件构造器。

依赖与配置

xml
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.7</version>
</dependency>
yaml
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0
  configuration:
    map-underscore-to-camel-case: true

实体映射

java
@MapperScan("com.example.demo.mapper")  // 启动类或配置类上标注
@TableName("t_article")
public class Article {

    @TableId(type = IdType.AUTO)
    private Long id;

    @TableField("title")
    private String title;

    @TableField("content")
    private String content;

    @TableField("author_id")
    private Long authorId;

    @TableField("status")
    private String status;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdAt;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedAt;

    @TableLogic  // 逻辑删除:执行 UPDATE SET deleted = 1 而非 DELETE
    private Integer deleted;
}

Mapper 接口

java
@Mapper
public interface ArticleMapper extends BaseMapper<Article> {

    // MyBatis-Plus 已提供 CRUD 方法(save/saveBatch/updateById/removeById 等)
    // 可直接注入 ArticleService 使用,或注入 BaseMapper<Article>

    // 自定义查询:XML 或注解
    List<Article> findByAuthorId(@Param("authorId") Long authorId);

    @Select("SELECT * FROM t_article WHERE status = #{status} ORDER BY created_at DESC LIMIT #{limit}")
    List<Article> findRecentByStatus(@Param("status") String status, @Param("limit") int limit);
}

Lambda 条件构造器

MyBatis-Plus 的 LambdaQueryWrapper 让你用方法引用而非字符串字段名写查询条件:

java
@Service
public class ArticleServiceImpl {

    @Autowired
    private ArticleMapper articleMapper;

    public IPage<Article> search(String keyword, String status, long page, long size) {
        return articleMapper.selectPage(
                new Page<>(page, size),
                new LambdaQueryWrapper<Article>()
                        .like(keyword != null, Article::getTitle, keyword)
                        .or()
                        .like(keyword != null, Article::getContent, keyword)
                        .eq(status != null, Article::getStatus, status)
                        .orderByDesc(Article::getCreatedAt)
        );
    }

    public void publish(Long id, Long authorId) {
        // 链式更新
        articleMapper.update(null, new LambdaUpdateWrapper<Article>()
                .eq(Article::getId, id)
                .eq(Article::getAuthorId, authorId)
                .set(Article::getStatus, "PUBLISHED")
                .set(Article::getUpdatedAt, LocalDateTime.now())
        );
    }
}

分页插件

java
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

JdbcTemplate 原始查询

对于简单场景,直接使用 JdbcTemplate 更轻量:

java
@Service
public class StatService {

    private final JdbcTemplate jdbcTemplate;

    public StatService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public long countUsers() {
        return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM t_user", Long.class);
    }

    public List<User> findActiveUsers() {
        return jdbcTemplate.query(
                "SELECT * FROM t_user WHERE status = 'ACTIVE'",
                (rs, rowNum) -> new User(
                        rs.getLong("id"),
                        rs.getString("username"),
                        rs.getString("email")
                )
        );
    }

    public int updateStatus(Long id, String status) {
        return jdbcTemplate.update(
                "UPDATE t_user SET status = ? WHERE id = ?",
                status, id
        );
    }
}

事务管理

Spring 的声明式事务通过 @Transactional 注解实现,是最常用的方式。

@Transactional 基础

java
@Service
public class TransferService {

    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        accountRepository.deduct(fromId, amount);       // 扣款
        accountRepository.deposit(toId, amount);          // 存款
        notificationService.notify(fromId, toId, amount); // 发送通知
        // 若任意一步抛异常,整个事务自动回滚
    }
}

事务传播行为

事务传播行为定义了当一个事务方法被另一个事务方法调用时,事务如何传播

传播行为说明
REQUIRED(默认)加入现有事务,若无则创建新事务
REQUIRES_NEW每次都创建新事务,挂起现有事务
SUPPORTS有事务则加入,无则非事务执行
MANDATORY必须在现有事务中运行,否则抛异常
NOT_SUPPORTED非事务执行,挂起现有事务
NEVER非事务执行,若有事务则抛异常
NESTED嵌套事务(MySQL Savepoint)
java
@Service
public class OrderService {

    @Transactional
    public void placeOrder(Order order) {
        orderRepository.save(order);
        inventoryService.reserve(order.getItems());  // REQUIRES_NEW:独立事务
        paymentService.process(order.getPayment());   // 加入当前事务
    }
}

@Service
public class InventoryService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void reserve(List<OrderItem> items) {
        // 无论 OrderService 事务是否成功,此事务独立提交或回滚
        items.forEach(item -> itemRepository.decrementStock(item));
    }
}

事务隔离级别

java
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    // READ_UNCOMMITTED:读未提交(可能有脏读)
    // READ_COMMITTED:读已提交(Oracle 默认)
    // REPEATABLE_READ:可重复读(MySQL InnoDB 默认)
    // SERIALIZABLE:串行化(最高隔离,性能最差)
}

事务失效场景

@Transactional 在以下场景会失效

java
@Service
public class BadExample {

    // ❌ private 方法:Spring AOP 无法代理 private 方法
    @Transactional
    private void doSomething() { ... }

    // ❌ 自调用:同类内部方法调用不走代理
    @Transactional
    public void methodA() {
        methodB();  // this.methodB(),不走 Spring 代理,事务不生效
    }

    @Transactional
    public void methodB() { ... }

    // ❌ 异常被 catch 吞掉:不会触发回滚
    @Transactional
    public void methodC() {
        try {
            doSomething();
        } catch (Exception e) {
            // 异常被吞掉,事务不会回滚
        }
    }

    // ✅ 解决方案1:注入自身(代理自调用)
    @Autowired
    private BadExample self;

    public void fixedMethodA() {
        self.methodB();  // 通过代理调用,事务生效
    }

    // ✅ 解决方案2:抛出原异常
    @Transactional(rollbackFor = Exception.class)
    public void fixedMethodC() throws Exception {
        try {
            doSomething();
        } catch (Exception e) {
            throw new RuntimeException(e);  // 原异常被重新抛出
        }
    }
}

JPA vs MyBatis-Plus 选型

维度Spring Data JPAMyBatis-Plus
SQL 编写自动推导,JPAQL手写 SQL,条件构造器辅助
学习曲线较陡(需理解 JPA 规范)平缓(熟悉 SQL 即可)
灵活性一般高(完全掌控 SQL)
动态 SQL较弱强大(XML / 注解 / Lambda)
生态Spring 官方社区活跃,国产友好
适用场景复杂对象模型,变更频繁复杂查询,性能敏感场景

小结

  • Spring Boot 自动配置 HikariCP 连接池,只需声明 spring.datasource 配置
  • Spring Data JPA 通过方法名推导@Query 注解大幅简化 DAO 层
  • @Enumerated(EnumType.STRING) 避免枚举存储为数字导致可读性问题
  • @Version 字段实现乐观锁,防止并发更新覆盖
  • MyBatis-Plus 的 LambdaQueryWrapper 用方法引用替代字符串字段名,防误写
  • @Transactional 默认传播行为是 REQUIRED,默认隔离级别是数据库默认值
  • 事务失效的三大场景:private 方法、自调用、异常被吞

下一篇文章我们将学习 多环境配置与外部化配置,掌握 Spring Boot 配置体系的精髓。

#springboot #jpa #mybatis #spring-data-jpa #jdbc #transaction

评论

A

Written by

AI-Writer

Related Articles

springboot
#3

依赖注入与 Bean 管理

深入理解 Spring IoC 容器核心概念、@Bean/@Component/@Configuration 注解使用、构造器注入与 Setter 注入、@Autowired 与 @Qualifier 精确注入、Bean 作用域与优先级控制

Read More
springboot
#5

数据访问:JPA 与 MyBatis

Spring Data JPA 实体映射与 Repository 接口、MyBatis-Plus 增强用法、JdbcTemplate 原始查询、事务管理(@Transactional 传播行为与隔离级别)完整指南

Read More
springboot
#7

Spring Security 安全认证

Spring Security 6.x 核心架构、认证与授权概念、基于 JWT 的无状态登录实现、OAuth 2.0 资源服务器入门、@PreAuthorize 方法级安全注解

Read More