最佳方法:定製@NamedEntityGraph、定製查詢和定製VO,可以做到按照需要最佳查詢,需要注意的地方:定製VO的欄位一定要等於或小於實際查詢的欄位,才不會複製的時候觸發N+1查詢。
1 問題復現
1.1 專案結構
1.2 entity
package com.xkzhangsan.jpa.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @Getter @Setter @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @OneToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY) @JoinColumn(name = "user_detail_id") private UserDetail userDetail; }
package com.xkzhangsan.jpa.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @Getter @Setter @Table(name = "user_detail") public class UserDetail { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String address; }
1.3 repository
package com.xkzhangsan.jpa.repository; import com.xkzhangsan.jpa.entity.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Integer> { }
1.4 service
package com.xkzhangsan.jpa.service; import com.xkzhangsan.jpa.entity.User; import com.xkzhangsan.jpa.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserService { @Autowired private UserRepository userRepository; public List<User> findAll(){ return userRepository.findAll(); } }
1.5 controller
package com.xkzhangsan.jpa.controller; import com.xkzhangsan.jpa.entity.User; import com.xkzhangsan.jpa.service.UserService; import com.xkzhangsan.jpa.vo.UserDetailVO; import com.xkzhangsan.jpa.vo.UserVO; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.stream.Collectors; @RestController public class UserController { @Autowired private UserService userService; @RequestMapping(value = "/") public List<UserVO> getPersons() { ModelMapper modelMapper = new ModelMapper(); List<User> userList = userService.findAll(); if (!CollectionUtils.isEmpty(userList)) { return userList.stream().map(user -> { UserVO userVO = modelMapper.map(user, UserVO.class); UserDetailVO userDetailVO = modelMapper.map(user.getUserDetail(), UserDetailVO.class); userVO.setUserDetailVO(userDetailVO); return userVO; }).collect(Collectors.toList()); } return null; } }
1.6 測試
查詢1次,實際查詢了4次,這裡的N指的是集合的數量3,所以 N+1,就是3+1=4
1.7 問題原因
User關聯物件懶載入導致
2 最佳解決方法
定製@NamedEntityGraph、定製查詢和定製VO,可以做到按照需要最佳查詢。
2.1 定製@NamedEntityGraph
package com.xkzhangsan.jpa.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @Getter @Setter @Table(name = "user") @NamedEntityGraph(name = "user.userDetail", attributeNodes = { @NamedAttributeNode(value = "userDetail") }) public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @OneToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY) @JoinColumn(name = "user_detail_id") private UserDetail userDetail; }
從程式碼種可以看出增加註解,使用user.userDetail會聯表查詢UserDetail
@NamedEntityGraph(name = "user.userDetail", attributeNodes = { @NamedAttributeNode(value = "userDetail") })
2.1.2 定製查詢
package com.xkzhangsan.jpa.repository; import com.xkzhangsan.jpa.entity.User; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface UserRepository extends JpaRepository<User, Integer> { @Override @EntityGraph(value = "user.userDetail", type = EntityGraph.EntityGraphType.FETCH) List<User> findAll(); }
從程式碼種可以看出,重寫了findAll,使用了user.userDetail
2.3 定製VO(可選)
根據需要返回的欄位定義一個VO,如果沒有需要則不需要定製,比如當前例項種就不需要。
測試結果如下,只查詢了一次,但聯表查詢了,left outer join user_detail
select user0_.id as id1_0_0_, userdetail1_.id as id1_1_1_, user0_.name as name2_0_0_, user0_.user_detail_id as user_det3_0_0_, userdetail1_.address as address2_1_1_ from user user0_ left outer join user_detail userdetail1_ on user0_.user_detail_id=userdetail1_.id
2.3.1 需要定製VO的例項
比如,只想返回使用者id和name,可以定製一個UserSimpleVO,如下:
package com.xkzhangsan.jpa.vo; import lombok.Getter; import lombok.Setter; @Getter @Setter public class UserSimpleVO { private Long id; private String name; }
因為不包括UserDetailVO userDetailVO,所以複製屬性的時候不會觸發更多的查詢。
需要注意的地方:定製VO的欄位一定要等於或小於實際查詢的欄位,才不會複製的時候觸發N+1查詢。
還有一種方式不用定製VO,透過設定ModelMapper跳過不需要的欄位,但這樣有2個問題
(1)需要設定ModelMapper,比較麻煩
(2)既然不需要一些欄位,定製VO是最有效的方法,這樣符合迪米特法則,比如不能展示的敏感欄位,如果查詢或處理過程中沒有處理好,可能導致誤返回,最好定製VO,在VO刪除這個欄位。
3 使用EAGER載入方式
既然是因為LAZY導致的,改成EAGER是否可以解決問題?經過驗證是不可行的,仍然會查詢多次,比如:
package com.xkzhangsan.jpa.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @Getter @Setter @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @OneToOne(cascade = CascadeType.DETACH, fetch = FetchType.EAGER) @JoinColumn(name = "user_detail_id") private UserDetail userDetail; }
發現仍然會執行多個sql
Hibernate: select user0_.id as id1_0_, user0_.name as name2_0_, user0_.user_detail_id as user_det3_0_ from user user0_ Hibernate: select userdetail0_.id as id1_1_0_, userdetail0_.address as address2_1_0_ from user_detail userdetail0_ where userdetail0_.id=? Hibernate: select userdetail0_.id as id1_1_0_, userdetail0_.address as address2_1_0_ from user_detail userdetail0_ where userdetail0_.id=? Hibernate: select userdetail0_.id as id1_1_0_, userdetail0_.address as address2_1_0_ from user_detail userdetail0_ where userdetail0_.id=?