资讯动态

JPA入门到实战:从实体映射到性能优化的完整指南

发布时间:2026/10/14 22:04:32 来源:尧图企业网站定制
做Java后端这几年我最怕听到的一句话就是“这个项目用JPA”。倒不是说JPA本身有多难而是很多人习惯了MyBatis那种“写SQL一时爽”的节奏突然切到JPA那些自动生成的SQL、懒加载、持久化状态分分钟让人怀疑人生。其实JPA就是一套Java官方的持久层规范核心思想很简单别再把表结构和Java对象割裂开而是直接用对象去操作数据库。这篇内容不打算写成又臭又长的官方翻译稿我按自己实际入门和踩坑的路线把JPA是什么、实体怎么映射、CRUD怎么写、查询怎么优化、事务怎么管一条一条拆开讲清楚。适合刚接触Spring Boot JPA的同学也适合那些用JPA写了好几个月却还在靠“试错”写代码的人。1. 先弄清楚JPA到底是干什么的1.1 从JDBC到JPA持久层开发到底在解决什么问题数据库访问这件事最初级的形态就是用JDBC写Connection、Statement、ResultSet。我大学实习时第一段代码就是用JDBC查用户表写起来是真的繁琐先加载驱动再拿连接拼SQL执行遍历结果集手动把每一列的值塞进对象最后还得关连接。一个查询写下来二三十行而且字段一多就特别容易对错位置。后来有了MyBatis把SQL和Java代码分离算是进步但本质上我们还是在“手动描述数据库表长什么样”。JPA的思路完全不同。它把数据库表当成Java对象的投影开发者写代码时操作的是实体对象JPA负责帮你把对象变成SQL发出去再把查询结果转回对象。这背后是一种叫ORMObject Relational Mapping对象关系映射的思想。打个比方JDBC时代你是在餐厅后厨亲手洗菜切菜MyBatis时代你是拿着菜单报菜名而JPA时代你是直接跟服务员说“我要一桌宴席”具体怎么采购、怎么加工由后厨决定。但注意JPA本身不是一个实现它只是规范。就像Java接口只定义了方法签名真正干活的是实现方。最主流的JPA实现是HibernateSpring Data JPA又是基于Hibernate封装的一层更友好的API。很多人分不清这三者的关系后面我会再展开。1.2 核心概念Entity、EntityManager、持久化上下文不管你用什么实现JPA有四个概念绕不开Entity实体映射数据库表的普通Java对象用Entity标记。EntityManagerJPA的核心入口负责实体的增删改查、查询创建、事务管理等操作。持久化上下文Persistence Context可以理解成EntityManager内部的一级缓存区它维护着“当前这个对象处于什么状态”。JPQL类似SQL但面向对象实体而非数据表的查询语言比如查所有用户写“from User u”而不是“select * from t_user”。实体状态是新手最容易懵的地方。一个对象和一个EntityManager打交道后可能处于四种状态new瞬时态还没被管理、managed托管态被持久化上下文管理、detached游离态之前被管理但上下文关闭了、removed删除态。你调用save或者persist的时候对象就进入了托管态之后你对它的修改事务提交时会自动同步到数据库这就是JPA著名的“脏检查dirty checking”。理解了这一点很多“我没调save怎么就更新了”的疑问就迎刃而解。1.3 JPA与Hibernate、Spring Data JPA的关系我在刚接触的时候网上一会儿说JPA一会儿说Hibernate一会儿说Spring Data JPA完全分不清。后来我总结了一个特别简单的类比JPA是驾照考试的大纲Hibernate是一辆按大纲制造的车Spring Data JPA则是帮你配了自动挡和导航的车。也就是说JPA定义了你“应该怎么开车”Hibernate实现了这套规范同时还有很多扩展功能而Spring Data JPA是Spring对Hibernate的再封装让你连“挂挡”都省了——只要写一个接口继承JpaRepository定义方法名比如findByUserName(String name)它就自动帮你生成查询连实现类都不用写。这个组合是目前Java后端最主流的持久层方案。所以后面我讲的所有内容都是基于Spring Boot 2.x Spring Data JPA Hibernate这套组合这是绝大多数项目里的真实形态。2. 从零搭一个能跑起来的JPA项目2.1 依赖引入与数据源配置新建一个Spring Boot项目时只需要引入两个起步依赖spring-boot-starter-data-jpa和对应数据库的驱动。以MySQL为例pom.xml里关键部分长这样dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency配置方面application.yml里面最基本的五项spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true这里重点说一下ddl-auto这个参数它有四个常用值none啥也不干、update没有表就建表有表就更新、create每次启动删表重建、create-drop用完就删常用于测试。我在真实项目里从不用create手一滑数据就没了。开发阶段顶多用update生产环境老老实实配成none表结构用独立的数据库迁移脚本管理。show-sql建议开发时打开能帮你看清JPA到底执行的什么SQL但生产环境必须关掉否则有性能开销。2.2 第一个实体类与主键策略实体类是JPA的地基。我以一个最典型的用户表为例代码直接抄作业package com.example.demo.entity; import javax.persistence.*; import java.time.LocalDateTime; Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name user_name, nullable false, length 50) private String userName; Column(nullable false, unique true, length 100) private String email; private Integer age; Column(name create_time) private LocalDateTime createTime; // 必须有无参构造器JPA底层通过反射创建对象 // getter/setter 省略 }注意几个细节。GeneratedValue的IDENTITY策略对应MySQL的自增主键这是最常见的用法。还有SEQUENCE策略适合Oracle/PostgreSQL、TABLE策略用一张序列表生成主键性能一般、AUTO让JPA自动选。新手容易踩的坑是把主键类型设成int结果数据多了超出范围直接报错建议直接用Long。Column里可以声明列名、是否为空、长度、是否唯一这些在ddl-auto为update时会影响建表语句。还有个坑是Java字段命名和数据库列名不一致。比如我代码里写了userName默认情况下Hibernate会把驼峰转成下划线生成user_name列这是Hibernate的隐式命名策略。但如果公司规范要求列名必须写成USER_NAME这种全大写你就得自己指定Column(name USER_NAME)。2.3 Repository接口零实现类的魔法有了实体之后需要写一个数据访问接口。Spring Data JPA最让人惊艳的就是这一步package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; Repository public interface UserRepository extends JpaRepositoryUser, Long { ListUser findByUserName(String userName); ListUser findByAgeGreaterThan(Integer age); User findByEmail(String email); }运行时Spring会为这个接口生成代理实现类。你不需要写任何实现只要遵循方法命名规范Spring帮你自动解析方法名并生成查询。findByUserName就是where user_name ?findByAgeGreaterThan就是where age ?。这块是Spring Data JPA独有的便利原生JPA可没有这么省事。写完这三个文件项目就能启动并自动建出t_user表了。我第一次跑通的时候感觉像开了挂但后来才知道这只是入门真正的坎还在后面。3. 实体映射与关联关系实操3.1 字段映射的常用注解整理实体映射有很多细节我整理一个自己常用的速查表注解作用关键参数说明Entity标记类是实体必须有无参构造器类不能是finalTable指定对应的表名name属性表名schema、uniqueConstraints等Id标记主键字段必须放在getter或字段上二选一别混用GeneratedValue主键生成策略strategy取值IDENTITY、SEQUENCE、TABLE、AUTOColumn字段到列的详细映射name、nullable、unique、length、precision等Transient标记不映射到数据库的字段比如临时计算属性Enumerated枚举字段的映射方式ORDINAL存下标STRING存枚举名建议用STRINGLob大字段对应MySQL的text/blobVersion乐观锁版本号每次更新自动加一防止并发修改其中Enumerated特别值得一说。假设有个性别枚举public enum Gender { MALE, FEMALE }如果你不写Enumerated默认是ORDINAL策略也就是存MALE和FEMALE的下标0和1。这会埋雷哪天你在枚举中间插了一个UNKNOWN所有老数据的值全部错位。所以凡是枚举字段我都强制写Enumerated(EnumType.STRING)牺牲一点空间换长期稳定性。3.2 一对多与多对一的正确姿势关联关系映射是JPA最容易翻车的地方。先说一对多和多对一这是业务里出现频率最高的。一个典型的场景一个用户有多篇文章。实体建模时User一侧有ListArticleArticle一侧有User user。写法是Entity Table(name t_user) public class User { OneToMany(mappedBy user) private ListArticle articles; } Entity Table(name t_article) public class Article { ManyToOne JoinColumn(name user_id) private User user; }关键在mappedBy。很多新手把mappedBy写反结果两边都去维护外键生成重复的更新语句甚至主键冲突。记住一个死规则mappedBy写在“一”的那一侧值是被“多”的那一侧关联字段名字。Article里的字段叫user所以User里的OneToMany(mappedBy user)就是这个意思。真正拥有外键的表是Article表的user_id列由JoinColumn来控制。这里还有个Cascade的问题。CascadeType有PERSIST、MERGE、REMOVE、REFRESH、DETACH和ALL。我见过有人图省事一对多直接配CascadeType.ALL然后在删除User时把关联的Article全删了业务直接崩。真实项目中一对多我一般只配CascadeType.PERSIST或者干脆不配删除操作自己写代码控制尽量避免从“一”侧级联删“多”侧风险太大。3.3 多对多映射学会用中间表说话多对多最典型的例子是用户和角色。JPA支持直接用ManyToMany JoinTable生成中间表但我后来在项目里踩过坑之后越来越倾向于把中间表显式建模成一个实体。所谓“显式建模”就是中间表不交给JPA自动管理而是自己建一个UserRole实体分别用ManyToOne关联User和Role然后User和Role各自持有List 。这么做的好处是将来中间表要加字段比如“分配时间”、“操作人”直接给UserRole加字段就行。而用ManyToMany自动生成的中间表加字段非常痛苦基本要重构关联模型。如果确实要用ManyToMany写法参考Entity Table(name t_user) public class User { ManyToMany JoinTable( name t_user_role, joinColumns JoinColumn(name user_id), inverseJoinColumns JoinColumn(name role_id) ) private ListRole roles; }同样要指定mappedBy只能让一侧维护关联关系另一侧写mappedBy避免重复维护。我建议维护侧放在逻辑上“主”的那一侧比如业务上经常从User查他有什么角色就把JoinTable放在User里。3.4 继承映射的几种策略如果多个实体有公共字段比如商品和订单里都有创建时间、创建人可以用继承映射。JPA提供三种策略我简单说下适用场景SINGLE_TABLE所有子类放在一张表里用discriminator列区分。查询性能好但会有很多空字段。适合层级简单、字段差异小的场景。JOINED父类和子类各建一张表查询时用join把数据拼起来。数据冗余少但查询关联多性能吃亏。TABLE_PER_CLASS每个子类建完整的一张表父类字段重复冗余。关联查询麻烦不太推荐。我个人实际操作里用到继承映射的频率很低因为大部分项目为了控制复杂度更喜欢用组合而非继承。但如果确实要用SINGLE_TABLE通常是第一选择尤其是字段差异不大的情况。4. 核心操作增删改查与查询全方案4.1 基础CRUD操作与Repository方法速查有了Repository之后的CRUD是非常清爽的继承JpaRepository就自带一堆方法// 新增或更新 User user new User(); user.setUserName(张三); user.setEmail(zhangsanexample.com); userRepository.save(user); // 根据主键查 OptionalUser userOpt userRepository.findById(1L); // 查全部 ListUser users userRepository.findAll(); // 删除 userRepository.deleteById(1L); // 数量统计 long count userRepository.count();注意findById返回的是Optional这是Spring Data JPA的规范做法。不是说你不能直接get()但要先判断是否是空否则直接NoSuchElementException。save方法比较特殊它既能做新增也能做更新主键为空就是新增主键不为空会先查一下存在就更新。但这个“先查一下”是有成本后面性能优化再细说。4.2 更新操作与脏检查的底层逻辑刚才说save既能新增又能更新但在真实项目里我更推荐用“查询实体-修改字段-提交事务”的方式做更新而不是每次拼一个新的实体调save。JPA的脏检查机制会让这种方式特别自然Transactional public void updateUserName(Long id, String newName) { User user userRepository.findById(id).orElseThrow(); user.setUserName(newName); // 不需要调用 save // 事务提交时Hibernate自动比较对象状态和数据库快照发现变化就执行update }这里最核心的机制是持久化上下文。事务开始后findById查出来的对象是托管态的Hibernate会存一个快照。你改了对象属性事务提交时Hibernate比对快照发现字段变了自动生成update语句。这就是所谓的“脏检查”它是JPA和MyBatis最大的体验差异之一。有一个容易被忽视的性能问题如果同一个事务里查了很多实体哪怕只改了其中一个Hibernate在flush的时候会对所有托管实体做快照比对。实体多了之后这个比对有开销但通常问题不大真遇到超大列表更新时再考虑用批量更新SQL绕过去。4.3 JPQL查询面向对象的SQL当Repository方法名解决不了复杂查询时就该上JPQL了。JPQL长得非常像SQL但目的是查询实体对象而非数据库行Repository public interface UserRepository extends JpaRepositoryUser, Long { Query(select u from User u where u.email :email) User findByEmailJPQL(Param(email) String email); Query(select u from User u where u.age between :min and :max order by u.age desc) ListUser findByAgeBetween(Param(min) Integer min, Param(max) Integer max); }注意JPQL里写的是实体类名User和字段名email不是表名和列名。因为大小写和命名的问题有时候报错你根本看不出哪里错了。还有个常见坑JPQL里千万不要把映射到数据库的关键字写成表里的列名比如你Java字段叫userNameJPQL里写u.user_name就错了。在关联查询上JPQL一句join fetch能解决很多问题这个我放在后面懒加载部分重点讲。4.4 Criteria API与 Specification动态查询JPQL的问题是字符串拼接容易出错写死了就不好动态拼条件。这时候可以用Criteria API它是类型安全的查询构造器。但在Spring Data JPA里我更喜欢用Specification它对动态查询非常友好public class UserSpecs { public static SpecificationUser nameContains(String keyword) { return (root, query, cb) - keyword null ? null : cb.like(root.get(userName), % keyword %); } public static SpecificationUser ageGreaterThanEqual(Integer minAge) { return (root, query, cb) - minAge null ? null : cb.greaterThanOrEqualTo(root.get(age), minAge); } }使用的时候只要ListUser result userRepository.findAll( Specification.where(UserSpecs.nameContains(keyword)) .and(UserSpecs.ageGreaterThanEqual(minAge)) );findAll(Specification)是JpaSpecificationExecutor接口提供的能力Repository需要额外继承它Repository public interface UserRepository extends JpaRepositoryUser, Long, JpaSpecificationExecutorUser { }Specification对那种“查询条件不确定、用户可能只填其中几个字段”的高级筛选场景非常管用不用写一堆if else拼接字符串。这也是我从原生JPQL转向动态查询的主要方案。4.5 分页与排序的规范写法分页查询是后端必须掌握的。Spring Data JPA提供了Pageable接口Pageable pageable PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, createTime)); PageUser userPage userRepository.findAll(pageable); // 返回给前端的数据 ListUser content userPage.getContent(); int totalPages userPage.getTotalPages(); long totalElements userPage.getTotalElements();分页条件复杂时Pageable也能和Specification配合PageUser pages userRepository.findAll(spec, pageable);这里有坑。当分页查询里带了distinct关键字count查询可能不准。还有就是在多对多join时Page的分页在数据库层做还是在内存做要特别关注。Spring Data JPA的默认实现里如果查询没有findAll(Pageable)的优化路径有时候会把整个结果集load进来再做内存分页数据量一大直接卡死。所以复杂查询我一般看日志确认SQL带没带limit没带就要换原生SQL查询。5. 事务、关联抓取与性能优化实战5.1 事务边界Transactional到底该放哪JPA的所有更新操作必须在事务里执行。Spring里最常用的就是Transactional注解但位置不同效果天差地别。我在项目里定的原则是事务加在Service层的公开方法上而不是Repository上。原因很简单一个业务操作往往要调用多个Repository方法如果每个Repository方法各自开一个事务最后一步出错时前面已经提交无法回滚。Service public class UserService { Transactional public void createUserWithDefaultRole(User user) { userRepository.save(user); // 其他写操作 } }有两点要注意Transactional在同一个类内部方法之间调用是失效的因为Spring的代理机制没法拦截内部调用运行期异常默认回滚但受检异常默认不会回滚。第二个坑之深我见过有人事务“死”了数据写了半截排查了半天发现是方法抛了个必检异常事务默默提交了。想统一处理可以配置rollbackFor Exception.class。5.2 懒加载与N1问题性能杀手懒加载Lazy Loading是JPA默认的关联加载策略。查User对象时articles列表不会立刻查数据库而是等到你真正访问list时才会发出SQL。但问题来了一旦持久化上下文关闭比如Service层事务结束再访问懒加载的属性就会报LazyInitializationException这是社区提问率最高的异常之一。另一个更隐蔽的问题是N1查询。举个具体场景查10个用户再循环遍历每个用户的文章列表。懒加载下查询用户列表一条SQL遍历每个用户时文章各发一条SQL总共11条SQL。数据量一上去数据库瞬间压力爆炸。解决N1的核心思路是“把需要的关联一次性查出来”。有两个方案比较常用一是在JPQL里用join fetchQuery(select u from User u left join fetch u.articles where u.id :id) User findWithArticles(Param(id) Long id);二是用EntityGraphEntityGraph(attributePaths articles) Query(select u from User u where u.id :id) User findWithArticlesByGraph(Param(id) Long id);两者效果类似但EntityGraph的好处是不用重写JPQL。我用的时候会刻意控制只在需要的查询上加抓取而不是一上来就在实体关联上配置EAGER因为全局改EAGER会牺牲灵活性而且可能让本来一条SQL的事变成join一堆表。5.3 批量操作优化saveAll也有性能陷阱处理批量插入时很多人会写for (User user : userList) { userRepository.save(user); }这样每条记录一次insert几万条数据插入能跑到怀疑人生。正确做法是用saveAlluserRepository.saveAll(userList);但saveAll并不是“批量”的万能药它的内部实现还是循环调用save。不过在适当配置下Hibernate可以合并insert语句这需要设置jdbc.batch_sizespring: jpa: properties: hibernate: jdbc: batch_size: 50注意MySQL默认需要额外配置rewriteBatchedStatementstrue才能让批量生效spring: datasource: url: jdbc:mysql://localhost:3306/demo?rewriteBatchedStatementstrue如果没有这个参数Hibernate发给MySQL的还是逐条insert。这个参数我建议所有MySQL Hibernate项目都加上对批量性能提升非常明显几乎没有副作用。还有一种更粗的选择就是直接用原生SQL做批量update或insert绕过EntityManager。这在某些场景下是最后的优化手段但代价是丢失了JPA的缓存一致性和事务里的脏检查要权衡着来。5.4 缓存机制一级缓存、二级缓存与查询缓存JPA自带一级缓存就是持久化上下文。同一个事务内多次查同一个主键Hibernate只发一次SQL后面的直接从快照里拿。这带来一个有趣的副作用该事务内对实体做的修改不提交的话第二次查还是修改后的对象和数据库真实值可能不一致。这不是Bug是JPA的设计。二级缓存是可选的跨Session缓存需要额外配置比如用Hibernate自带的或第三方缓存框架。痛点在于一旦数据被外部直接改了库缓存会变脏。我的做法是默认不开启二级缓存只有读多写少且数据量可控的表比如字典表、地区表才考虑。别贪多缓存一致性带来的麻烦往往大于性能收益。另外有意思的是Request查询缓存默认也是关闭的因为JPQL查询结果依赖表数据一旦有更新就要重新查询。开了之后还要维护失效策略复杂度和回报不成正比。6. 常见问题与排查技巧实录6.1 最经典异常LazyInitializationException这个异常我在群里回复过无数次。典型日志报错长这样org.hibernate.LazyInitializationException: could not initialize proxy - no Session。意思是持久化上下文已经关了你还在访问懒加载的关联对象。解决路径有几种加上fetch join或者EntityGraph让关联在事务内查出来。把Transactional注解的范围扩大让整个Web请求都处于事务中open session in view。在实体里把关联改成EAGER。第三种我极度不建议除非关联对象极少。现实中最合理的方案是Service层中就把需要返回给前端的字段用DTO组装好避免前端反序列化的时候触发懒加载。Spring Boot默认会开open-session-in-view很多项目因此“感觉没报错”但代价是数据库连接被长期占用性能隐患很大。我现在的新项目都会把这个开关显式关掉spring: jpa: open-in-view: false6.2 数据库表没生成或字段没更新怎么办这个问题的检查顺序我很明确。先看ddl-auto是不是update或create如果为none肯定不建表再看实体扫描路径配没配对Spring Boot会自动扫描主类所在包及子包下的Entity如果你的实体放在脱离主包的位置就要显式配置EntityScan最后看数据库日志或show-sql确认有没有执行建表语句。另一种情况是表建了但字段不更新。Hibernate的update策略能加字段但绝不会帮你删字段或改字段类型这是安全机制。所以开发时改字段类型要么手动改数据库要么把ddl-auto临时改成update再手动删掉列要么干脆用迁移脚本管理。我用过好几款数据库迁移工具之后越来越觉得生产环境表结构变更就应该走迁移脚本JPA的ddl-auto只留给开发环境用。6.3 字段名映射导致SQL异常有个特别容易踩的坑数据库列名用了MySQL保留字比如order、desc、group你建实体时Java字段叫orderJPA生成的SQL直接语法错误。解决办法很简单表的列名加反引号或者改列名但最靠谱的是从一开始就别用保留字当列名。我还遇到过一个更隐蔽的情况Java字段是isDeletedHibernate默认映射时可能会把is前缀处理掉生成列名deleted导致和数据库里的is_deleted对不上直接报“Unknown column”。遇到这种问题打开show-sql日志对照一下实际生成的列名就一目了然。6.4 主键冲突与重复保存的坑调用save传了一个带主键的对象但数据库里已经存在这个id此时Hibernate会先查一遍如果查到了就走更新查不到就insert一般不报主键冲突。但如果主键是自动生成IDENTITY你却手动给id赋了值就可能出现主键冲突。还有一种常见问题批量保存时集合里多个对象共享同一个主键id或者对象在持久化上下文里反复save导致“identifier of an instance of X was altered from a to b”异常。这类问题大多是因为手动管理id导致的解决思路是让JPA统一管理主键不要自己给自动生成主键的实体去设置id。6.5 常见问题速查表现象可能原因解决建议启动报“Table xxx doesnt exist”ddl-auto配置不对/实体未扫描检查ddl-auto、EntityScan路径懒加载报LazyInitializationException事务外访问懒加载属性join fetch/EntityGraph/事务边界调整一次性插入很慢未开启JDBC批量配置jdbc.batch_size rewriteBatchedStatements查询出重复记录多对多join导致笛卡尔积用distinct或在关联集合上使用Set修改了对象没更新数据库事务没提交/受检异常回滚策略检查Transactional边界、rollbackFor列名不对报Unknown column命名策略与字段名不一致显式指定Column(name ...)全表查询超时N1查询/未分页添加join fetch、分页查询优化6.6 调优时看哪些日志和指标排查性能问题时我通常先做三件事打开show-sql看每条SQL是否合理用p6spy或者Hibernate的SQL日志统计执行次数定位N1检查有没有循环调用save的地方。如果发现一个业务方法发出几十条SQL十有八九关联抓取策略有问题。另外可以用数据库慢查询日志看看到底是哪些SQL占大头。有个小技巧Hibernate输出SQL时默认参数是占位符?想知道实际值可以加配置spring: jpa: properties: hibernate: format_sql: true use_sql_comments: true再配合日志框架把org.hibernate.SQL级别调成DEBUG就能看到实际执行的SQL和参数值排查问题会方便很多。最后再分享一个实用习惯如果说有什么经验是希望当初有人早点告诉我的那就是写JPA业务代码的时候始终问自己一句话——这个方法最终会发出几条SQL如果答案是“不确定”或者“可能很多条”那就要认真看关联抓取和查询策略了。另一个习惯是给每个Repository方法命名时就想清楚它到底干的是精确查询还是模糊查询findBy和findDistinctBy虽然差别不大但语义上完全两回事将来维护的人很可能就是你会感谢这个清晰的命名。JPA这个东西入门门槛不高但想用得优雅、不翻车需要在实际项目里反复打磨。希望这篇文章里的内容可以帮助你少走一些我当年走过的弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑