
那天下午,产品经理又双叒叕提了一个"简单"的需求:给用户列表加个按注册时间、活跃度、地区的复合查询功能。我看了看现有的JDBC代码,心里一万匹草泥马奔腾而过——又得写一堆PreparedStatement,又得手工拼接SQL,还得处理各种异常…
直到遇见了Spring Data JPA,我的数据库操作人生彻底改变了。
说白了,传统的JDBC就像用记事本写代码——能用,但效率感人。每次写个简单的查询都得:
// 传统JDBC的痛苦回忆
String sql = "SELECT * FROM user WHERE age > ? AND status = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setInt(, );
ps.setString(, "ACTIVE");
ResultSet rs = ps.executeQuery();
// 还得手工把ResultSet转成对象...
而用了Spring Data JPA后,同样的逻辑变成了:
// JPA的优雅写法
List<User> users = userRepository.findByAgeGreaterThanAndStatus(, "ACTIVE");
看到区别了吗?一行代码搞定!不需要写SQL,不需要处理连接,不需要手工映射。JPA帮你把所有脏活累活都干了。
JPA最核心的思想就是对象关系映射(ORM)。简单说,就是让数据库表和Java对象一一对应。
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", unique = true, nullable = false)
private String username;
@Column(name = "email")
private String email;
@Enumerated(EnumType.STRING)
private UserStatus status;
@CreationTimestamp
private LocalDateTime createdAt;
// getter/setter省略...
}
这些注解看起来多,但每个都有自己的使命:
@Entity告诉JPA这是个数据库实体@Table指定对应的表名@Id标识主键@GeneratedValue让主键自增@Column定义字段属性配置一次,终身受益。
Spring Data JPA最强大的地方在于Repository接口。你只需要继承JpaRepository,就自动获得了增删改查的全套功能:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 方法名查询 - Spring会自动解析方法名生成SQL
List<User> findByStatus(UserStatus status);
List<User> findByUsernameContaining(String keyword);
List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
// 自定义查询
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
// 原生SQL(实在搞不定的时候用)
@Query(value = "SELECT * FROM users WHERE YEAR(created_at) = ?1", nativeQuery = true)
List<User> findByYear(int year);
}
方法名查询是JPA的黑魔法。你按照约定的规则命名方法,Spring会自动帮你生成对应的SQL。比如:
findBy + 字段名:查询countBy + 字段名:统计deleteBy + 字段名:删除GreaterThan、LessThan、Between、Like:各种条件我刚学会这个特性的时候,兴奋得像发现了新大陆,连续写了十几个查询方法,感觉自己就是SQL之神。
真实业务中,表与表之间肯定有关联。JPA提供了@OneToOne、@OneToMany、@ManyToOne、@ManyToMany四种关联注解:
@Entity
public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Order> orders;
@ManyToMany
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles;
}
这里有个坑要注意:懒加载(FetchType.LAZY)是个双刃剑。用得好能提升性能,用不好会遇到LazyInitializationException。我的建议是,除非你确定需要立即加载关联数据,否则默认用LAZY。
动态查询是个常见需求。用户可能选择任意组合的查询条件,传统方式要写一堆if-else拼接SQL。JPA提供了Criteria API和Specification:
public class UserSpecification {
public static Specification<User> hasStatus(UserStatus status) {
return (root, query, builder) ->
status == null ? null : builder.equal(root.get("status"), status);
}
public static Specification<User> usernameContains(String keyword) {
return (root, query, builder) ->
keyword == null ? null : builder.like(root.get("username"), "%" + keyword + "%");
}
}
// 使用时组合条件
Specification<User> spec = Specification.where(hasStatus(ACTIVE))
.and(usernameContains("admin"));
List<User> users = userRepository.findAll(spec);
分页查询也是一行代码搞定:
Pageable pageable = PageRequest.of(, , Sort.by("createdAt").descending());
Page<User> userPage = userRepository.findAll(pageable);
JPA虽好,但也不是银弹。我踩过的坑你们可能也会遇到:
N+1查询问题:查询用户列表时,如果要显示每个用户的订单数量,可能会执行1次查询用户 + N次查询订单的SQL。解决方案是用@EntityGraph或者写JOIN查询。
批量操作性能:单条插入/更新效率低下。这时候可以考虑用@Modifying注解的批量更新,或者直接用JDBC批处理。
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.lastLoginTime < :time")
int updateInactiveUsers(@Param("status") UserStatus status, @Param("time") LocalDateTime time);
Spring Data JPA让我们告别了繁琐的SQL编写,但记住:工具只是工具,理解业务逻辑和数据结构设计才是根本。当你发现JPA搞不定某个复杂查询时,别硬撑,该用原生SQL就用原生SQL。毕竟,没有什么比解决问题更重要的了。