前言
使用 JPA 时,我们一般通过 @Entity 进行实体类映射,从数据库中查询出对象。然而,在实际开发中,有时须要自定义查询结果并将其直接映射到 DTO,而不是实体类。这种需求可以通过 JPA 原生 SQL 查询和 DTO 投影 来实现。博主将以实际开发场景 为例,快速摘要如安在 JPA 中实现基于原生 SQL 的 DTO 投影
开始 - 实现步骤
以下是实现 DTO 投影的完整步骤,包括实体类、SQL 映射配置、接口调用和 DTO 设计。
一、配置实体类及映射
首先在实体类中定义 @SqlResultSetMapping,用于将原生 SQL 查询结果映射到 DTO 类。在这个例子中,我们定义了 IssueVideo 实体,并通过 @SqlResultSetMapping 和 @NamedNativeQuery 配置了一个纯sql查询
两个注解 详解 (已理解可以跳过)
- 1. @NamedNativeQuery
焦点作用
- @NamedNativeQuery 是用来定义 原生 SQL 查询 的。
- 尽管JPA 中已经为sql 提供了许多方便的办理方式,但是某些场景下,我们照旧须要直接使用原生 SQL , 例如:
- 数据查询逻辑复杂,无法用 JPQL 表达
- 涉及数据库特定的功能(如窗口函数、分区排序等)
- 查询结果无法直接映射到实体类(如 DTO、聚合结果)
通过 @NamedNativeQuery,我们可以直接在实体类中绑定一个原生 SQL 查询,并为这个查询定名。在调用时,可以通过指定这个定名的查询名称直接执行该 SQL
- 2. @SqlResultSetMapping
焦点作用
- @SqlResultSetMapping 是用来定义 查询结果的映射 规则的。
- 当我们使用原生 SQL 查询时,返回的结果是数据库的行列数据,与实体类的属性或 DTO 的结构未必完全匹配。
- @SqlResultSetMapping(name = "", ...) -> 它长这样
复制代码 它用来告诉 JPA:
- 查询返回的列和 DTO 的字段如何一一对应
- 如何将原生 SQL 查询的结果映射为自定义的类(DTO)
没有 @SqlResultSetMapping 时,JPA 会尝试将查询结果映射到实体类,但如果结果不是直接对应实体类那么映射就会失败。这时,我们就须要 @SqlResultSetMapping 来自定义映射规则。
- 3. 为什么须要将它们写在实体类上?
- 实体类是 SQL 映射的入口
在 JPA 中,实体类是我们与数据库表交互的焦点对象。因此:
- 将 @NamedNativeQuery 和 @SqlResultSetMapping 写在实体类上,可以明确这段查询与该实体相关,方便维护和查阅。
- JPA 的原生查询和结果映射机制依赖于实体类的原数据,通过注解绑定的方式,可以让这些查询和映射规则作为实体类的一部分,便于复用。
- 关联性强
- @NamedNativeQuery
定义了原生 SQL 查询,而 @SqlResultSetMapping 定义了如何映射这个查询的结果,它们是 成对使用的。二者写在同一个实体类上,能清晰地表达“该查询和该实体类相关”的逻辑。
- 如果你把它们分散到其他地方,大概会增长代码复杂性和维护成本。
简朴来说,@NamedNativeQuery 和 @SqlResultSetMapping 分别办理了两个差别的问题:
- @NamedNativeQuery 负责“如何查询”
它定义了 SQL 的逻辑以及参数。
- @SqlResultSetMapping 负责“如何处理查询结果”
它定义了如何将 SQL 返回的数据映射到 DTO中
二者通过查询名称(name 属性)接洽在一起。例如:
- @NamedNativeQuery(
- name = "IssueRecommendRespDTOQuery", // 查询名称
- resultSetMapping = "IssueRecommendRespDTOResult", // 映射↓
- query = """
- SELECT ... -- 原生 SQL 查询
- """
- )
- @SqlResultSetMapping(
- name = "IssueRecommendRespDTOResult", // 映射名称 对应↑
- classes = @ConstructorResult(
- targetClass = IssueRecommendRespDTO.class, // 映射到的 DTO
- columns = {
- @ColumnResult(name = "isdId", type = Integer.class),
- ...
- }
- )
- )
复制代码 两个注解表明 - end
实体类代码示例
- @AllArgsConstructor
- @NoArgsConstructor
- @Data
- @Entity
- @Table(name = "issue_video")
- // region jpa 投影主页查询目标dto (可以通过 "region <> endregion" 将其折叠起来)
- @SqlResultSetMapping(
- name = "IssueRecommendRespDTOResult",
- classes = @ConstructorResult(
- targetClass = IssueRecommendRespDTO.class,
- columns = {
- @ColumnResult(name = "isdId", type = Integer.class),
- @ColumnResult(name = "videoUrl", type = String.class),
- @ColumnResult(name = "duration", type = Integer.class),
- @ColumnResult(name = "issId", type = Integer.class),
- @ColumnResult(name = "title", type = String.class),
- @ColumnResult(name = "cover", type = String.class),
- @ColumnResult(name = "watchNum", type = BigInteger.class),
- @ColumnResult(name = "commentNum", type = Integer.class),
- @ColumnResult(name = "creTime", type = LocalDateTime.class),
- @ColumnResult(name = "authorId", type = Integer.class),
- @ColumnResult(name = "authorName", type = String.class)
- }
- )
- )
- // endregion
- // region jpa 投影主页推荐查询sql
- @NamedNativeQuery(
- name = "IssueRecommendRespDTOQuery",
- resultSetMapping = "IssueRecommendRespDTOResult",
- query = """
- SELECT
- sub.isd_id AS isdId,
- sub.video_url AS videoUrl,
- sub.duration AS duration,
- sub.iss_id AS issId,
- sub.title AS title,
- sub.cover AS cover,
- sub.watch_num AS watchNum,
- sub.comment_num AS commentNum,
- sub.cre_time AS creTime,
- sub.u_id AS authorId,
- sub.name AS authorName
- FROM (
- SELECT
- i.score,
- v.isd_id,
- v.video_url,
- v.duration,
- i.iss_id,
- i.title,
- i.cover,
- i.watch_num,
- i.comment_num,
- i.cre_time,
- author.u_id,
- author.name,
- case
- when ROW_NUMBER() OVER (
- PARTITION BY i.su_id
- ORDER BY
- CASE
- WHEN sp.su_id IS NOT NULL THEN (i.score + sp.score)
- ELSE i.score
- END DESC
- ) <= 3 then 1
- else 2
- end AS rank_within_partition,
- RANK() OVER (
- PARTITION BY i.su_id
- ORDER BY
- CASE
- WHEN sp.su_id IS NOT NULL THEN (i.score + sp.score)
- ELSE i.score
- END DESC
- ) as global_rank
- FROM issue_video v
- JOIN issue i ON i.iss_id = v.iss_id
- JOIN user author ON author.u_id = i.u_id
- LEFT JOIN subarea_preference sp
- ON sp.su_id = i.su_id
- and sp.u_id = :uId
- order by (i.score + sp.score) desc
- ) sub
- order by rank_within_partition, global_rank
- """
- )
- // endregion
- public class IssueVideo {
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Id
- private Integer isdId;
- private String videoUrl;
- private int duration;
- private int size;
- private LocalDateTime updTime;
- private String remark;
- private LocalDateTime issueTime;
- private String permission;
- private Boolean isDeclare;
- private Boolean offDanmu;
- private Boolean offComm;
- private Boolean onGretestComm;
- private Boolean isDel;
- @ManyToOne
- @JoinColumn(name = "vd_id")
- private VideoDeclare videoDeclare;
- private BigInteger danmuNum;
- @OneToOne
- @JoinColumn(name = "iss_id")
- private Issue issue;
- }
复制代码
二、配置对应 JPA 接口
在 JPA 接口中,直接通过 @Query 注解调用刚刚定义的原生 SQL 查询,并将结果映射成期望的 DTO 返回类型
Repository 代码示例
- public interface IssueVideoRepo extends JpaRepository<IssueVideo, Integer> {
- /**
- * 获取主页推荐视频
- * @param uId 用户 ID
- * @return DTO 列表
- */
- @Query(name = "IssueRecommendRespDTOQuery", nativeQuery = true)
- List<IssueRecommendRespDTO> getRecommendVideos(@Param("uId") Integer uId);
- }
复制代码
三、事件层调用接口
事件层负责调用 Repository 接口,并将返回的结果处理为终极服务层须要的数据。以下为服务实现代码:
Service 代码示例
- @Service
- public class IssueVideoServiceImpl implements IssueVideoService {
- @Autowired
- private IssueVideoRepo issueVideoRepo;
- @Override
- public List<IssueRecommendRespDTO> getRecommendVideos(Integer uId) {
- List<IssueRecommendRespDTO> recommendVideos = null;
- try {
- recommendVideos = issueVideoRepo.getRecommendVideos(uId);
- } catch (Exception exception) {
- exception.printStackTrace();
- }
- return recommendVideos;
- }
- }
复制代码
四、定义 DTO 类
最后,定义与查询结果对应的 DTO 类。DTO 结构须要与 @SqlResultSetMapping 中的字段一一对应。
DTO 代码示例
- @Data // 主dto 不需要lombok 全参数注解 @AllArgsConstructor, 因为要额外配置
- public class IssueRecommendRespDTO {
- private Integer isdId;
- private String videoUrl;
- private Integer duration;
- private IssueRecommendInDTO issue;
- public IssueRecommendRespDTO() {}
- public IssueRecommendRespDTO(Integer isdId, String videoUrl, Integer duration,
- Integer issId, String title, String cover,
- BigInteger watchNum, Integer commentNum, LocalDateTime creTime,
- Integer authorId, String authorName) {
- this.isdId = isdId;
- this.videoUrl = videoUrl;
- this.duration = duration;
- this.issue = new IssueRecommendInDTO(
- issId, title, cover,
- watchNum, commentNum, creTime,
- new AuthorInDTO(authorId, authorName)
- );
- }
- }
- @AllArgsConstructor
- @NoArgsConstructor
- @Data
- public class IssueRecommendInDTO { // In命名表示 该dto 很可能处于内部使用 > internal : 内部的
- private Integer issId;
- private String title;
- private String cover;
- private BigInteger watchNum;
- private Integer commentNum;
- private LocalDateTime creTime;
- private AuthorInDTO author;
- }
- @AllArgsConstructor
- @NoArgsConstructor
- @Data
- public class AuthorInDTO {
- private Integer authorId;
- private String authorName;
- }
复制代码
总结
上述步骤以颠末实际开发测试,保证有效!
- 高效性:直接通过原生 SQL 查询所需数据,减少不须要字段的查询和映射。
- 灵活性:可以自由定义 DTO 结构,满意复杂查询的需求。
- 可维护性:使用 @SqlResultSetMapping 将 SQL 与 Java 类关联,便于后续维护。
该文章适用于须要自定义复杂查询且无需将查询结果绑定到实体类的场景。如果你也有类似需求,不妨以文章为参照上手试试!
end…
如果这篇文章帮到你, 帮忙点个关注呗, 不想那那那点赞或收藏也行鸭 (。•̀ᴗ-)✧ ~
'(இ﹏இ`。)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |