You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Data JPA优化查询:获取Rating时仅加载Player ID而非完整实体

问题:Spring Data JPA查询Rating时避免加载完整Player实体

我在Spring Data JPA中配置了Player和Rating实体,需求是根据用户ID查询其所有Rating数据,但不想为每条Rating加载完整的Player实体,仅需获取Player ID(或无需Player相关额外数据),如同数据库存储的形式。目前我在查询后映射为DTO,但认为这种方式不够优化,请问能否通过自定义查询或投影等方式实现,避免不必要的实体加载以提升性能?


实体定义

Rating 实体

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Rating {

    @EmbeddedId
    private RatingId ratingId;

    @ManyToOne
    @MapsId("playerId")
    @NotNull
    private Player player;

    @ManyToOne
    @MapsId("gameId")
    @NotNull
    private Game game;

    private float points;

    @ManyToOne
    private Platform platform;

    private Long startDate;

    private Long endDate;

    private Status status;

    public Rating(Player player, Game game) {
        this.ratingId = new RatingId(player.getId(), game.getId());
        this.player = player;
        this.game = game;
    }
}

Player 实体

@Entity
@EqualsAndHashCode
@NoArgsConstructor
@Getter
@Setter
public class Player {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Setter(AccessLevel.NONE)
    @Column(updatable = false)
    private Long id;

    @NotBlank
    @Size(max = 20)
    @Column(unique = true)
    private String username;

    @NotBlank
    @Size(max = 50)
    @Email
    @Column(unique = true)
    private String email;

    @NotBlank
    @Size(max = 120)
    private String password;

    @EqualsAndHashCode.Exclude
    private Set<Role> roles = new HashSet<>();

    public Player(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }
}

RatingId 嵌入式ID

@Embeddable
@NoArgsConstructor
@AllArgsConstructor
public class RatingId implements Serializable {

    private Long playerId;
    private Long gameId;
}

现有Repository

@Repository
public interface RatingRepository extends JpaRepository<Rating, RatingId> {
    List<Rating> findAllByPlayerId(Long playerId);
}

数据库直接查询结果

database=# select * from rating where rating.player_id = 1;

 end_date | points | start_date | status | player_id | game_id | platform_id
----------+--------+------------+--------+-----------+---------+------------- 
          |    1.5 |            |      0 |         1 |    9174 |           6

解决方案

下面提供几种无需加载完整Player实体的优化方案,按性能和易用性排序:

1. 使用Spring Data Projections(投影)

创建一个仅包含所需字段的投影接口,Spring Data会自动生成只查询指定字段的SQL,完全避免加载Player实体。

步骤1:定义投影接口

public interface RatingProjection {
    // Rating核心字段
    Float getPoints();
    Long getStartDate();
    Long getEndDate();
    Status getStatus();
    // 直接从嵌入式ID获取Player/Game ID,无需关联实体
    Long getPlayerId();
    Long getGameId();
    // 如需Platform ID
    Long getPlatformId();
}

步骤2:修改Repository方法

@Repository
public interface RatingRepository extends JpaRepository<Rating, RatingId> {
    List<RatingProjection> findAllByPlayerId(Long playerId);
}

优势:无需手写SQL,自动生成最优查询,仅选择需要的字段,性能最高。

2. 自定义JPQL查询(映射到DTO或Map)

通过JPQL明确指定查询字段,完全控制SQL逻辑,避免关联Player表。

方式A:映射到DTO

先定义DTO类:

public class RatingDto {
    private Long playerId;
    private Long gameId;
    private Float points;
    private Long startDate;
    private Long endDate;
    private Status status;
    private Long platformId;

    // 构造方法参数顺序需与JPQL字段顺序一致
    public RatingDto(Long playerId, Long gameId, Float points, Long startDate, Long endDate, Status status, Long platformId) {
        this.playerId = playerId;
        this.gameId = gameId;
        this.points = points;
        this.startDate = startDate;
        this.endDate = endDate;
        this.status = status;
        this.platformId = platformId;
    }

    // Getter方法
    public Long getPlayerId() { return playerId; }
    // 其他Getter省略
}

然后在Repository中添加查询方法:

@Query("SELECT new com.yourpackage.RatingDto(" +
       "r.ratingId.playerId, r.ratingId.gameId, r.points, r.startDate, r.endDate, r.status, r.platform.id) " +
       "FROM Rating r WHERE r.ratingId.playerId = :playerId")
List<RatingDto> findRatingsByPlayerId(@Param("playerId") Long playerId);

方式B:返回Map

如果不需要DTO,也可以直接返回Map:

@Query("SELECT r.ratingId.playerId as playerId, r.ratingId.gameId as gameId, r.points, r.startDate, r.endDate, r.status, r.platform.id as platformId " +
       "FROM Rating r WHERE r.ratingId.playerId = :playerId")
List<Map<String, Object>> findRatingsByPlayerId(@Param("playerId") Long playerId);

3. 修改关联的FetchType为LAZY(保留Rating实体)

默认@ManyToOne的FetchType是EAGER,改为LAZY后,Player会被初始化为代理对象,不会触发实际查询。你可以通过嵌入式ID直接获取Player ID,无需访问关联的Player实体。

修改Rating实体的Player关联

@ManyToOne(fetch = FetchType.LAZY)
@MapsId("playerId")
@NotNull
private Player player;

使用原有Repository方法

List<Rating> findAllByPlayerId(Long playerId);

注意:避免在事务上下文之外访问rating.getPlayer()的其他属性,否则会抛出懒加载异常。直接通过rating.getRatingId().getPlayerId()获取ID即可。

4. 原生SQL查询

如果需要完全匹配数据库原生查询逻辑,可以使用原生SQL:

@Query(value = "SELECT player_id, game_id, points, start_date, end_date, status, platform_id " +
               "FROM rating WHERE player_id = :playerId", nativeQuery = true)
List<Object[]> findRatingsByPlayerIdNative(@Param("playerId") Long playerId);

返回的Object[]中元素顺序与SQL字段顺序一致,可手动映射到DTO或直接使用。


总结
  • 若无需完整Rating实体:优先选择投影(方案1)或JPQL构造DTO(方案2),性能最优,仅查询所需字段。
  • 若需保留Rating实体:选择方案3(懒加载+嵌入式ID),但需注意避免触发懒加载。

内容的提问来源于stack exchange,提问作者Feldwebel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 11:49:54