【问题标题】:How to avoid N+1 problem with native SQL query in springboot with Hibernate?如何在使用 Hibernate 的 Spring Boot 中避免原生 SQL 查询出现 N+1 问题?
【发布时间】:2021-07-21 22:25:52
【问题描述】:

我正在使用 POSTGIS 内置函数查询我的数据库,以检索给定位置的最接近的Machines。 我必须使用原生 SQL,因为 Hibernate 不支持 POSTGIS 和 CTE:

    @Repository
    public interface MachineRepository extends JpaRepository<Machine, Long>{
    @Query(value =
                "with nearest_machines as\n" +
                "         (\n" +
                "             select distance_between_days(:id_day, machine_availability.id_day) as distance_in_time,\n" +
                "                    ST_Distance(geom\\:\\:geography, ST_SetSrid(ST_MakePoint(:longitude, :latitude), 4326)\\:\\:geography) as distance_in_meters,\n" +
                "                    min(id_day) over (partition by machine.id) as closest_timeslot_per_machine,\n" +
                "                    machine_availability.id_day,\n" +
                "                    machine.*\n" +
                "             from machine\n" +
                "                      join machine_availability on machine.id = machine_availability.id_machine\n" +
                "             where machine_availability.available = true\n" +
                "               and machine_availability.id_day >= :today\n" +
                "               and ST_DWithin(geom\\:\\:geography, ST_SetSrid(ST_MakePoint(:longitude, :latitude), 4326)\\:\\:geography, 1000)\n" +
                "         )\n" +
                "select nearest_machines.*\n" +
                    "from nearest_machines\n" +
                    "where id_day = closest_timeslot_per_machine\n" +
                    "order by distance_in_time, distance_in_meters\n" +
                    "limit 20;",
            nativeQuery = true)
        List<Machine> findMachinesAccordingToAvailabilities(@Param("longitude") BigDecimal longitude,
                                                            @Param("latitude") BigDecimal latitude,
                                                            @Param("id_day") String idDay,
                                                            @Param("today") String today);
}

当然,MachineMachineAvailability@Entity 的。它们与@OneToMany(fetch = FetchType.EAGER) 相关。我将默认 LAZY 更改为 EAGER,因为我需要在最终 JSON 中使用 MachineAvailability

问题在于它触发了结果机器的另外 2 个请求(即著名的 N+1 问题)。

1.我怎样才能在一个请求中解决这个问题?

2.是否有可能以某种方式创建我的 JSON 并直接在 MachineController 中返回它?

【问题讨论】:

  • 这里有些不对劲,因为如果您发出的是原生 SQL 查询,那么 Hibernate N+1 问题根本不应该是一个问题,甚至应该是不可能的。是什么让您认为这里发生了 N+1?
  • 因为我在日志中看到了请求。
  • 那么它们来自您的原始 SQL 查询,而不是来自 Hibernate/JPA。 AFAIK 当您将 native 设置为 true 时,您基本上只是在执行本机 JDBC 准备语句。
  • 他们来自@OneToMany(fetch = FetchType.EAGER)。当我切换到 LAZY 时,问题消失了,但我没有可用性!!!

标签: postgresql spring-boot hibernate postgis rawsql


【解决方案1】:

在 1 个请求中解决这个问题很困难,因为您必须使用 Hibernate 原生 API 来映射可用性集合的表别名。您需要在主查询中为可用性添加一个连接并执行以下操作:session.createNativeQuery("...").addEntity("m", Machine.class).addFetch("av", "m", "availabilities")

另一种选择是使用Blaze-Persistence Entity Views,因为 Blaze-Persistence 支持 CTE 和 PostgreSQL 提供的更多好东西,这对您来说可能是一个有趣的解决方案。

我创建了该库以允许在 JPA 模型和自定义接口或抽象类定义模型之间轻松映射,例如 Spring Data Projections on steroids。这个想法是您按照自己喜欢的方式定义目标结构(域模型),并通过 JPQL 表达式将属性(getter)映射到实体模型。

我不知道您的模型,但是对于您的用例,可能的 DTO 模型使用 Blaze-Persistence Entity-Views 可能如下所示:

@EntityView(Machine.class)
@With(NearestMachineCteProvider.class)
@EntityViewRoot(name = "nearest", entity = NearestMachine.class, condition = "machineId = VIEW(id)", joinType = JoinType.INNER)
public interface MachineDto {
    @IdMapping
    Integer getId();
    String getName();
    @Mapping("nearest.distanceInTime")
    Integer getDistanceInTime();
    @Mapping("nearest.distanceInMeters")
    Double getDistanceInMeters();
    Set<MachineAvailabilityDto> getAvailabilities();

    @EntityView(MachineAvailability.class)
    interface MachineAvailabilityDto {
        @IdMapping
        Integer getId();
        String getName();
    }

    class NearestMachineCteProvider implements CTEProvider {
        @Override
        public void applyCtes(CTEBuilder<?> builder, Map<String, Object> optionalParameters) {
            builder.with(NearestMachine.class)
                .from(Machine.class, "m")
                .bind("distanceInTime").select("CAST_INTEGER(FUNCTION('distance_between_days', :id_day, m.availabilities.idDay))")
                .bind("distanceInMeters").select("CAST_DOUBLE(FUNCTION('ST_Distance', m.geom, FUNCTION('ST_SetSrid', FUNCTION('ST_MakePoint', :longitude, :latitude), 4326)))")
                .bind("closestTimeslotId").select("min(m.availabilities.idDay) over (partition by m.id)")
                .bind("machineId").select("m.id")
                .bind("machineAvailabilityDay").select("m.availabilities.idDay")
                .where("m.availabilities.available").eqLiteral(true)
                .where("m.availabilities.idDay").geExpression(":today")
                .where("FUNCTION('ST_DWithin', m.geom, FUNCTION('ST_SetSrid', FUNCTION('ST_MakePoint', :longitude, :latitude), 4326), 1000)").eqLiteral(true)
                .end();
        }
    }
}

@CTE
@Entity
public class NearestMachine {
    private Integer distanceInTime;
    private Double distanceInMeters;
    private Integer closestTimeslotId;
    private Integer machineId;
    private Integer machineAvailabilityDay;
}

查询是将实体视图应用于查询的问题,最简单的就是通过 id 进行查询。

MachineDto a = entityViewManager.find(entityManager, MachineDto.class, id);

Spring Data 集成让您可以像使用 Spring Data Projections 一样使用它:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

Page<MachineDto> findAll(Pageable pageable);

然后您可以使用Sort.asc("distanceInTime")Sort.asc("distanceInMeters") 进行排序

最好的部分是,它只会获取实际需要的状态!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-11-30
    • 2013-03-30
    • 2016-09-30
    • 2017-11-06
    • 1970-01-01
    • 1970-01-01
    • 2022-01-19
    • 2018-09-29
    相关资源
    最近更新 更多