“N+1选择问题”在对象关系映射(ORM)讨论中通常被称为一个问题,我理解这与必须为对象世界中看似简单的东西进行大量数据库查询有关。

有人对这个问题有更详细的解释吗?


当前回答

在不深入技术堆栈实现细节的情况下,从架构上讲,N+1问题至少有两种解决方案:

只有1个-大查询-带有联接。这使得大量信息从数据库传输到应用程序层,特别是如果有多个子记录。数据库的典型结果是一组行,而不是对象图(有不同的DB系统的解决方案)有两个(或更多需要连接的子级)查询-1个用于父级,在有了它们之后-通过ID查询子级并映射它们。这将最大限度地减少DB和APP层之间的数据传输。

其他回答

SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

这将获得一个结果集,其中表2中的子行通过返回表2中每个子行的表1结果而导致重复。O/R映射器应根据唯一的键字段区分表1实例,然后使用所有表2列填充子实例。

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N+1是第一个查询填充主对象,第二个查询填充返回的每个唯一主对象的所有子对象的位置。

考虑:

class House
{
    int Id { get; set; }
    string Address { get; set; }
    Person[] Inhabitants { get; set; }
}

class Person
{
    string Name { get; set; }
    int HouseId { get; set; }
}

以及具有类似结构的表格。对地址“22 Valley St”的单个查询可能返回:

Id Address      Name HouseId
1  22 Valley St Dave 1
1  22 Valley St John 1
1  22 Valley St Mike 1

O/RM应该用ID=1,Address=“22 Valley St”填充Home的实例,然后用Dave、John和Mike的People实例填充Inhabitants数组,只需一个查询。

对上述相同地址的N+1查询将导致:

Id Address
1  22 Valley St

使用单独的查询,如

SELECT * FROM Person WHERE HouseId = 1

并产生单独的数据集,如

Name    HouseId
Dave    1
John    1
Mike    1

并且最终结果与上述单个查询相同。

单一选择的优点是您可以提前获得所有数据,这可能是您最终想要的。N+1的优点是减少了查询复杂性,并且可以使用延迟加载,其中子结果集仅在第一次请求时加载。

Hibernate和Spring数据JPA中的N+1问题

N+1问题是对象关系映射中的一个性能问题,它为应用层的单个选择查询在数据库中触发多个选择查询(准确地说是N+1,其中N=表中的记录数)。Hibernate&Spring Data JPA提供了多种方法来捕获和解决这个性能问题。

什么是N+1问题?

为了理解N+1问题,让我们考虑一个场景。假设我们有一个映射到数据库中DB_User表的User对象集合,每个用户都有一个使用联接表DB_User_Role映射到DB_Role表的集合或角色。在ORM级别,用户与角色具有多对多关系。

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
 }

一个用户可以有许多角色。角色加载缓慢。现在假设我们想从这个表中获取所有用户,并为每个用户打印角色。非常幼稚的对象关系实现可能是-具有findAllBy方法的UserRepository

public interface UserRepository extends CrudRepository<User, Long> {

    List<User> findAllBy();
}

ORM执行的等效SQL查询为:

首先获取所有用户(1)

Select * from DB_USER;

然后获取每个用户执行N次的角色(其中N是用户数)

Select * from DB_USER_ROLE where userid = <userid>;

因此,我们需要为User选择一个选项,为每个用户提取角色选择N个选项,其中N是用户总数。这是ORM中的一个经典N+1问题。

如何识别?

Hibernate提供了跟踪选项,可以在控制台/日志中启用SQL日志记录。使用日志,您可以很容易地看到hibernate是否为给定的调用发出N+1个查询。

如果您在给定的select查询中看到多个SQL条目,那么很可能是由于N+1问题。

N+1分辨率

在SQL级别,ORM要避免N+1,需要实现的是启动一个连接两个表的查询,并在单个查询中获得组合结果。

获取联接SQL,用于检索单一查询中的所有内容(用户和角色)

OR普通SQL

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.弹簧数据JPA方法:

如果我们使用的是SpringDataJPA,那么我们有两个选项来实现这一点-使用EntityGraph或使用带有fetch连接的select查询。

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个查询在数据库级别使用左连接获取发出,我们使用attributePaths解决N+1问题,Spring Data JPA避免N+1问题

2.休眠方法:

如果它是纯Hibernate,那么以下解决方案将起作用。

使用HQL:

from User u *join fetch* u.roles roles roles

使用标准API:

Criteria criteria = session.createCriteria(User.class);
criteria.setFetchMode("roles", FetchMode.EAGER);

所有这些方法的工作原理都类似,它们使用左连接获取发出类似的数据库查询

N+1选择问题是一个难题,在单元测试中检测这种情况是有意义的。我已经开发了一个小型库,用于验证给定测试方法或任意代码块执行的查询数量-JDBC Sniffer

只需在测试类中添加一个特殊的JUnit规则,并在测试方法上放置带有预期查询数的注释:

@Rule
public final QueryCounter queryCounter = new QueryCounter();

@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}

假设您有一组Car对象(数据库行),每个Car都有一组Wheel对象(也有行)。换句话说,汽车→ 轮子是一对多的关系。

现在,假设您需要遍历所有汽车,并为每辆汽车打印出车轮列表。天真的O/R实现将执行以下操作:

SELECT * FROM Cars;

然后,对于每辆车:

SELECT * FROM Wheel WHERE CarId = ?

换言之,您可以为汽车选择一个选项,然后再选择N个选项,其中N是汽车的总数。

或者,可以获取所有轮子并在内存中执行查找:

SELECT * FROM Wheel;

这将到数据库的往返次数从N+1减少到2。大多数ORM工具提供了几种防止N+1选择的方法。

参考资料:Java Persistence with Hibernate,第13章。

提供的链接有一个非常简单的n+1问题示例。如果你将它应用于Hibernate,它基本上是在谈论相同的事情。查询对象时,实体将被加载,但任何关联(除非另有配置)都将被延迟加载。因此,一个查询用于根对象,另一个查询加载每个根对象的关联。返回的100个对象意味着一个初始查询,然后是100个附加查询,以获得每个对象的关联,n+1。

http://pramatr.com/2009/02/05/sql-n-1-selects-explained/