JPA中用AccessType.PROPERTY实现ID不可变并避免额外查询
这个问题其实是Hibernate属性访问策略的一个常见痛点——它默认要求属性访问的字段必须有对应的setter,但我们完全可以在保持ID不可变的前提下解决这个问题,给你几个实用的方案:
方案1:提供包级私有(package-private)的setId方法
Hibernate可以通过反射访问包级私有方法,所以你可以给ID字段添加一个包级私有的setter,这样外部代码无法调用(保证ID在业务代码中不可变),但Hibernate能正常使用属性访问策略:
@Entity @Table(name = "Book") public class Book { @Id @Access(AccessType.PROPERTY) @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "AUT_ID") @Fetch(FetchMode.JOIN) private Author author; Book(){} public Book(Author author) { this.author = author; } public Long getId() { return this.id; } public Author getAuthor() { return this.author; } // 包级私有setter,仅Hibernate可通过反射调用 void setId(Long id) { this.id = id; } } @Entity @Table(name = "Author") public class Author { @Id @Access(AccessType.PROPERTY) @GeneratedValue(strategy= GenerationType.AUTO) private Long id; @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) private List<Book> books; Author(){} public Long getId() { return this.id; } public List<Book> getBooks() { return this.books; } // 包级私有setter void setId(Long id) { this.id = id; } }
这样既保留了属性访问的核心优势(加载Book时同时获取Author的ID,避免额外查询),又确保ID在业务代码中不可被修改。
方案2:字段访问策略+EntityGraph预加载关联ID
如果你不想修改实体的访问策略,可以移除@Access注解(保持默认的字段访问),然后通过Spring Data JPA的@EntityGraph强制查询Book时预加载Author的ID,这样调用book.getAuthor().getId()不会触发额外查询:
首先修改你的BookRepository:
public interface BookRepository extends CrudRepository<Book, Long> { @EntityGraph(attributePaths = {"author.id"}) Optional<Book> findById(Long id); }
执行这个方法时,Hibernate会生成左连接查询,把Author的ID一起加载进来,SQL类似:
select book0_.id as id1_1_, book0_.aut_id as aut_id2_1_, author1_.id as id1_0_ from book book0_ left outer join author author1_ on book0_.aut_id=author1_.id where book0_.id=?
此时book.getAuthor().getId()会直接返回已加载的ID,不会触发二次查询,同时你的ID字段不需要任何setter,保持完全不可变。
方案3:用@Formula直接映射Author的ID到Book
另一种更直接的思路是在Book实体中单独映射Author的ID作为只读字段,这样不需要通过Author对象就能获取ID,彻底避免懒加载代理的问题:
@Entity @Table(name = "Book") public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "AUT_ID") private Author author; // 直接映射数据库中的AUT_ID字段为只读属性 @Formula("AUT_ID") private Long authorId; Book(){} public Book(Author author) { this.author = author; } public Long getId() { return this.id; } public Author getAuthor() { return this.author; } public Long getAuthorId() { return authorId; } }
现在你可以直接调用book.getAuthorId()获取作者ID,完全不需要访问Author对象,自然不会触发额外查询,ID字段的不可变性也能得到保证。
补充说明:为什么@Fetch(FetchMode.JOIN)没生效?
你之前加了@Fetch(FetchMode.JOIN)但还是触发两次查询,是因为这个注解是Hibernate专属的,它仅对HQL/Criteria查询生效,而Spring Data JPA默认的findById使用的是EntityManager.find()方法,这个方法不会应用@Fetch的配置,所以需要用@EntityGraph或者自定义HQL查询来强制关联加载。
内容的提问来源于stack exchange,提问作者Eduardo

