使用 Spring Data JPA 時,經常會看到 findById、getOne、findOne 三個方法。
從字面上理解,他們都是根據 ID 、或根據指定的查詢條件,獲取單個實體對象。
但他們的底層獲取機制、返回值類型、以及拋異常的機制是不一樣的,因此對應的使用場景也不一樣。
1、findById 方法
findById 方法會立即(EAGER)訪問數據庫,並返回和指定 ID 關聯的實體對象;如果沒有找到,則返回 Optional.empty()。
定義如下:
public interface CrudRepository<T, ID> extends Repository<T, ID> {
/**
* Retrieves an entity by its id.
*
* @param id must not be {@literal null}.
* @return the entity with the given id or {@literal Optional#empty()} if none found.
* @throws IllegalArgumentException if {@literal id} is {@literal null}.
*/
Optional<T> findById(ID id);
}
2、getOne 方法
getOne 是一個延遲加載方法,它並不立即訪問數據庫,而是返回一個代理(proxy)對象,這個代理對象是對實體對象的引用,僅在 使用代理對象訪問對象屬性時才會去真正訪問數據庫 ,如果找不到,則拋出 EntityNotFoundException。
定義如下:
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
/**
* Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
* implemented this is very likely to always return an instance and throw an
* {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
* immediately.
*
* @param id must not be {@literal null}.
* @return a reference to the entity with the given identifier.
* @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
*/
T getOne(ID id);
}
3、findOne 方法
除了 findById、getOne 外,Spring Data JPA 還提供了兩個 findOne 方法:
Optional<T> findOne(@Nullable Specification<T> spec)<S extends T> Optional<S> findOne(Example<S> example)
這兩個方法用於需要動態構建多條件查詢的場景中,它們都是立即訪問數據庫的。
定義如下:
public interface QueryByExampleExecutor<T> {
/**
* Returns a single entity matching the given {@link Example} or {@literal null} if none was found.
*
* @param example must not be {@literal null}.
* @return a single entity matching the given {@link Example} or {@link Optional#empty()} if none was found.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the Example yields more than one result.
*/
<S extends T> Optional<S> findOne(Example<S> example);
}
public interface JpaSpecificationExecutor<T> {
/**
* Returns a single entity matching the given {@link Specification} or {@link Optional#empty()} if none found.
*
* @param spec can be {@literal null}.
* @return never {@literal null}.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found.
*/
Optional<T> findOne(@Nullable Specification<T> spec);
}
返回類型為 Optional ,如果沒有檢索到,返回 Optional.empty(),結果滿足條件的記錄條數超過一條,則拋出 IncorrectResultSizeDataAccessException
4、如何選擇
從上面的描述可以看出:它們主要的區別在於 加載時期 及 是否支持動態構建查詢條件 的不同。
4.1、多條件查詢的場景中,可使用 findOne
例如,根據 openId 以及 state 查詢指定的用戶:
Optional<User> user =
postRepository.findOne(
(root, query, cb) ->
cb.and(cb.equal(root.get("openId"), "aaa"), cb.equal(root.get("state"), 1)));
或者使用 findOne(Example<S> example):
User u = new User();
u.setOpenId("xxx");
u.setState(1);
Example<User> example = Example.of(u);
Optional<User> user = postRepository.findOne(example);
以上兩種寫法是等效的,執行的 SQL 如下:
Hibernate: select user0_.id as id1_0_, user0_.created_at as created_2_0_, user0_.open_id as open_id3_0_, user0_.state as state4_0_ from t_users user0_ where user0_.open_id=? and user0_.state=1
4.2、按 ID 檢索的場景中,使用 findById 或 getOne
前面已經描述過,findById 和 getOne 的最大的區別是加載時期的不同。
因此:
1、在需要延遲加載時,選擇 getOne
2、不需要延遲加載時,選擇 findById
當然了,findOne 也可以根據 ID 進行查詢,但是寫法累贅、且晦澀不易讀,不要那樣。
Optional<User> user = postRepository.findOne((root, query, cb) -> cb.equal(root.get("id"), 1));
下面,針對延遲加載的場景舉個例子:
假如有一個 Post 對象和一個 Comment 對象,Comment 對象中有一個對 Post 對象的引用,Post 和 Comment 是一對多的關系。
Post:
@Entity
@Table(name = "t_posts")
public class Post {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
private String title;
}
Comment:
@Entity
@Table(name = "t_comments")
public class Comment {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
private String content;
@ManyToOne
private Post post;
}
添加一條評論,代碼如下:
public Comment addComment(long postId) {
final Post p = postRepository.findById(postId); //(1)
Comment c = new Comment();
c.setContent("xxx");
c.setPost(p);
commentRepository.save(c);
}
上面代碼中的 (1) 處,如果使用的是 findById,則 SQL 輸出為:
select id, title from t_posts where id=?
insert into t_comments (id, content, postId) values (?, ?, ?)
如果使用的是 getOne,則 SQL 輸出為:
insert into t_comments (id, content, postId) values (?, ?, ?)
由於新建 Comment 對象時,需要設置它關聯的 Post 對象,如果使用 findById 來獲取 Post 對象,會多觸發一次數據庫加載,而如果利用 getOne 的延遲加載策略機制,可以減少一次不必要的數據庫交互。
5、總結
1、getOne 是延遲加載;而 findById、findOne 是立即加載。
2、 getOne 如果找不到記錄會拋出EntityNotFoundException;而 findById、findOne 會返回 Optional.empty()。
3、 在 @ManyToOne 的場景中使用 findOne ,可以獲得延遲加載機制帶來的性能優勢。
4、 在根據非 ID 查詢、或動態查詢的場景中,使用 findOne。
5、 findOne 查詢結果不能返回超過一條,否則會拋出 IncorrectResultSizeDataAccessException。
(完)
