整合 Spring Data JPA
JPA(Hibernate)走的是另一条路:面向对象操作数据库。定义好实体,CRUD 不用写一行 SQL,还能根据方法名自动生成查询。适合快速开发和模型驱动的项目。
第 1 步:依赖与配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: update # 根据实体自动维护表结构
show-sql: true # 打印 SQL(生产关闭)
open-in-view: false # 关闭 OSIV,避免懒加载问题被掩盖
ddl-auto 取值:
| 值 | 行为 | 适用 |
|---|---|---|
create | 每次启动删表重建 | 仅本地玩票 |
update | 只加新字段/新表,不删列 | 开发环境 |
validate | 只校验实体与表是否一致 | 生产环境 |
none | 什么都不做 | 生产环境(推荐配合 Flyway) |
warning
生产环境不要依赖 ddl-auto 改表,用 Flyway/Liquibase 做版本化数据库迁移。
第 2 步:定义实体
package com.example.demo.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 自增主键
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
private String email;
private Integer age;
private Integer status;
@Column(name = "create_time", updatable = false)
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
@PrePersist // 插入前回调
void prePersist() {
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
if (this.status == null) this.status = 1;
}
@PreUpdate // 更新前回调
void preUpdate() {
this.updateTime = LocalDateTime.now();
}
// getter / setter
}
常用实体注解:
| 注解 | 作用 |
|---|---|
@Entity | 声明 JPA 实体 |
@Table(name) | 指定表名 |
@Id + @GeneratedValue | 主键及生成策略(IDENTITY 自增 / SEQUENCE / UUID) |
@Column | 字段映射:列名、长度、唯一、非空 |
@Transient | 该字段不映射到数据库 |
@ManyToOne / @OneToMany | 关联关系 |
@PrePersist / @PreUpdate | 生命周期回调 |
第 3 步:Repository——零 SQL 的 CRUD
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;
public interface UserRepository extends JpaRepository<User, Long> {
// 无需实现,继承即得:
// save / findById / findAll / deleteById / count / existsById ...
}
Service 中直接用:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User create(User user) {
if (userRepository.existsByUsername(user.getUsername())) {
throw new BizException(1002, "用户名已存在");
}
return userRepository.save(user); // 无 id 走 INSERT,有 id 走 UPDATE
}
@Transactional
public User update(Long id, UserUpdateDTO dto) {
User user = userRepository.findById(id)
.orElseThrow(() -> new BizException(1001, "用户不存在"));
if (dto.getEmail() != null) user.setEmail(dto.getEmail());
if (dto.getAge() != null) user.setAge(dto.getAge());
return userRepository.save(user);
}
}
派生查询:方法名即 SQL
按命名规则写方法,JPA 自动生成查询:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
List<User> findByAgeGreaterThanEqual(Integer age);
List<User> findByUsernameLikeAndStatus(String name, Integer status);
List<User> findByCreateTimeBetween(LocalDateTime start, LocalDateTime end);
List<User> findTop10ByOrderByIdDesc();
// 分页 + 排序
Page<User> findByUsernameContaining(String name, Pageable pageable);
}
关键字速查:And / Or / Between / LessThan / GreaterThanEqual / Like / Containing / In / IsNull / OrderBy...Desc / Top10 / Distinct。
调用分页:
Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending());
Page<User> result = userRepository.findByUsernameContaining(name, pageable);
// result.getContent() / getTotalElements() / getTotalPages()
@Query:写 JPQL 或原生 SQL
派生查询搞不定复杂场景时,显式写查询:
public interface OrderRepository extends JpaRepository<Order, Long> {
// JPQL:面向实体和字段,不是表和列
@Query("SELECT o FROM Order o WHERE o.user.id = :userId AND o.amount > :min")
List<Order> findBigOrders(@Param("userId") Long userId, @Param("min") BigDecimal min);
// 原生 SQL
@Query(value = "SELECT * FROM `order` WHERE DATE(create_time) = CURDATE()",
nativeQuery = true)
List<Order> findTodayOrders();
// 修改类操作必须加 @Modifying,且所在方法要有事务
@Modifying
@Query("UPDATE User u SET u.status = 0 WHERE u.id = :id")
int disable(@Param("id") Long id);
}
关联映射
@Entity
@Table(name = "`order`")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // 多订单对一用户;务必 LAZY
@JoinColumn(name = "user_id")
private User user;
private BigDecimal amount;
// getter / setter
}
- 关联一律用
LAZY懒加载,EAGER容易引发 N+1 和级联大查询。 - 访问懒加载属性需在事务/会话内,这就是配置里关
open-in-view后要注意的点。
JPA vs MyBatis 怎么选
| 对比项 | Spring Data JPA | MyBatis |
|---|---|---|
| 开发效率 | 极高,单表零 SQL | 需手写 SQL |
| SQL 控制力 | 弱,生成 SQL 不可控 | 完全掌控,可深度优化 |
| 复杂报表/多表关联 | 写起来别扭 | 得心应手 |
| 数据库迁移成本 | 低(方言切换) | SQL 与数据库耦合 |
| 国内生态 | 一般 | 主流(尤其 MyBatis-Plus) |
经验法则:管理后台、快速原型用 JPA;SQL 复杂、性能敏感、国内团队用 MyBatis。