Spring Data JPA 实战指南:从基础配置到高级技巧

一、项目结构与职责划分

  • 实体类(Entity) :写 @ManyToOne@OneToMany 等关系映射注解
  • Repository 接口 :写 @EntityGraph@Query 等查询定制注解
  • 测试类 / Service:只负责调用方法,不写这些配置

二、核心组件实现

2.1 JpaApplication 启动类

java 复制代码
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.example.repository")
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}

注解说明:

  • @SpringBootApplication:组合注解,包含 @Configuration@EnableAutoConfiguration@ComponentScan,用于启动 Spring Boot 应用
  • @EnableJpaRepositories:启用 Spring Data JPA 仓库功能,指定扫描 com.example.repository 包下的仓库接口
  • main 方法:启动 Spring Boot 应用

2.2 User 实体类

java 复制代码
@Entity
@Table(name = "sys_user")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "username", length = 50, nullable = false, unique = true)
    private String username;
    @Column(name = "email", length = 100)
    private String email;
    @Column(name = "age")
    private Integer age;
    @Enumerated(EnumType.STRING)
    @Column(name = "status", length = 20)
    private UserStatus status = UserStatus.ACTIVE;
    @Column(name = "create_time", updatable = false)
    private LocalDateTime createTime;
    @Column(name = "update_time")
    private LocalDateTime updateTime;
    @PrePersist
    public void prePersist() {
        this.createTime = LocalDateTime.now();
        this.updateTime = LocalDateTime.now();
    }
    @PreUpdate
    public void preUpdate() {
        this.updateTime = LocalDateTime.now();
    }
}

重要提醒:

  • 数据库里的约束 → 是给数据库自己用的
  • Java 代码里的注解 → 是给 JPA 框架用的
  • 👉 JPA 根本不会去读取数据库的表结构、自动识别主键/自增!它没这个功能!

JPA 标准注解详解:

注解 作用
@Entity 告诉 JPA ------ 这个类是实体类,对应数据库一张表
@Table 指定对应的数据库表名为 sys_user
@Data Lombok 注解,自动生成 getter、setter、toString 等方法
@Id 指定主键
@GeneratedValue 指定主键生成策略为自增
@Column 指定字段对应的数据库列名、长度、是否可为空等属性
@Enumerated(EnumType.STRING) 指定枚举类型的存储方式为字符串
@PrePersist / @PreUpdate JPA 的生命周期回调方法,分别在实体插入和更新前执行

2.3 UserStatus 枚举类

java 复制代码
public enum UserStatus {
    ACTIVE, INACTIVE, DELETED
}

定义用户状态的枚举类型。

2.4 UserRepository 接口

java 复制代码
public interface UserRepository extends JpaRepository<User, Long> {
    
    // JpaRepository 已提供: 
    // - save(S entity) / saveAll(Iterable<S> entities)
    // - findById(ID id) / findAll() / findAllById(Iterable<ID> ids)
    // - count() / existsById(ID id)
    // - deleteById(ID id) / delete(T entity) / deleteAll()
    // - findAll(Sort sort) / findAll(Pageable pageable)
    
    // 自定义查询方法 
    User findByUsername(String username); 
    List<User> findByStatus(UserStatus status);
    List<User> findByAgeBetween(Integer min, Integer max); 
    
    @Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
    List<User> findByEmailDomain(@Param("domain") String domain);
}

说明:

  • 继承 JpaRepository,获得基本的 CRUD 操作能力
  • 自定义查询方法,Spring Data JPA 会根据方法名自动生成 SQL 查询

2.5 UserService 服务类

java 复制代码
@Service
@Transactional
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    // 业务方法
    public List<User> listAll() {
        return userRepository.findAll(); 
    } 
    
    public Page<User> listPage(int page, int size) { 
        return userRepository.findAll(PageRequest.of(page, size));
    }
}

说明:

  • @Service:表明这是一个服务层组件
  • @Transactional:开启事务管理
  • 通过 @Autowired 注入 UserRepository,实现用户管理的业务逻辑

2.6 QuickStartTest 测试类

java 复制代码
@SpringBootTest
public class QuickStartTest {
    @Autowired
    private UserRepository userRepository;
    // 测试方法
}

说明:

  • @SpringBootTest:用于启动 Spring Boot 应用上下文进行测试
  • 包含插入查询、更新、删除等测试方法,验证代码的功能

三、实体类映射详解

3.1 基础注解

java 复制代码
@Entity
@Table( 
    name = "sys_user", // 1. 表名(你懂的)
    schema = "mydb", // 2. 数据库名(库名) 
    indexes = { // 3. 定义【普通索引/唯一索引】
        @Index(name = "idx_username", columnList = "username", unique = true),
        @Index(name = "idx_create_time", columnList = "create_time")
    },
    uniqueConstraints = { // 4. 定义【唯一约束】
        @UniqueConstraint(name = "uk_email", columnNames = {"email"}) 
    }
)
@Data
public class User {
    /**
     * @Id - 主键
     * @GeneratedValue - 主键生成策略
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    // GenerationType 可选值:
    // - IDENTITY: 数据库自增(MySQL)
    // - SEQUENCE: 序列(Oracle)
    // - TABLE: 使用单独的表生成主键
    // - AUTO: JPA 自动选择(默认)
    private Long id;
    
    /**
     * @Column - 字段映射
     */
    @Column(name = "username",      // 数据库列名
            length = 50,            // 长度
            nullable = false,       // 是否允许 null
            unique = true,          // 是否唯一
            insertable = true,      // 是否参与 insert
            updatable = true,       // 是否参与 update
            columnDefinition = "VARCHAR(50) COMMENT '用户名'")  // DDL 定义
    private String username;
    
    /**
     * @Enumerated - 枚举映射
     */
    @Enumerated(EnumType.STRING)  // 存储枚举名称
    // EnumType.ORDINAL: 存储枚举序号(默认,不推荐)
    // EnumType.STRING: 存储枚举名称
    @Column(name = "status")
    private UserStatus status;
    
    /**
     * @Temporal - 日期时间映射
     */
    @Temporal(TemporalType.TIMESTAMP)
    // TemporalType.DATE: 只存日期
    // TemporalType.TIME: 只存时间
    // TemporalType.TIMESTAMP: 日期时间
    @Column(name = "create_time")
    private Date createTime;
    
    /**
     * @Lob - 大对象
     */
    @Lob
    @Column(name = "avatar", columnDefinition = "LONGBLOB")
    private byte[] avatar;
    
    @Lob
    @Column(name = "description", columnDefinition = "LONGTEXT")
    private String description;
    
    /**
     * @Transient - 忽略映射,临时字段,只在代码运行时用,用完就丢,不存库
     */
    @Transient
    private String extraField;
    
    /**
     * @Basic - 基本配置
     */
    @Basic(fetch = FetchType.LAZY)  // 延迟加载
    @Column(name = "large_content")
    private String largeContent;
}

@Lob 详解:

  • Lob = Large Object(大对象)
  • 是 JPA 标准注解,专门用来存超大数据,分两种:
    • BLOB(Binary):存二进制数据 → 图片、文件、音频
    • CLOB:存大文本数据 → 超长文章、富文本
  • 作用:告诉 JPA → 这个字段不是普通字符串/数字,是大文件,别按普通字段处理!

两个加载模式:

  1. FetchType.EAGER(默认,立即加载):查询用户时,把所有字段一次性查出来
  2. FetchType.LAZY(延迟加载) :查询用户时,先不查这个大文本字段,等到代码里调用 user.getLargeContent() 时才去数据库查

3.2 复合主键

java 复制代码
@Data
@Embeddable
public class OrderItemId implements Serializable {
    @Column(name = "order_id")
    private Long orderId;
    
    @Column(name = "product_id")
    private Long productId;
}

说明:

  • @Embeddable:我是一个复合主键组件,可以被嵌入到实体类里
  • Serializable:JPA 要求复合主键类必须加(固定写法)
java 复制代码
@Entity
@Table(name = "order_item")
@Data
public class OrderItem {
    @EmbeddedId     // 用这个注解引入复合主键类
    private OrderItemId id;
    
    @Column(name = "quantity")
    private Integer quantity;
    
    @Column(name = "price")
    private BigDecimal price;
}

另一种方式:@IdClass(过时)

java 复制代码
@Entity
@Table(name = "order_item2")
@IdClass(OrderItemId.class)
@Data
public class OrderItem2 {
    @Id
    @Column(name = "order_id")
    private Long orderId;
    
    @Id
    @Column(name = "product_id")
    private Long productId;
    
    @Column(name = "quantity")
    private Integer quantity;
}

复合主键场景:

  • 一个订单下有多个商品
  • 单独 order_id 会重复
  • 单独 product_id 也会重复
  • order_id + product_id 合起来绝对唯一 → 这就是复合主键!

四、关联映射详解

4.1 一对一关联

java 复制代码
@Entity
@Table(name = "user")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;
    
    /**
     * 一对一 - 主表
     */
    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(
        name = "profile_id",        // 当前表(user)的外键列名
        referencedColumnName = "id"  // 关联表(user_profile)的主键
    )
    private UserProfile profile;
}
java 复制代码
@Entity
@Table(name = "user_profile")
@Data
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String realName;
    private String phone;
    
    /**
     * 一对一 - 从表(可选)
     */
    @OneToOne(mappedBy = "profile", fetch = FetchType.LAZY)
    private User user;
}

说明:

  • @OneToOne:定义一对一关联关系。fetch = FetchType.LAZY 表示懒加载,cascade = CascadeType.ALL 表示级联操作
  • @JoinColumn:指定外键列名和关联列名
  • mappedBy:表示关系的被维护方
  • User 有外键 → 用 @JoinColumn
  • UserProfile 没外键 → 用 mappedBy

4.2 一对多关联

java 复制代码
@Entity
@Table(name = "dept")
@Data
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    /**
     * 一对多 - 部门方
     */
    @OneToMany(mappedBy = "department", // 关系由员工方维护
               fetch = FetchType.LAZY,
               cascade = CascadeType.ALL, // 删部门,员工全删 
               orphanRemoval = true) // 把员工从部门移除,员工直接删
    private List<User> users = new ArrayList<>();
}
java 复制代码
@Entity
@Table(name = "user")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;
    
    /**
     * 多对一 - 用户方
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "dept_id")
    private Department department;
}

orphanRemoval vs CascadeType.REMOVE:

  1. CascadeType.REMOVE(级联删除):删父 → 子跟着全删,父对象没了,子才删
  2. orphanRemoval = true(孤儿删除):父还在,只是把子"踢走"(解除关联)→ 子直接删

orphanRemoval = true:谁没人管(孤儿),就把谁清理掉,子对象不能单独存在,脱离父对象就没用了,必须删掉!

4.3 多对多关联

java 复制代码
@Entity
@Table(name = "user")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @EqualsAndHashCode.Include
    private Long id;
    
    private String username;
    
    /**
     * 多对多 - User 方(维护方)
     */
    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    @JoinTable(name = "user_role", // 中间表的表名
               joinColumns = @JoinColumn(name = "user_id"),     // 当前表在中间表的外键列
               inverseJoinColumns = @JoinColumn(name = "role_id")) // 关联表在中间表的外键列
    private Set<Role> roles = new HashSet<>();
}
java 复制代码
@Entity
@Table(name = "role")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @EqualsAndHashCode.Include
    private Long id;
    
    private String name;
    
    /**
     * 多对多 - Role 方(被维护方)
     */
    @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
    private Set<User> users = new HashSet<>();
}

说明:

  • @JoinTable:定义中间表名字 + 两个外键(同时说明了 User 是关系维护方,有外键)
  • joinColumns:当前表(维护方)的外键
  • inverseJoinColumns:对方表的外键
  • mappedBy:标记被维护方,不操作中间表

4.4 关联映射最佳实践

java 复制代码
@SpringBootTest
public class AssociationMappingTest {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private DepartmentRepository departmentRepository;
    
    /**
     * 级联操作
     */
    @Test
    public void testCascade() {
        // 创建部门
        Department dept = new Department();
        dept.setName("技术部");
        
        // 创建用户
        User user1 = new User();
        user1.setUsername("张三");
        user1.setDepartment(dept); // 🔥 关键:给用户绑定部门(维护外键)
        
        User user2 = new User();
        user2.setUsername("李四");
        user2.setDepartment(dept);
        
        // 给部门绑定用户(双向关联,完整绑定关系)
        dept.setUsers(Arrays.asList(user1, user2));
        
        // 🔥 只保存部门!用户自动保存(级联的魔力)
        departmentRepository.save(dept);
    }
}

级联的方向:谁配 cascade,操作谁,谁带动别人!

java 复制代码
@Test 
public void testManyToMany() { 
    // 1. 从数据库查出 id=1 的用户(张三)
    User user = userRepository.findById(1L).orElse(null);
    
    // 2. 构造一个已存在的角色对象(id=1,管理员,数据库里已经有了)
    // 因为角色已存在,不用查库,只需要设置id,JPA就能识别 
    Role role = new Role();
    role.setId(1L); 
    
    // ===================== 第一步:给用户【绑定】角色 ===================== 
    // 把角色添加到用户的角色集合里
    user.getRoles().add(role); 
    // 保存用户(维护方操作 → JPA自动往中间表插数据)
    userRepository.save(user); 
    
    // ===================== 第二步:给用户【解绑】角色 ===================== 
    // 把角色从用户的角色集合里移除 
    user.getRoles().remove(role); 
    // 保存用户(维护方操作 → JPA自动从中间表删数据)
    userRepository.save(user);
}

4.5 N+1 查询问题与解决方案

N+1 问题 = 查一次主表,再循环查 N 次子表,SQL 太多,性能爆炸

原因分析:

  • 因为用了懒加载(LAZY)
  • 查用户时,不查部门(节省性能)
  • 调用 getDepartment() 时,才临时查部门
  • 循环调用 → 循环查库 → N+1

问题代码:

java 复制代码
@Test
public void testNPlusOneProblem() {
    List<User> users = userRepository.findAll();
    for (User user : users) {
        // 每次 getDepartment() 都会发起一次查询
        System.out.println(user.getDepartment().getName());
    }
}

解决方案:

方案 实现方式 适用场景
JOIN FETCH(最常用、最推荐) @Query("SELECT u FROM User u JOIN FETCH u.department") 强制连表查询,一次性把用户+部门全部查出来
EntityGraph(注解版) @EntityGraph(attributePaths = {"department"}) 不用写 SQL
批量抓取(折中方案) @BatchSize(size = 100) 在部门实体的用户集合上加注解 一次批量查100个

五、JPA 继承映射策略

5.1 SINGLE_TABLE(单表继承)

java 复制代码
@Entity
@Table(name = "payment")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
    name = "payment_type",  // 数据库新建字段用作区分不同的子类
    discriminatorType = DiscriminatorType.STRING  // 规定这个字段存字符串(ALIPAY/WECHAT)
)
public abstract class Payment {
    // 公有字段
    private Long id;
    private BigDecimal amount; // 金额
    private LocalDateTime payTime; // 支付时间
}

@Entity
@DiscriminatorValue("ALIPAY")
public class AlipayPayment extends Payment {
    // 子类独有字段
    private String alipayAccount; // 支付宝账号
}

说明:

  • @Inheritance(SINGLE_TABLE) = 父子类共用 1 张表
  • @DiscriminatorColumn = 表中加个类型字段
  • @DiscriminatorValue = 给子类贴个类型标签

5.2 JOINED(联合表继承)

策略对比:

策略 表数量 特点
SINGLE_TABLE(单表) 1 张 所有数据挤一起,有鉴别列,有空字段
JOINED(联合表) 父 1 张 + 子 N 张 表拆分最规范,无空值、无冗余
java 复制代码
@Entity
@Table(name = "vehicle")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Vehicle {
    private Long id;       // 主键
    private String brand;  // 品牌(公共字段)
    private Double price;  // 价格(公共字段)
}

@Entity
@Table(name = "car")
@PrimaryKeyJoinColumn(name = "vehicle_id")  // 修改子类表的主键列名
@Data 
@EqualsAndHashCode(callSuper = true)
public class Car extends Vehicle {
    private Integer seatCount; // 座位数(独有字段)
}

说明:

  • 父类:只存储所有子类共用的字段,单独建一张表 vehicle
  • 子类:只存自己特有的字段,不重复存品牌/价格

5.3 TABLE_PER_CLASS(每类一张表)

java 复制代码
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Document {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)  // 🔥 重点:不能用 IDENTITY
    private Long id;
    private String title;   // 公共字段
    private String content; // 公共字段
}

@Entity
@Table(name = "pdf_document")
public class PdfDocument extends Document {
    private Integer pageCount; // 独有字段
}

说明:

  • 父类是抽象类,不生成数据库表
  • 主键必须用 AUTO,不能用 IDENTITY(因为多张子类表需要全局唯一 ID)
  • AUTO 全局唯一 ID,不依赖单表自增

六、Repository 自定义扩展

6.1 Repository 继承体系

复制代码
Repository (标记接口)
    └── CrudRepository (CRUD 操作)
            └── PagingAndSortingRepository (分页排序)
                    └── JpaRepository (JPA 扩展,最强大)

6.2 自定义 Repository 三步法

第 1 步:定义自定义接口(写方法名)

java 复制代码
public interface CustomUserRepository {
    // 查询所有激活用户
    List<User> findActiveUsers();
    // 批量更新用户状态
    void updateUserStatusBatch(List<Long> userIds, UserStatus status);
}

第 2 步:写实现类(真正干活的地方)

java 复制代码
public class CustomUserRepositoryImpl implements CustomUserRepository {
    @PersistenceContext
    private EntityManager entityManager; // JPA 核心工具
    
    @Override
    public List<User> findActiveUsers() {
        // JPQL 查询:查状态为 ACTIVE 的用户
        return entityManager.createQuery("SELECT u FROM User u WHERE u.status = 'ACTIVE'")
                .getResultList();
    }
}

说明:

  • EntityManager:JPA 原生的、最底层的数据库操作工具,我们平时用的 JpaRepository,底层全是靠它实现的
  • @PersistenceContext:专门用来注入 EntityManager 的注解,作用 ≈ @Autowired,但这是 JPA 专属注解

工具对比:

工具 层级 用法 场景
JpaRepository 高层封装 简单 CRUD、方法名查询 90% 日常开发
EntityManager 底层原生 自定义复杂查询、批量操作 自定义 Repository、高级场景

为什么不用 @Autowired,要用 @PersistenceContext?

  • 因为 EntityManager 不是普通的 Bean,它是与事务绑定的线程安全对象
java 复制代码
@Override
@Transactional
public void updateUserStatusBatch(List<Long> userIds, UserStatus status) {
    entityManager.createQuery(
        "UPDATE User u SET u.status = :status WHERE u.id IN :ids")
        .setParameter("status", status)
        .setParameter("ids", userIds)
        .executeUpdate();
}

链式调用(方法链) :和 MyBatis-Plus 的 LambdaQueryWrapper/QueryWrapper 是完全一样的设计思路!

第 3 步:让你的 UserRepository 继承它

java 复制代码
public interface UserRepository 
        extends JpaRepository<User, Long>, CustomUserRepository {
}

6.3 测试效果

java 复制代码
@SpringBootTest
public class Test {
    @Autowired
    private UserRepository userRepository;
    
    @Test
    public void test() {
        // 1. 用 JPA 默认方法
        userRepository.findById(1L);
        userRepository.findAll();
        
        // 2. 用自定义方法!
        List<User> activeUsers = userRepository.findActiveUsers();
        userRepository.updateUserStatusBatch(Arrays.asList(1L, 2L), UserStatus.ACTIVE);
    }
}

七、查询方法详解

7.1 方法名查询(JPA 最核心、最常用、最推荐)

规则格式: findBy + 属性名 + 关键字 + [And/Or] + 属性名 + 关键字

java 复制代码
public interface UserRepository extends JpaRepository<User, Long> {
    // 1. 等值查询(最最常用)
    User findByUsername(String username);
    
    // 2. 不等查询
    List<User> findByStatusNot(UserStatus status);
    
    // 3. 模糊查询(包含)
    List<User> findByUsernameContaining(String name);
    
    // 4. 范围查询
    List<User> findByAgeBetween(Integer min, Integer max);
    
    // 5. IN 查询(批量状态/ID)
    List<User> findByStatusIn(List<UserStatus> statuses);
    
    // 6. 非空/为空
    List<User> findByEmailIsNull();
    
    // 7. 排序
    List<User> findByStatusOrderByCreateTimeDesc(UserStatus status);
    
    // 8. 分页(必用)
    Page<User> findByStatus(UserStatus status, Pageable pageable);
    
    // 9. 关联对象查询(连表不用写JOIN)
    List<User> findByDepartmentName(String deptName);
    
    // 10. 去重
    List<User> findDistinctByUsername(String username);
    
    // 11. 限制 Limit 5
    User findTop5ByUsername(String username);
}

JPA vs MyBatis-Plus 对比:

操作 MyBatis-Plus 写法 JPA 方法命名查询
用户名 = 张三 eq(User::getUsername, "张三") findByUsername("张三")
用户名包含张 like(User::getUsername, "%张%") findByUsernameContaining("张")
年龄 20-30 between(age, 20, 30) findByAgeBetween(20, 30)
状态在指定列表中 in(status, list) findByStatusIn(list)
按创建时间倒序 orderByDesc(createTime) findByStatusOrderByCreateTimeDesc

7.2 @Query 查询

java 复制代码
public interface UserRepository extends JpaRepository<User, Long> {
    /**
     * 基本 JPQL 查询
     */
    @Query("SELECT u FROM User u WHERE u.username = :username") 
    User findByUsernameJPQL(@Param("username") String username);
}

注意事项:

  1. ⚠️ 更新/删除操作 2 个注解缺一不可
java 复制代码
@Modifying   // 标记这是更新/删除语句
@Transactional  // 必须开启事务(写在测试类/Service层)
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(...);
  1. ⚠️ @Modifying 缓存坑(超级常见)
java 复制代码
@Modifying(clearAutomatically = true, flushAutomatically = true)
// flushAutomatically = true:执行前刷新缓存
// clearAutomatically = true:执行后清空缓存
  1. ⚠️ 关联查询用 JOIN FETCH 解决 N+1
java 复制代码
@Query("SELECT u FROM User u JOIN FETCH u.department WHERE u.id = :id")
// 普通 JOIN 无法解决 N+1,必须用 JOIN FETCH
  1. ⚠️ 原生 SQL 分页必须写 countQuery
java 复制代码
@Query(
    value = "SELECT * FROM user WHERE status = :status",
    countQuery = "SELECT COUNT(*) FROM user WHERE status = :status",
    nativeQuery = true
)
Page<User> findByStatusNative(...);
  1. ⚠️ DTO 投影必须写全限定类名
java 复制代码
@Query("SELECT new com.example.dto.UserDTO(u.id, u.username) FROM User u")
// 类名必须全路径,DTO 必须有对应参数的构造方法
  1. ⚠️ 模糊查询 LIKE 写法
java 复制代码
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword%")
// 不要在传参时手动加 %,直接写在语句里
  1. ⚠️ 批量更新用 IN,参数支持集合
java 复制代码
@Query("UPDATE User u SET u.status = :status WHERE u.id IN :ids")
int batchUpdateStatus(@Param("ids") List<Long> ids);
  1. ⚠️ GROUP BY 分组返回 List<Object[]>
java 复制代码
@Query("SELECT u.status, COUNT(u) FROM User u GROUP BY u.status")
List<Object[]> countGroupByStatus();
// 下标 0 = status,下标 1 = 数量

7.3 极简口诀(记这个就够)

  1. 查询不加 Modifying,更新必须加
  2. 更新必配 Transactional
  3. JPQL 操作实体,SQL 操作表
  4. 原生分页要写 countQuery
  5. 缓存刷新加 clearAutomatically
  6. 关联查询用 JOIN FETCH

7.4 总结

  • @Query 是 JPA 处理复杂查询的终极方案
  • 简单查询:用方法命名(不写 SQL)
  • 复杂/更新 :用 @Query(手写 JPQL/SQL)
  • 只要遵守上面的规则,基本不会出 bug!

延伸阅读:

相关推荐
小徐学编程-zZ7 小时前
Test-mall--后端联调与启动
数据库
一写代码就开心7 小时前
redis-cli 客户端查询set集合里面的具体数据
数据库·redis·缓存
wang3zc8 小时前
mysql如何提升InnoDB写入性能_对比MyISAM的写入锁机制
jvm·数据库·python
YL200404268 小时前
MySQL-基础篇-事务
数据库·mysql
whn19778 小时前
达梦dbms_sql对字段类型的展示
数据库
ITMr.罗9 小时前
【无标题】
数据库
KaMeidebaby9 小时前
卡梅德生物技术快报|细菌 FISH 实验 + 流式细胞术:尿路感染活菌快速定量系统实现与数据验证
前端·数据库·其他·百度·新浪微博
昆曲之源_娄江河畔9 小时前
DBGridEh Footer的使用
前端·数据库·delphi·dbgrideh
邮专薛之谦9 小时前
MySQL 完整SQL指令大全(含详细解释+实战示例)
数据库·sql·mysql
YL200404269 小时前
MySQL-进阶篇-SQL优化
数据库·sql·mysql