***** Table: Supplier *****+-----+-------------------+| ID | NAME |+-----+-------------------+| 1 | Supplier Name 1 || 2 | Supplier Name 2 || 3 | Supplier Name 3 || 4 | Supplier Name 4 |+-----+-------------------+
***** Table: Product *****+-----+-----------+--------------------+-------+------------+| ID | NAME | DESCRIPTION | PRICE | SUPPLIERID |+-----+-----------+--------------------+-------+------------+|1 | Product 1 | Name for Product 1 | 2.0 | 1 ||2 | Product 2 | Name for Product 2 | 22.0 | 1 ||3 | Product 3 | Name for Product 3 | 30.0 | 2 ||4 | Product 4 | Name for Product 4 | 7.0 | 3 |+-----+-----------+--------------------+-------+------------+
因素:
供应商的懒惰模式设置为“true”(默认)
用于查询产品的获取模式是选择
获取模式(默认):访问供应商信息
缓存第一次不起作用
供应商被访问
抓取模式为选择抓取(默认)
// It takes Select fetch mode as a defaultQuery query = session.createQuery( "from Product p");List list = query.list();// Supplier is being accesseddisplayProductsListWithSupplierName(results);
select ... various field names ... from PRODUCTselect ... various field names ... from SUPPLIER where SUPPLIER.id=?select ... various field names ... from SUPPLIER where SUPPLIER.id=?select ... various field names ... from SUPPLIER where SUPPLIER.id=?
$cats = load_cats();foreach ($cats as $cat) {$cats_hats => load_hats_for_cat($cat);// ...}
假设load_cats()有一个实现,可以归结为:
SELECT * FROM cat WHERE ...
…并且load_hats_for_cat($cat)有一个这样的实现:
SELECT * FROM hat WHERE catID = ...
…您将在代码执行时发出“N+1”查询,其中N是猫的数量:
SELECT * FROM cat WHERE ...SELECT * FROM hat WHERE catID = 1SELECT * FROM hat WHERE catID = 2SELECT * FROM hat WHERE catID = 3SELECT * FROM hat WHERE catID = 4...
@Rulepublic final QueryCounter queryCounter = new QueryCounter();
@Expectation(atMost = 3)@Testpublic void testInvokingDatabase() {// your JDBC or JPA code}
INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 1', 1)
INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 2', 2)
INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 3', 3)
INSERT INTO post (title, id)VALUES ('High-Performance Java Persistence - Part 4', 4)
并且,我们还将创建4post_comment子记录:
INSERT INTO post_comment (post_id, review, id)VALUES (1, 'Excellent book to understand Java Persistence', 1)
INSERT INTO post_comment (post_id, review, id)VALUES (2, 'Must-read for Java developers', 2)
INSERT INTO post_comment (post_id, review, id)VALUES (3, 'Five Stars', 3)
INSERT INTO post_comment (post_id, review, id)VALUES (4, 'A great reference book', 4)
N+1个纯SQL查询问题
如果您使用此SQL查询选择post_comments:
List<Tuple> comments = entityManager.createNativeQuery("""SELECTpc.id AS id,pc.review AS review,pc.post_id AS postIdFROM post_comment pc""", Tuple.class).getResultList();
然后,您决定为每个post_comment获取关联的posttitle:
for (Tuple comment : comments) {String review = (String) comment.get("review");Long postId = ((Number) comment.get("postId")).longValue();
String postTitle = (String) entityManager.createNativeQuery("""SELECTp.titleFROM post pWHERE p.id = :postId""").setParameter("postId", postId).getSingleResult();
LOGGER.info("The Post '{}' got this review '{}'",postTitle,review);}
您将触发N+1查询问题,因为您执行的不是一个SQL查询,而是5(1+4):
SELECTpc.id AS id,pc.review AS review,pc.post_id AS postIdFROM post_comment pc
SELECT p.title FROM post p WHERE p.id = 1-- The Post 'High-Performance Java Persistence - Part 1' got this review-- 'Excellent book to understand Java Persistence'
SELECT p.title FROM post p WHERE p.id = 2-- The Post 'High-Performance Java Persistence - Part 2' got this review-- 'Must-read for Java developers'
SELECT p.title FROM post p WHERE p.id = 3-- The Post 'High-Performance Java Persistence - Part 3' got this review-- 'Five Stars'
SELECT p.title FROM post p WHERE p.id = 4-- The Post 'High-Performance Java Persistence - Part 4' got this review-- 'A great reference book'
修复N+1查询问题非常简单。你需要做的就是提取原始SQL查询中需要的所有数据,如下所示:
List<Tuple> comments = entityManager.createNativeQuery("""SELECTpc.id AS id,pc.review AS review,p.title AS postTitleFROM post_comment pcJOIN post p ON pc.post_id = p.id""", Tuple.class).getResultList();
for (Tuple comment : comments) {String review = (String) comment.get("review");String postTitle = (String) comment.get("postTitle");
LOGGER.info("The Post '{}' got this review '{}'",postTitle,review);}
@Entity(name = "Post")@Table(name = "post")public class Post {
@Idprivate Long id;
private String title;
//Getters and setters omitted for brevity}
@Entity(name = "PostComment")@Table(name = "post_comment")public class PostComment {
@Idprivate Long id;
@ManyToOneprivate Post post;
private String review;
//Getters and setters omitted for brevity}
SELECTpc.id AS id1_1_,pc.post_id AS post_id3_1_,pc.review AS review2_1_FROMpost_comment pc
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
SELECTpc.id as id1_1_0_,pc.post_id as post_id3_1_0_,pc.review as review2_1_0_,p.id as id1_0_1_,p.title as title2_0_1_FROMpost_comment pcINNER JOINpost p ON pc.post_id = p.id
-- The Post 'High-Performance Java Persistence - Part 1' got this review-- 'Excellent book to understand Java Persistence'
-- The Post 'High-Performance Java Persistence - Part 2' got this review-- 'Must-read for Java developers'
-- The Post 'High-Performance Java Persistence - Part 3' got this review-- 'Five Stars'
-- The Post 'High-Performance Java Persistence - Part 4' got this review-- 'A great reference book'
FetchType.LAZY
即使您切换到对所有关联显式使用FetchType.LAZY,您仍然可能遇到N+1问题。
这一次,post关联映射如下:
@ManyToOne(fetch = FetchType.LAZY)private Post post;
SELECTpc.id AS id1_1_,pc.post_id AS post_id3_1_,pc.review AS review2_1_FROMpost_comment pc
但是,如果之后,您将引用延迟加载的post关联:
for(PostComment comment : comments) {LOGGER.info("The Post '{}' got this review '{}'",comment.getPost().getTitle(),comment.getReview());}
您将获得N+1查询问题:
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1-- The Post 'High-Performance Java Persistence - Part 1' got this review-- 'Excellent book to understand Java Persistence'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2-- The Post 'High-Performance Java Persistence - Part 2' got this review-- 'Must-read for Java developers'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3-- The Post 'High-Performance Java Persistence - Part 3' got this review-- 'Five Stars'
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4-- The Post 'High-Performance Java Persistence - Part 4' got this review-- 'A great reference book'
SELECTpc.id as id1_1_,pc.post_id as post_id3_1_,pc.review as review2_1_FROMpost_comment pc
SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 2
-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!
@SpringBootTestclass LazyLoadingTest {
@Autowiredprivate JPlusOneAssertionContext assertionContext;
@Autowiredprivate SampleService sampleService;
@Testpublic void shouldBusinessCheckOperationAgainstJPlusOneAssertionRule() {JPlusOneAssertionRule rule = JPlusOneAssertionRule.within().lastSession().shouldBe().noImplicitOperations().exceptAnyOf(exclusions -> exclusions.loadingEntity(Author.class).times(atMost(2)).loadingCollection(Author.class, "books"));
// trigger business operation which you wish to be asserted against the rule,// i.e. calling a service or sending request to your API controllersampleService.executeBusinessOperation();
rule.check(assertionContext);}}
Entity Model@Entity@Table(name = "DB_USER")public class User {
@Id@GeneratedValue(strategy=GenerationType.AUTO)private Long id;private String name;
@ManyToMany(fetch = FetchType.LAZY)private Set<Role> roles;//Getter and Setters}
@Entity@Table(name = "DB_ROLE")public class Role {
@Id@GeneratedValue(strategy= GenerationType.AUTO)private Long id;
private String name;//Getter and Setters}
select user0_.id, role2_.id, user0_.name, role2_.name, roles1_.user_id, roles1_.roles_id from db_user user0_ left outer join db_user_roles roles1_ on user0_.id=roles1_.user_id left outer join db_role role2_ on roles1_.roles_id=role2_.id
Hibernate和Spring Data JPA提供了解决N+1 ORM问题的机制。
1. Spring Data JPA方法:
如果我们使用Spring Data JPA,那么我们有两种选择来实现这一点-使用实体图或使用选择带有获取连接的查询。
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findAllBy();
@Query("SELECT p FROM User p LEFT JOIN FETCH p.roles")List<User> findWithoutNPlusOne();
@EntityGraph(attributePaths = {"roles"})List<User> findAll();}
使用左连接获取在数据库级别发出N+1个查询,我们使用属性路径解决N+1问题,Spring Data JPA避免了N+1问题
-- This loop is executed oncefor parent in (select * from parent) loop
-- This loop is executed N timesfor child in (select * from child where parent_id = parent.id) loop...end loop;end loop;
最好使用连接来实现这一点(在这种情况下):
for rec in (select *from parent pjoin child c on c.parent_id = p.id)loop...end loop;