当前位置:首页 > 技术文章 > 正文内容

最强ORM让你开发效率提升百倍(orm开发框架有哪些)

zonemu1周前 (08-16)技术文章8

最强ORM让你开发效率提升百倍

easy-query 在经过2年的迭代目前已经在查询领域可以说是无敌的存在,任何 orm 都不是对手,这几年的功能点简单罗列一下

  • 动态join:查询涉及到对应的关系表就会自动添加join反之则不会讲join加入到sql中(2025年了感觉也不是什么新鲜特性了)
  • 结构化DTO,自动根据DTO和表关系的路径自动筛选出需要的结构化属性(开发效率杀手)
  • 结构化DTO额外配置,支持结构化DTO的返回下还能额外添加查询条件和筛选条件
  • 隐式PARTITION BY
  • 隐式子查询
  • 子查询转GroupJoin 全世界应该是独一份的功能,解决多对多深层关系在ORM中的子查询过多导致的性能问题真正解决了ORM在复杂查询下开发效率和性能的兼顾

框架地址 github.com/dromara/eas… 文档地址
www.easy-query.com/easy-query-… 该文章 demo 地址 github.com/xuejmnet/eq… #技术分享

刚好前几天我看到公众号有篇关于 efcore 的性能文章,我看了其实我一眼就知道了他的问题就是 eq 的 子查询转 GroupJoin 但是正如强大的 efcore 也是没有实现该功能,话不多说本章节我们将入门通过公众号的 demo 实现大部分帖子相关的查询和功能

建模

实体关系如下:

  • 用户User:每个用户有多篇帖子和多条评论和多个点赞
  • 分类Category:帖子所属分类类目支持多个分类一个帖子或者多个帖子公用同一个分类
  • 帖子Post:每篇帖子有多个分类并可获得多个赞
  • 评论Comment:每条评论属于一个用户并关联一篇帖子 且评论支持楼中楼
  • 点赞Like:每个赞关联一篇帖子,多个点赞可以关联同一篇帖子
  • 分类帖子关联CategoryPost:帖子和分类的关联关系表

点击查看实体代码

@Data
@Table("t_user")
@EntityProxy
@EasyAlias("t_user")
@EasyAssertMessage("未找到对应的用户信息")
public class User implements ProxyEntityAvailable<User , UserProxy> {
    @Column(primaryKey = true,comment = "用户id")
    private String id;
    @Column(comment = "用户姓名")
    private String name;
    @Column(comment = "用户手机")
    private String phone;
    @Column(comment = "创建时间")
    private LocalDateTime createAt;
}

@Data @Table("t_category") @EntityProxy @EasyAlias("t_category") @EasyAssertMessage("未找到对应的类目信息") public class Category implements ProxyEntityAvailable<Category , CategoryProxy> { @Column(primaryKey = true,comment = "类目 id") private String id; @Column(comment = "类目姓名") private String name; @Column(comment = "类目排序") private Integer sort; }

@Data @Table("t_post") @EntityProxy @EasyAlias("t_post") @EasyAssertMessage("未找到对应的帖子信息") public class Post implements ProxyEntityAvailable<Post, PostProxy> { @Column(primaryKey = true,comment = "帖子 id") private String id; @Column(comment = "帖子标题") private String title; @Column(comment = "帖子内容") private String content; @Column(comment = "用户 id") private String userId; @Column(comment = "发布时间") private LocalDateTime publishAt; }

@Data @Table("t_comment") @EntityProxy @EasyAlias("t_comment") @EasyAssertMessage("未找到对应的评论信息") public class Comment implements ProxyEntityAvailable<Comment , CommentProxy> { @Column(primaryKey = true,comment = "评论 id") private String id; @Column(comment = "父 id") private String parentId; @Column(comment = "帖子内容") private String content; @Column(comment = "用户 id",nullable = false) private String userId; @Column(comment = "帖子 id",nullable = false) private String postId; @Column(comment = "回复时间") private LocalDateTime createAt; } @Data @Table("t_like") @EntityProxy @EasyAlias("t_like") @EasyAssertMessage("未找到对应的点赞信息") public class Like implements ProxyEntityAvailable<Like , LikeProxy> { @Column(primaryKey = true,comment = "评论 id") private String id; @Column(comment = "用户 id",nullable = false) private String userId; @Column(comment = "帖子 id",nullable = false) private String postId; @Column(comment = "点赞时间") private LocalDateTime createAt; }

@Data @Table("t_category_post") @EntityProxy @EasyAlias("t_category_post") @EasyAssertMessage("未找到对应的类目帖子关联信息") public class CategoryPost implements ProxyEntityAvailable<CategoryPost , CategoryPostProxy> { @Column(primaryKey = true,comment = "评论 id") private String id; @Column(comment = "帖子 id",nullable = false) private String postId; @Column(comment = "类目 id",nullable = false) private String categoryId; }

帖子相关查询

帖子分页

对 Post 表进行分页按 publishAt 倒序进行排序按 title 进行搜索

首先我们定一个公用类

@Data
public class PageRequest {
    private Integer pageIndex=1;
    private Integer pageSize=5;
}

定义请求参数

@Data
public class PostPageRequest extends PageRequest {
    private String title;
}

分页动态条件

@PostMapping("/page")
    public EasyPageResult<Post> page(@RequestBody PostPageRequest request) {
        return easyEntityQuery.queryable(Post.class)
                .where(t_post -> {

t_post.title().contains(EasyStringUtil.isNotBlank(request.getTitle()),request.getTitle()); }) .orderBy(t_post -> t_post.publishAt().desc()) .toPageResult(request.getPageIndex(),request.getPageSize()); }

这边提供了两种方式实现动态查询,当 title 不为空的时候加入表达式筛选,执行我们来看看实际情况

  • 使用if函数包裹表达式断言,支持任意java表达式
  • 使用断言函数第一个参数重载,默认第一个参数为true才会执行断言操作
  • 使用where重载第一个参数为true执行当前where

请求参数

{"pageIndex":1,"pageSize":5,"title":"电影"}
==> Preparing: SELECT COUNT(*) FROM `t_post` WHERE `title` LIKE CONCAT('%',?,'%')
==> Parameters: 电影(String)

==> Preparing: SELECT `id`,`title`,`content`,`user_id`,`publish_at` FROM `t_post` WHERE `title` LIKE CONCAT('%',?,'%') ORDER BY `publish_at` DESC LIMIT 3 ==> Parameters: 电影(String)

container 还是 like!!! > 细心地朋友会发现我们使用了 contains 函数而不是 like 函数,因为当传入的查询条件本身带有%时那么 like 会让%变成通配符,而 contains 会将%视为被查询的一部分,这是需要用户注意的,具体使用 contains 还是 like 应该有用户自行决断

推荐写法: 可能由用户会问如果我添加的条件有很多怎么办难道每一个都要这么写一遍吗?eq 贴心的提供了多种方式来实现动态查询比如 filterConfigure

easyEntityQuery.queryable(Post.class)
          .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
          .where(t_post -> {
              t_post.title().contains(request.getTitle());
          })
          .orderBy(t_post -> t_post.publishAt().desc())
          .toPageResult(pageIndex,pageSize);

通过添加 filterConfigure 支持让参数为 null 不参与业务,如果是字符串则必须保证 isNotBlank ,当然用户也可以通过自己的自定义来实现

更多的动态条件设置请参考文档

> 正常我们推荐使用 filterConfigure 或者使用 if 函数包裹条件而不是使用方法参数的第一个 boolean 类型来控制,因为参数 boolean 类型重载相对会让表达式不够直观且难以阅读所以我们极力推荐另外几种方式

我们学会了如何在单表查询分页下使用动态参数控制 sql,那么接下来我们将学习如何使用参数外部控制动态排序

分页动态排序

首先我们对请求的条件进行修改

@Data
public class PostPage3Request extends PageRequest {
    private String title;

private List<InternalOrder> orders;

@Data public static class InternalOrder{ private String property; private boolean asc; } }

@PostMapping("/page3") public EasyPageResult<Post> page3(@RequestBody PostPage3Request request) { return easyEntityQuery.queryable(Post.class) .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS) .where(t_post -> { t_post.title().contains(request.getTitle()); }) .orderBy(request.getOrders()!=null,t_post -> { for (PostPage3Request.InternalOrder order : request.getOrders()) { t_post.anyColumn(order.getProperty()).orderBy(order.isAsc()); } }) .toPageResult(request.getPageIndex(),request.getPageSize()); }

请求参数

{"pageIndex":1,"pageSize":5,"title":"","orders":[{"property":"publishAt","asc":false},{"property":"title","asc":true}]}

执行请求后生成的 sql 为

==> Preparing: SELECT COUNT(*) FROM `t_post`
<== Time Elapsed: 13(ms)

==> Preparing: SELECT `id`,`title`,`content`,`user_id`,`publish_at` FROM `t_post` ORDER BY `publish_at` DESC,`title` ASC LIMIT 5 <== Time Elapsed: 17(ms)

支持我们已经支持了分页的动态排序,当然动态排序功能远不止此, 更多动态排序请点击链接

分页join筛选

当然对于大部分业务而言实体对象不是一个孤单对象,当前的 Post 对象也是如此,我们经常会有连表或者子查询等操作,那么 eq 是如何快速便捷的实现 join 的呢

下面这种通过关联关系实现 join 的操作我们称之为 隐式 join

  • 查询帖子要求查询条件是某个用户的

首先因为涉及到 join 那么 eq 提供了关联关系将原先的 Post 单表和用户表进行多对一的关联

通过插件生成关联关系

  • 第一步呼出ui界面
  • 第二步设置关联关系

选择好对应的关联键后点击确认插件会帮你自动生成强类型属性|lombok 属性或字符串

当然你也可以手写关联关系

@Data
@Table("t_post")
@EntityProxy
@EasyAlias("t_post")
@EasyAssertMessage("未找到对应的帖子信息")
public class Post implements ProxyEntityAvailable<Post, PostProxy> {
    @Column(primaryKey = true, comment = "帖子id")
    private String id;
    @Column(comment = "帖子标题")
    private String title;
    @Column(comment = "帖子内容")
    private String content;
    @Column(comment = "用户id")
    private String userId;
    @Column(comment = "发布时间")
    private LocalDateTime publishAt;

@Navigate(value = RelationTypeEnum.ManyToOne, selfProperty = {PostProxy.Fields.userId}, targetProperty = {UserProxy.Fields.id}) private User user; }

修改完实体对象后我们做了一个 post.userId=user.id 的关系接下来我们创建查询对象

@Data
public class PostPage4Request extends PageRequest {
    private String title;

private String userName; ①

private List<InternalOrder> orders;

@Data public static class InternalOrder{ private String property; private boolean asc; } }

@PostMapping("/page4") public EasyPageResult<Post> page4(@RequestBody PostPage4Request request) { return easyEntityQuery.queryable(Post.class) .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS) .where(t_post -> { t_post.title().contains(request.getTitle()); t_post.user().name().contains(request.getUserName()); }) .orderBy(request.getOrders()!=null,t_post -> { for (PostPage4Request.InternalOrder order : request.getOrders()) { t_post.anyColumn(order.getProperty()).orderBy(order.isAsc()); } }) .toPageResult(request.getPageIndex(),request.getPageSize()); }
  • ①是我们新添加的查询属性 userName

接下来我们发送请求

{
    "pageIndex": 1,
    "pageSize": 5,
    "title": "",
    "userName": "用户A",①
    "orders": [{
        "property": "user.createAt",②
        "asc": false
    }, {
        "property": "title",
        "asc": true
    }]
}
==> Preparing: SELECT COUNT(*) FROM `t_post` t LEFT JOIN `t_user` t1 ON t1.`id` = t.`user_id` WHERE t1.`name` LIKE CONCAT('%',?,'%')
==> Parameters: 用户A(String)

==> Preparing: SELECT t.`id`,t.`title`,t.`content`,t.`user_id`,t.`publish_at` FROM `t_post` t LEFT JOIN `t_user` t1 ON t1.`id` = t.`user_id` WHERE t1.`name` LIKE CONCAT('%',?,'%') ORDER BY t1.`create_at` DESC,t.`title` ASC LIMIT 3 ==> Parameters: 用户 A(String)
  • ①我们使用了用户名称作为筛选条件
  • ②我们使用了用户下的创建时间作为排序时间, user.createAtuser 是关联导航属性就是我们之前定义的多对一, createAt 是这个导航属性的字段名

当我们传递 userName 那么看下 sql 会是怎么样的

==> Preparing: SELECT COUNT(*) FROM `t_post` t

==> Preparing: SELECT t.`id`,t.`title`,t.`content`,t.`user_id`,t.`publish_at` FROM `t_post` t LEFT JOIN `t_user` t1 ON t1.`id` = t.`user_id` ORDER BY t1.`create_at` DESC,t.`title` ASC LIMIT 5

我们惊讶的发现 eq 非常智能的将分页中的 total 查询的所有 join 都去掉了,并且返回集合的那个 sql 任然保留,如果我们将 orderBy 也去掉会发现 eq 居然整个 sql 都不会添加 join 选项

==> Preparing: SELECT COUNT(*) FROM `t_post` t
<== Time Elapsed: 21(ms)
<== Total: 1
==> Preparing: SELECT t.`id`,t.`title`,t.`content`,t.`user_id`,t.`publish_at` FROM `t_post` t ORDER BY t.`title` ASC LIMIT 5
<== Time Elapsed: 18(ms)
<== Total: 5

你没有看错动态 join 就是这么简单,这就是真正的只能 orm 框架

回顾一下

  • 首先我们添加了动态查询筛选器配置 filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS) 让所有条件参数非null非空的值支持加入条件,这样就做到了动态查询的特性
  • 第二点因为我们传递userName参数,所以表达式的 t_post.user().name().contains(request.getUserName()); 会生效并且会自动根据对应的关系使用 leftJoin 将post和user关联起来并且查询post下的user下的姓名
  • 第三点因为我们没有传递userName参数,所以表达式的 t_post.user().name().contains(request.getUserName()); 不会生效,但是 orderByuser.createAt 还是会生效,所以page的时候 total 的哪一次查询因为没有使用 user 表所以不会 join ,但是toList的那一次因为 orderBy 用到了所以任然会进行 leftJoin

扩展篇

为什么使用leftJoin

因为任何两张表的关系在没有明确指定一定存在的情况下那么 leftJoin 的操作是不会影响主表的结果集,假如每个 Post 并不是都会有一个 user 的情况下我如果使用 user.createAt 进行排序那么 inner join 会让主表的结果集变少,但这是完全不被允许的这种做法会大大增加用户使用的心智负担

那么如果我希望使用 innerJoin 而不是 leftJoin 呢,我们可以再配置 @Navigate 的时候通过属性 required=true 来告知框架 Post 必定有 user

public class Post{

@Navigate(value = RelationTypeEnum.ManyToOne, selfProperty = {PostProxy.Fields.userId}, targetProperty = {UserProxy.Fields.id}, required = true) ① private User user; }

添加①属性 required = true 这样查询我们就能够发现框架会智能的使用 innerJoin 而不是 leftJoin

==> Preparing: SELECT COUNT(*) FROM `t_post` t INNER JOIN `t_user` t1 ON t1.`id` = t.`user_id` WHERE t1.`name` LIKE CONCAT('%',?,'%')
==> Parameters: 用户A(String)

==> Preparing: SELECT t.`id`,t.`title`,t.`content`,t.`user_id`,t.`publish_at` FROM `t_post` t INNER JOIN `t_user` t1 ON t1.`id` = t.`user_id` WHERE t1.`name` LIKE CONCAT('%',?,'%') ORDER BY t1.`create_at` DESC,t.`title` ASC LIMIT 3 ==> Parameters: 用户 A(String)

隐式join怎么添加on条件

很多细心的盆友可能希望我们在关联用户的时候添加额外的查询条件那么应该如何实现呢

请求 json 为如下不查询 userName,不进行 user 的属性排序

{
    "pageIndex":1,
    "pageSize":5,
    "title":"",
    "userName":"",
    "orders":[
        {
            "property":"title",
            "asc":true
        }
    ]
}
@PostMapping("/page4")
    public EasyPageResult<Post> page4(@RequestBody PostPage4Request request) {
        return easyEntityQuery.queryable(Post.class)
                .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
                .where(t_post -> {
                    t_post.user().filter(u -> { ①
                        u.phone().ne("123");
                    });
                    t_post.title().contains(request.getTitle());
                    t_post.user().name().contains(request.getUserName());
                })
                .orderBy(request.getOrders() != null, t_post -> {
                    for (PostPage4Request.InternalOrder order : request.getOrders()) {
                        t_post.anyColumn(order.getProperty()).orderBy(order.isAsc());
                    }
                })
                .toPageResult(request.getPageIndex(), request.getPageSize());
    }
==> Preparing: SELECT COUNT(*) FROM `t_post` t INNER JOIN `t_user` t1 ON t1.`id` = t.`user_id` AND t1.`phone` <> ?
==> Parameters: 123(String)

==> Preparing: SELECT t.`id`,t.`title`,t.`content`,t.`user_id`,t.`publish_at` FROM `t_post` t INNER JOIN `t_user` t1 ON t1.`id` = t.`user_id` AND t1.`phone` <> ? ORDER BY t.`title` ASC LIMIT 5 ==> Parameters: 123(String)
  • ①会将条件添加到join的on上面实现关联关系的定义筛选

奇怪的事情发生了为什么这次我们没有传递 user 相关的数据依然给我们把 inner join 加上了,其实本质是 inner joinon 条件是会影响主表数量,本质和写到 where 里面是一样的,所以虽然你没有 where 的条件但是 inner joinon 条件依然会让整个表达式的 join 无法动态优化,

filter!!! > 关联关系的 filter 会以 join on 的形式出现在 sql 中,相当于是额外对关联关系的筛选,缩小关系表,又因为 post 和 user 的关系为 post 必定有 user: required=true 所以会使用 inner join 代替 left join

帖子内容返回用户名

我们之前使用关联让帖子筛选支持用户姓名,那么如果我们需要返回帖子和对应的发帖人姓名应该怎么处理呢

创建响应dto

@Data
@EntityProxy ②
@SuppressWarnings("EasyQueryFieldMissMatch") ③
public class PostPage4Response {
    private String id;
    private String title;
    private String content;
    private String userId;
    private LocalDateTime publishAt;

    private String userName; ④
}
  • ①在dto上标记当前表来自于哪张表,插件可以提示相关错误
  • ②自定义dto对象代理实现表达式内赋值
  • ③因为①的存在所以④会有插件提示不存在这个字段的警告,通过添加③来让插件不进行提示
  • ④额外增加一个字段接受用户姓名
@PostMapping("/page5")
    public EasyPageResult<PostPage4Response> page5(@RequestBody PostPage4Request request) {
        return easyEntityQuery.queryable(Post.class)
                .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
                .where(t_post -> {
                    t_post.title().contains(request.getTitle());
                    t_post.user().name().contains(request.getUserName());
                })
                .orderBy(request.getOrders() != null, t_post -> {
                    for (PostPage4Request.InternalOrder order : request.getOrders()) {
                        t_post.anyColumn(order.getProperty()).orderBy(order.isAsc());
                    }
                })
                .select(t_post -> new PostPage4ResponseProxy() ①
                        .id().set(t_post.id())
                        .title().set(t_post.title())
                        .content().set(t_post.content())
                        .userId().set(t_post.userId())
                        .publishAt().set(t_post.publishAt())
                        .userName().set(t_post.user().name()) ②
                )
                .toPageResult(request.getPageIndex(), request.getPageSize());
    }
  • ①通过@EntityProxy注解eq框架会生成代理对象,改对象支持dsl表达式赋值
  • ②通过使用隐式join的方式赋值到dto中
==> Preparing: SELECT COUNT(*) FROM `t_post` t

==> Preparing: SELECT t.`id` AS `id`,t.`title` AS `title`,t.`content` AS `content`,t.`user_id` AS `user_id`,t.`publish_at` AS `publish_at`,t1.`name` AS `user_name` FROM `t_post` t INNER JOIN `t_user` t1 ON t1.`id` = t.`user_id` ORDER BY t.`title` ASC LIMIT 5

我们可以看到生成的 sql 将 joinuser 表的 name 赋值给了 dto 的 userName 属性

那么如果属性很多又一样我们是否可以有建议方便的做法呢

.select(t_post -> new PostPage4ResponseProxy()
        .selectAll(t_post) ①
        .userName().set(t_post.user().name())
)
  • ①将原先的属性赋值使用 selectAll 进行复制如果存在不需要的字段则可通过 selectIgnores 进行排除如下
return easyEntityQuery.queryable(Post.class)
                .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
                .where(t_post -> {
                    t_post.title().contains(request.getTitle());
                    t_post.user().name().contains(request.getUserName());
                })
                .orderBy(request.getOrders() != null, t_post -> {
                    for (PostPage4Request.InternalOrder order : request.getOrders()) {
                        t_post.anyColumn(order.getProperty()).orderBy(order.isAsc());
                    }
                })
                .select(t_post -> new PostPage4ResponseProxy()
                        .selectAll(t_post)
                        .selectIgnores(t_post.title())
                        .userName().set(t_post.user().name())
                )
                .toPageResult(request.getPageIndex(), request.getPageSize());
==> Preparing: SELECT COUNT(*) FROM `t_post` t

==> Preparing: SELECT t.`id`,t.`content`,t.`user_id`,t.`publish_at`,t1.`name` AS `user_name` FROM `t_post` t INNER JOIN `t_user` t1 ON t1.`id` = t.`user_id` ORDER BY t.`title` ASC LIMIT 5

那么是否有不使用@EntityProxy 的方式来返回呢

include查询

有时候我们希望返回的数据内容包含用户相关信息那么我们应该如何操作才能将返回的 post 信息里面包含 user 信息呢

@PostMapping("/page7")
    public EasyPageResult<Post> page7(@RequestBody PostPage7Request request) {
        return easyEntityQuery.queryable(Post.class)
                .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
                .include(t_post -> t_post.user())
                .where(t_post -> {
                    t_post.title().contains(request.getTitle());
                    t_post.user().name().contains(request.getUserName());
                })
                .toPageResult(request.getPageIndex(), request.getPageSize());
    }

这次我们选择返回 post 本体对象,并且不定义 dto 结构返回

==> Preparing: SELECT COUNT(*) FROM `t_post` t

==> Preparing: SELECT t.`id`,t.`title`,t.`content`,t.`user_id`,t.`publish_at` FROM `t_post` t LIMIT 5

==> Preparing: SELECT t.`id`,t.`name`,t.`phone`,t.`create_at` FROM `t_user` t WHERE t.`id` IN (?,?,?,?) ==> Parameters: c529b9ba-a90d-490e-9bad-15ef7c4f33cc(String),8510a91a-274e-494f-9325-f55c004706e5(String),1b59fa07-1824-4e01-a491-c780d167cf44(String),23376c96-a315-4a3f-aeb8-2e29c02f330b(String)

框架通过多次分批返回将整个数据返回(注意数据二次查询没有 N+1问题完全放心使用),且返回的数据是以结构化对象的形式来返回到前端的

返回的响应数据

{
            "id": "0c7fd05f-f999-4fcc-8c98-c0509b22b7f1",
            "title": "健身计划分享",
            "content": "# 这是用户用户D的帖子内容\n包含丰富的文本内容...",

"userId": "c529b9ba-a90d-490e-9bad-15ef7c4f33cc", "publishAt": "2025-08-03T21:24:00.577", "user": { "id": "c529b9ba-a90d-490e-9bad-15ef7c4f33cc", "name": "用户 D", "phone": "18806982998", "createAt": "2025-07-10T13:24:00.576" } }

那么如果我们希望返回的时候只返回 user 的 id 和 name 应该如何实现

  • 第一种返回数据库对象但是只查询id和name
  • 第二种定义dto使用 selectAutoInclude

include部分列

easyEntityQuery.queryable(Post.class)
                .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
                .include(t_post -> t_post.user(),uq->{
                    uq.select(u->u.FETCHER.id().name());
                })
                .where(t_post -> {
                    t_post.title().contains(request.getTitle());
                    t_post.user().name().contains(request.getUserName());
                })
                .toList()
==> Preparing: SELECT COUNT(*) FROM `t_post` t

==> Preparing: SELECT t.`id`,t.`title`,t.`content`,t.`user_id`,t.`publish_at` FROM `t_post` t LIMIT 5

==> Preparing: SELECT t.`id`,t.`name` FROM `t_user` t WHERE t.`id` IN (?,?,?,?) ==> Parameters: c529b9ba-a90d-490e-9bad-15ef7c4f33cc(String),8510a91a-274e-494f-9325-f55c004706e5(String),1b59fa07-1824-4e01-a491-c780d167cf44(String),23376c96-a315-4a3f-aeb8-2e29c02f330b(String)

返回的响应数据

{
            "id": "0c7fd05f-f999-4fcc-8c98-c0509b22b7f1",
            "title": "健身计划分享",
            "content": "# 这是用户用户D的帖子内容\n包含丰富的文本内容...",

"userId": "c529b9ba-a90d-490e-9bad-15ef7c4f33cc", "publishAt": "2025-08-03T21:24:00.577", "user": { "id": "c529b9ba-a90d-490e-9bad-15ef7c4f33cc", "name": "用户 D", "phone": null, "createAt": null } }

include 函数存在多个重载其中第二参数用于描述前一个 include 和对应的额外操作这边设置为只返回 id 和 name

我们看到查询的时候仅查询 id 和 name

这种查询返回的任然是数据库对象所以无法再返回的形状上移除 phonecreateAt ,那么是否有一种办法可以做到形状确定呢

答案是有的时候 dto 来代替数据库对象在使用 selectAutoInclude api

结构化dto

结构化 dto 用来返回 dto 且形状确定适合生成文档和下游数据交互那么可以通过安装插件后进行如下操作

第一步我们使用插件创建结构化 dto

在 dto 的 package 处右键选择 CreateStructDTO

第二步选择要返回的对象

第三步勾选要返回的字段

确定 dto 名称后框架会帮我们直接生成 dto 对象

@Data
public class PostDTO {

@Column(comment = "帖子 id") private String id; @Column(comment = "帖子标题") private String title; @Column(comment = "帖子内容") private String content; @Column(comment = "发布时间") private LocalDateTime publishAt; @Navigate(value = RelationTypeEnum.ManyToOne) private InternalUser user;

@Data public static class InternalUser { @Column(comment = "用户 id") private String id; @Column(comment = "用户姓名") private String name;

}

}

@PostMapping("/selectAutoInclude") public List<PostDTO> selectAutoInclude(@RequestBody PostPage7Request request) { return easyEntityQuery.queryable(Post.class) .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS) .include(t_post -> t_post.user(),uq->{ uq.select(u->u.FETCHER.id().name()); }) .where(t_post -> { t_post.title().contains(request.getTitle()); t_post.user().name().contains(request.getUserName()); }) .selectAutoInclude(PostDTO.class) ① .toList(); }
==> Preparing: SELECT t.`id`,t.`title`,t.`content`,t.`publish_at`,t.`user_id` AS `__relation__userId` FROM `t_post` t

==> Preparing: SELECT t.`id`,t.`name` FROM `t_user` t WHERE t.`id` IN (?,?,?,?,?) ==> Parameters: c529b9ba-a90d-490e-9bad-15ef7c4f33cc(String),8510a91a-274e-494f-9325-f55c004706e5(String),1b59fa07-1824-4e01-a491-c780d167cf44(String),23376c96-a315-4a3f-aeb8-2e29c02f330b(String),947ee5fd-5fd0-4889-94e3-03c5efff2c3a(String)
{
        "id": "0c7fd05f-f999-4fcc-8c98-c0509b22b7f1",
        "title": "健身计划分享",
        "content": "# 这是用户用户D的帖子内容\n包含丰富的文本内容...",

"publishAt": "2025-08-03T21:24:00.577", "user": { "id": "c529b9ba-a90d-490e-9bad-15ef7c4f33cc", "name": "用户 D" } }

框架依然通过 in 来解决 n+1的问题实现结构化的对象返回,框架支持任意结构化对象返回包括结构化对象扁平化

  • selectAutoIncludeselect api和 include 的结合,会自动安装dto的要求将数据结构进行组装返回

说明!!! > 注意千万不要再 selectAutoInclude 中传入数据库对象,因为数据库对象的传入会导致 selectAutoInclude 将整个关系树连根拔起都查询出来 > 注意千万不要再 selectAutoInclude 中传入数据库对象,因为数据库对象的传入会导致 selectAutoInclude 将整个关系树连根拔起都查询出来 > 注意千万不要再 selectAutoInclude 中传入数据库对象,因为数据库对象的传入会导致 selectAutoInclude 将整个关系树连根拔起都查询出来

selectAutoInclude!!! > selectAutoInclude 这个 api 是 eq 的核心数据查询 api 之一用户必须完全掌握可以提高1000%的效率,并且没有 n+1问题支持后续一对一 一对多的任意数据穿透查询

NavigateFlat

返回数据的时候我们如果不希望以结构化对象的形式返回,希望将 user 对象平铺到整个 post 中,又不希望使用 set 手动复制那么可以通过 @NavigateFlat 来实现额外属性的获取

@Data
public class PostPage6Response {
    private String id;
    private String title;
    private String content;
    private String userId;
    private LocalDateTime publishAt;

@NavigateFlat(pathAlias = "user.id") ② private String userName; }

注意我们必须要将①的 link 表示添加上,这样我们在写②的 pathAlias 时插件会自动给出相应的提示,查询是我们将使用 selectAutoInclude 来实现万能查询

@PostMapping("/page6")
    public EasyPageResult<PostPage6Response> page6(@RequestBody PostPage4Request request) {
        return easyEntityQuery.queryable(Post.class)
                .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
                .where(t_post -> {
                    t_post.title().contains(request.getTitle());
                    t_post.user().name().contains(request.getUserName());
                })
                .orderBy(request.getOrders() != null, t_post -> {
                    for (PostPage4Request.InternalOrder order : request.getOrders()) {
                        t_post.anyColumn(order.getProperty()).orderBy(order.isAsc());
                    }
                })
                .selectAutoInclude(PostPage6Response.class)
                .toPageResult(request.getPageIndex(), request.getPageSize());
    }
==> Preparing: SELECT COUNT(*) FROM `t_post` t

==> Preparing: SELECT t.`id`,t.`title`,t.`content`,t.`user_id`,t.`publish_at` FROM `t_post` t ORDER BY t.`title` ASC LIMIT 5

==> Preparing: SELECT `id` FROM `t_user` WHERE `id` IN (?,?,?) ==> Parameters: 8510a91a-274e-494f-9325-f55c004706e5(String),23376c96-a315-4a3f-aeb8-2e29c02f330b(String),c529b9ba-a90d-490e-9bad-15ef7c4f33cc(String)

> 注意千万不要再 selectAutoInclude 中传入数据库对象,因为数据库对象的传入会导致 selectAutoInclude 将整个关系树连根拔起都查询出来 > 注意千万不要再 selectAutoInclude 中传入数据库对象,因为数据库对象的传入会导致 selectAutoInclude 将整个关系树连根拔起都查询出来 > 注意千万不要再 selectAutoInclude 中传入数据库对象,因为数据库对象的传入会导致 selectAutoInclude 将整个关系树连根拔起都查询出来

@NavigateFlat 支持任意级别对象关系获取,如果对象关系获取中间存在 toMany 无论是 OneToMany 还是 ManyToMany 那么最终都会变成 List<?> 集合

帖子内容带评论

简单的额外对象获取后我们希望实现返回给前端帖子内容并且携带上前三条相关评论,那么 eq 有几种方式呢

  • NaviagteFlat + limit + union
  • NaviagteFlat + limit + partition by
  • subquery + limit + joining

评论关系添加

@Data
@Table("t_post")
@EntityProxy
@EasyAlias("t_post")
@EasyAssertMessage("未找到对应的帖子信息")
public class Post implements ProxyEntityAvailable<Post, PostProxy> {

@Navigate(value = RelationTypeEnum.ManyToOne, selfProperty = {PostProxy.Fields.userId}, targetProperty = {UserProxy.Fields.id}, required = true) private User user;

@Navigate(value = RelationTypeEnum.OneToMany, selfProperty = {PostProxy.Fields.id}, targetProperty = {CommentProxy.Fields.postId}) private List<Comment> commentList; }

因为帖子和评论的关系是一对多所以我们在帖子里面通过插件或者手动添加关联关系

limit+union

首先我们定义好需要返回的对象

@Data
public class PostPage8Response {
    private String id;
    private String title;
    private String content;
    private String userId;
    private LocalDateTime publishAt;

@NavigateFlat(pathAlias = "user.id") private String userName;

@Navigate(value = RelationTypeEnum.OneToMany,orderByProps = { @OrderByProperty(property = "createAt",asc = true) },limit = 3) private List<InternalComment> commentList;

@Data public static class InternalComment { private String id; private String parentId; private String content; private LocalDateTime createAt; }

}

这样我们就设置好了要返回的数据并且支持额外返回3条评论

@PostMapping("/postWithCommentPage")
    public List<PostPage8Response> postWithCommentPage(@RequestBody PostPage7Request request) {
        return easyEntityQuery.queryable(Post.class)
                .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
                .where(t_post -> {
                    t_post.title().contains(request.getTitle());
                    t_post.user().name().contains(request.getUserName());
                })
                .selectAutoInclude(PostPage8Response.class).toList();
    }
{
        "id": "0c7fd05f-f999-4fcc-8c98-c0509b22b7f1",
        "title": "健身计划分享",
        "content": "# 这是用户用户D的帖子内容\n包含丰富的文本内容...",

"userId": "c529b9ba-a90d-490e-9bad-15ef7c4f33cc", "publishAt": "2025-08-03T21:24:00.577", "userName": "c529b9ba-a90d-490e-9bad-15ef7c4f33cc", "commentList": [ { "id": "67c9ceb0-3eef-44ba-8bbc-c0d1f15f00ad", "parentId": "0", "content": "期待更多这样的内容", "createAt": "2025-08-05T17:24:00.579" }, { "id": "d7753586-4bb9-448b-bedb-b178df897bca", "parentId": "fa80aaa0-9742-4a02-9209-a08d1bd979df", "content": "@用户 B 我也这么认为", "createAt": "2025-08-06T00:24:00.579" }, { "id": "2b40e873-5c0d-41c4-bf10-a38461017300", "parentId": "67c9ceb0-3eef-44ba-8bbc-c0d1f15f00ad", "content": "@用户 C 具体是指哪方面?", "createAt": "2025-08-06T03:24:00.579" } ] }

我们看到真的和编写的 dto 如出一辙的返回了查询结果

SELECT
        t.`id`,
        t.`title`,
        t.`content`,
        t.`user_id`,
        t.`publish_at`
    FROM
        `t_post` t

SELECT t1.`id`, t1.`parent_id`, t1.`content`, t1.`create_at`, t1.`post_id` AS `__relation__postId` FROM ( (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = 'c529b9ba-a90d-490e-9bad-15ef7c4f33cc' ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = '8510a91a-274e-494f-9325-f55c004706e5' ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = '1b59fa07-1824-4e01-a491-c780d167cf44' ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = '23376c96-a315-4a3f-aeb8-2e29c02f330b' ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = '947ee5fd-5fd0-4889-94e3-03c5efff2c3a' ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = ? ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = ? ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = ? ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = ? ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = ? ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = ? ORDER BY t.`create_at` ASC LIMIT 3) UNION ALL (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id` , t.`create_at` FROM `t_comment` t WHERE t.`post_id` = ? ORDER BY t.`create_at` ASC LIMIT 3) ) t1

SELECT `id` FROM `t_user` WHERE `id` IN (?, ?, ?, ?, ?)

一个生成了三条 sql,中 limit+union 是第二条 sql,但是 union 相对 sql 会变得复杂并且冗余所以我们尝试 eq 提供的第二种方式

limit+partition by

springboot 的 a pplication.yml 增加配置项

easy-query:

  database: mysql

  name-conversion: underlined
  default-track: true
  include-limit-mode: partition
  • include-limit-mode: partition 这句话让原先的 union all 变成 partition (后续 partition 可能会变成默认)

接下来我们继续请求

SELECT
        t.`id`,
        t.`title`,
        t.`content`,
        t.`user_id`,
        t.`publish_at`
    FROM
        `t_post` t

SELECT t2.`id` AS `id`, t2.`parent_id` AS `parent_id`, t2.`content` AS `content`, t2.`create_at` AS `create_at`, t2.`post_id` AS `__relation__postId` FROM (SELECT t1.`id` AS `id`, t1.`parent_id` AS `parent_id`, t1.`content` AS `content`, t1.`user_id` AS `user_id`, t1.`post_id` AS `post_id`, t1.`create_at` AS `create_at` FROM (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id`, t.`create_at`, (ROW_NUMBER() OVER (PARTITION BY t.`post_id` ORDER BY t.`create_at` ASC)) AS `__row__` FROM `t_comment` t WHERE t.`post_id` IN ('09e8395e-b7f7-48b4-8227-fcbf96c35d1e', '5d40f560-af15-4566-93cd-9359e0a27501', '76cdba56-b1f8-4432-bc0e-764d491c6cd5', '81eb5fb7-ec57-45d3-b9b9-5e6217ec4d31', '8a6f16a6-b51e-4a39-9ea9-fda57502bb29', 'a6982186-afc5-4f49-977d-97ff8c25cd9f', 'b1eb997d-9cb0-40ca-9495-a9d41da21125', 'b4f74aeb-3868-4810-9845-cab9e882229b', 'bf7e62ee-d833-4f5a-9a0a-07b9634ba26a', 'c6d0631f-160a-4a8c-8401-62db614f87c8', 'd9629994-d9fa-46a3-bd7c-5982f0900a3d', 'ed01ea8a-4162-42ba-a632-dd6d67bf9d45', 'f27edcf7-0fd8-44e3-b3cc-cbba41427dfe')) t1 WHERE t1.`__row__` >= 1 AND t1.`__row__` <= 3) t2

SELECT `id` FROM `t_user` WHERE `id` IN ('15b6a7c1-3f27-4d21-b67c-9c05cd9bf4b6', '2ae21dfa-9330-4d8c-bbfa-6b4618c56c45', '6e50464d-17a7-4f12-8458-c896d55dd276', '1d2a9d56-63df-4413-bd83-ff9a0c0a2166', '2ede4599-8f1a-4d0c-a0af-0dd50d903b87')
{
        "id": "5d40f560-af15-4566-93cd-9359e0a27501",
        "title": "健身计划分享",
        "content": "# 这是用户用户E的帖子内容\n包含丰富的文本内容...",

"userId": "2ae21dfa-9330-4d8c-bbfa-6b4618c56c45", "publishAt": "2025-08-04T08:09:30.301", "userName": "2ae21dfa-9330-4d8c-bbfa-6b4618c56c45", "commentList": [ { "id": "de5337b2-e13c-49f3-9b15-ac393784fc6f", "parentId": "46da0914-b046-45ad-8847-1c65c82ac71c", "content": "@用户 C 有不同看法:", "createAt": "2025-08-07T06:09:30.304" }, { "id": "daf5102c-b4dd-4f65-bee6-9b3df4f1b5d9", "parentId": "0", "content": "写得真详细", "createAt": "2025-08-07T09:09:30.304" }, { "id": "46da0914-b046-45ad-8847-1c65c82ac71c", "parentId": "0", "content": "期待更多这样的内容", "createAt": "2025-08-07T10:09:30.304" } ] }

我们看到通过简单的配置我们将一对多返回前 n 条变动轻松简单并且可以快速实现支持分页,但是细心的朋友肯定发现了一个问题,我们需要的评论并不是平铺到整个 post 贴子的,帖子和评论虽然是一对多但是评论自己也是自关联,评论设计也是楼中楼为支持的那么我们应该如何设置让我们返回的评论支持返回第一层级呢

EXTRA_AUTO_INCLUDE_CONFIGURE

使用 eq 的
EXTRA_AUTO_INCLUDE_CONFIGURE
可以对 selectAutoInclude 的查询添加额外字段或额外搜索排序等处理

关于
EXTRA_AUTO_INCLUDE_CONFIGURE
的更多信息请 查看文档

第一步对原始的 dto 对象进行插件快速提示插入
EXTRA_AUTO_INCLUDE_CONFIGURE

我们移除 select 操作因为我们不需要

最终我们的返回 dto 如下

@Data
public class PostPage9Response {
    private String id;
    private String title;
    private String content;
    private String userId;
    private LocalDateTime publishAt;

@NavigateFlat(pathAlias = "user.id") private String userName;

@Navigate(value = RelationTypeEnum.OneToMany,orderByProps = { @OrderByProperty(property = "createAt",asc = true) },limit = 3) private List<InternalComment> commentList;

@Data public static class InternalComment {

private static final ExtraAutoIncludeConfigure EXTRA_AUTO_INCLUDE_CONFIGURE = CommentProxy.TABLE.EXTRA_AUTO_INCLUDE_CONFIGURE() .where(t_comment -> { t_comment.parentId().eq("0"); });

private String id; private String parentId; private String content; private LocalDateTime createAt; }

}

我们看中间 sql 如下

SELECT
        t2.`id` AS `id`,
        t2.`parent_id` AS `parent_id`,
        t2.`content` AS `content`,
        t2.`create_at` AS `create_at`,
        t2.`post_id` AS `__relation__postId`
    FROM
        (SELECT
            t1.`id` AS `id`,
            t1.`parent_id` AS `parent_id`,
            t1.`content` AS `content`,
            t1.`user_id` AS `user_id`,
            t1.`post_id` AS `post_id`,
            t1.`create_at` AS `create_at`
        FROM
            (SELECT
                t.`id`,
                t.`parent_id`,
                t.`content`,
                t.`user_id`,
                t.`post_id`,
                t.`create_at`,
                (ROW_NUMBER() OVER (PARTITION
            BY
                t.`post_id`
            ORDER BY
                t.`create_at` ASC)) AS `__row__`
            FROM
                `t_comment` t
            WHERE
                t.`parent_id` = '0'  ①
                AND t.`post_id` IN ('09e8395e-b7f7-48b4-8227-fcbf96c35d1e', '5d40f560-af15-4566-93cd-9359e0a27501', '76cdba56-b1f8-4432-bc0e-764d491c6cd5', '81eb5fb7-ec57-45d3-b9b9-5e6217ec4d31', '8a6f16a6-b51e-4a39-9ea9-fda57502bb29', 'a6982186-afc5-4f49-977d-97ff8c25cd9f', 'b1eb997d-9cb0-40ca-9495-a9d41da21125', 'b4f74aeb-3868-4810-9845-cab9e882229b', 'bf7e62ee-d833-4f5a-9a0a-07b9634ba26a', 'c6d0631f-160a-4a8c-8401-62db614f87c8', 'd9629994-d9fa-46a3-bd7c-5982f0900a3d', 'ed01ea8a-4162-42ba-a632-dd6d67bf9d45', 'f27edcf7-0fd8-44e3-b3cc-cbba41427dfe')) t1
        WHERE
            t1.`__row__` >= 1
            AND t1.`__row__` <= 3) t2
  • ①是我们通过额外配置添加上去的

返回的 json 如下

{
        "id": "b4f74aeb-3868-4810-9845-cab9e882229b",
        "title": "初探人工智能",
        "content": "# 这是用户用户E的帖子内容\n包含丰富的文本内容...",

"userId": "2ae21dfa-9330-4d8c-bbfa-6b4618c56c45", "publishAt": "2025-08-07T02:09:30.301", "userName": "2ae21dfa-9330-4d8c-bbfa-6b4618c56c45", "commentList": [ { "id": "238fea11-c5d1-4485-977d-a0126cb74402", "parentId": "0", "content": "期待更多这样的内容", "createAt": "2025-08-07T09:09:30.304" }, { "id": "e216eaf8-bf15-4eeb-aa4c-6489be83c355", "parentId": "0", "content": "内容很实用", "createAt": "2025-08-07T16:09:30.304" }, { "id": "830bd1d9-1600-43a2-94b7-f6426a8a78c9", "parentId": "0", "content": "写得真详细", "createAt": "2025-08-07T17:09:30.304" } ] }

我们返回的 post 节点完美的符合我们内容

但是有时候我们可能需要返回的是 post 信息和前三条内容并且将前三条内容合并到一个字段上去那么应该怎么做

joining逗号分割

一如既往我们还是定义对应的 dto

@Data
@EntityProxy
public class PostPage10Response {
    private String id;
    private String title;
    private String content;
    private String userId;
    private LocalDateTime publishAt;

private String userName; private String commentContent;

}

@PostMapping("/postWithCommentPage3") public List<PostPage10Response> postWithCommentPage3(@RequestBody PostPage7Request request) { return easyEntityQuery.queryable(Post.class) .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS) .where(t_post -> { t_post.title().contains(request.getTitle()); t_post.user().name().contains(request.getUserName()); }) .select(t_post -> new PostPage10ResponseProxy() .selectAll(t_post) .userName().set(t_post.user().name()) .commentContent().set(t_post.commentList().where(c->c.parentId().eq("0")).elements(0,2).joining(c->c.content())) ).toList(); }

我们来看下表达式 t_post.commentList().where(c->c.parentId().eq("0")).joining(c->c.content()) 这个表达式是将 post 下方的评论集合 commentList 通过 where 筛选取前三个的 content 内容合并

{
        "id": "09e8395e-b7f7-48b4-8227-fcbf96c35d1e",
        "title": "夏日旅行攻略",
        "content": "# 这是用户用户D的帖子内容\n包含丰富的文本内容...",

"userId": "15b6a7c1-3f27-4d21-b67c-9c05cd9bf4b6", "publishAt": "2025-08-05T03:09:30.301", "userName": "用户 D", "commentContent": "非常好的分享!,期待更多这样的内容,非常好的分享!" }, { "id": "5d40f560-af15-4566-93cd-9359e0a27501", "title": "健身计划分享", "content": "# 这是用户用户 E 的帖子内容\n 包含丰富的文本内容...",

"userId": "2ae21dfa-9330-4d8c-bbfa-6b4618c56c45", "publishAt": "2025-08-04T08:09:30.301", "userName": "用户 E", "commentContent": "完全同意你的观点,期待更多这样的内容,写得真详细" }

通过结果我们可以清晰地看到 commentContentjoining 函数通过逗号分割组合在一起了 我们再来看对应的 sql

SELECT
        t.`id`,
        t.`title`,
        t.`content`,
        t.`user_id`,
        t.`publish_at`,
        t1.`name` AS `user_name`,
        (SELECT
            GROUP_CONCAT(t2.`content` SEPARATOR ',')
        FROM
            `t_comment` t2
        WHERE
            t2.`post_id` = t.`id`
            AND t2.`parent_id` = '0'
        LIMIT
            3) AS `comment_content`
    FROM
        `t_post` t
    INNER JOIN
        `t_user` t1
            ON t1.`id` = t.`user_id`

框架通过 select 子查询 将结果清晰的将结果集通过 group_concat 函数组装到了 comment_content 列上

::: warning 性能!!!

如果由用户嫌弃select子查询性能低下eq贴心的提供了子查询转 groupJoin 助力用户实现更高效的sql

:::

当然这边为了演示使用了内容逗号分割,其实本质而言应该是将类目逗号分割更加合适

接下来我们创建帖子的类目关系表

帖子和类目关系是多对多通过 CategoryPost 表进行关联

@Data
@Table("t_post")
@EntityProxy
@EasyAlias("t_post")
@EasyAssertMessage("未找到对应的帖子信息")
public class Post implements ProxyEntityAvailable<Post, PostProxy> {

@Navigate(value = RelationTypeEnum.ManyToMany, selfProperty = {PostProxy.Fields.id}, selfMappingProperty = {CategoryPostProxy.Fields.postId}, mappingClass = CategoryPost.class, targetProperty = {CategoryProxy.Fields.id}, targetMappingProperty = {CategoryPostProxy.Fields.categoryId}, subQueryToGroupJoin = true) ① private List<Category> categoryList; }
  • 其中我们看到① subQueryToGroupJoin = true 该配置项让原本的多对多子查询可以直接在使用的时候使用 groupJoin 来代替可以让生成的sql性能更加高效

返回帖子内容+用户+评论前三个+所属类目逗号分割

设置返回 dto

@Data
public class PostPage11Response {

private static final ExtraAutoIncludeConfigure EXTRA_AUTO_INCLUDE_CONFIGURE = PostProxy.TABLE.EXTRA_AUTO_INCLUDE_CONFIGURE() .select(t_post -> Select.of( t_post.categoryList().joining(cate->cate.name()).as("categoryNames") )); private String id; private String title; private String content; private String userId; private LocalDateTime publishAt;

@NavigateFlat(pathAlias = "user.id") private String userName;

@SuppressWarnings("EasyQueryFieldMissMatch") private String categoryNames;

@Navigate(value = RelationTypeEnum.OneToMany, orderByProps = { @OrderByProperty(property = "createAt", asc = true) }, limit = 3) private List<InternalComment> commentList;

@Data public static class InternalComment {

private static final ExtraAutoIncludeConfigure EXTRA_AUTO_INCLUDE_CONFIGURE = CommentProxy.TABLE.EXTRA_AUTO_INCLUDE_CONFIGURE() .where(t_comment -> { t_comment.parentId().eq("0"); });

private String id; private String parentId; private String content; private LocalDateTime createAt; }

}

@PostMapping("/postList4") public List<PostPage11Response> postList4(@RequestBody PostPage7Request request) { return easyEntityQuery.queryable(Post.class) .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS) .where(t_post -> { t_post.title().contains(request.getTitle()); t_post.user().name().contains(request.getUserName()); }) .selectAutoInclude(PostPage11Response.class).toList(); }

我们通过对主表进行额外字段的添加让其直接支持额外字段返回

  • @SuppressWarnings("EasyQueryFieldMissMatch") 这个注解主要是用来抑制插件警告,您如果觉得警告无所谓那么可以不加该注解对结果没有影响
{
        "id": "0c6ab3ab-29a4-4320-a08e-195bdac27095",
        "title": "JVM调优实战",
        "content": "# 这是用户用户C的帖子内容\n包含丰富的文本内容...",

"userId": "2e509ef4-0282-448f-ace0-43501d46ccf4", "publishAt": "2025-08-04T23:42:43.525", "userName": "2e509ef4-0282-448f-ace0-43501d46ccf4", "categoryNames": "娱乐,教育", "commentList": [ { "id": "2d3643e6-8fb5-4a2b-a0bc-1c92030bfa34", "parentId": "0", "content": "完全同意你的观点", "createAt": "2025-08-07T00:42:43.526" }, { "id": "5f7b2333-5578-40cd-940e-28e97d1b0aa1", "parentId": "0", "content": "完全同意你的观点", "createAt": "2025-08-07T11:42:43.526" }, { "id": "0b1d0cbd-62a7-4922-b5fe-0ef4780e4c24", "parentId": "0", "content": "内容很实用", "createAt": "2025-08-07T15:42:43.526" } ] }, { "id": "1a0e5854-c748-4c6b-a11d-d5bbb58326a1", "title": "电影推荐合集", "content": "# 这是用户用户 B 的帖子内容\n 包含丰富的文本内容...",

"userId": "70ec5f9f-7e9b-4f57-b2a4-9a35a163bd3e", "publishAt": "2025-08-03T02:42:43.525", "userName": "70ec5f9f-7e9b-4f57-b2a4-9a35a163bd3e", "categoryNames": "教育,科技", "commentList": [ { "id": "723a588c-0d95-4db7-be6b-1745bfcfc540", "parentId": "0", "content": "内容很实用", "createAt": "2025-08-07T00:42:43.526" }, { "id": "116ab46b-9b61-4644-ac10-73e65f5a01b9", "parentId": "0", "content": "内容很实用", "createAt": "2025-08-07T18:42:43.526" }, { "id": "65cb0f86-7076-46a6-b333-c9c50e9336ae", "parentId": "0", "content": "写得真详细", "createAt": "2025-08-07T18:42:43.526" } ] }

完全完美符合我们需要的结果

SELECT
        t5.`__joining2__` AS `category_names`,
        t.`id`,
        t.`title`,
        t.`content`,
        t.`user_id`,
        t.`publish_at`
    FROM
        `t_post` t
    LEFT JOIN
        (SELECT
            t3.`post_id` AS `post_id`, GROUP_CONCAT(t2.`name` SEPARATOR ',') AS `__joining2__` FROM `t_category` t2
        INNER JOIN
            `t_category_post` t3
                ON t2.`id` = t3.`category_id`
        GROUP BY
            t3.`post_id`) t5
            ON t5.`post_id` = t.`id`

SELECT t2.`id` AS `id`, t2.`parent_id` AS `parent_id`, t2.`content` AS `content`, t2.`create_at` AS `create_at`, t2.`post_id` AS `__relation__postId` FROM (SELECT t1.`id` AS `id`, t1.`parent_id` AS `parent_id`, t1.`content` AS `content`, t1.`user_id` AS `user_id`, t1.`post_id` AS `post_id`, t1.`create_at` AS `create_at` FROM (SELECT t.`id`, t.`parent_id`, t.`content`, t.`user_id`, t.`post_id`, t.`create_at`, (ROW_NUMBER() OVER (PARTITION BY t.`post_id` ORDER BY t.`create_at` ASC)) AS `__row__` FROM `t_comment` t WHERE t.`parent_id` = '0' AND t.`post_id` IN ('015c8538-0eaa-4afb-a1c7-4cca00dd6638', '0c6ab3ab-29a4-4320-a08e-195bdac27095', '1a0e5854-c748-4c6b-a11d-d5bbb58326a1', '31a955ba-04ec-4d07-a6d4-fac6c408ab7d', '36eba6b0-5dd4-41b3-a4af-d9c522a86b3a', '573ca56a-4575-458e-8258-7b76c2cfe959', '5f72b5bf-3ae6-4bd6-9df9-cf0c43abc37c', '63d5b82f-64e6-4985-ad4b-acf71d8368fc', '669ce2a5-abaf-49e8-bb7e-e498f7377b15', '73f5d341-c6df-43a1-afcd-e246c4d1fcc9', '89bf6652-0ae0-451a-8a16-d9b543898f81', '8dbcfcfe-44a7-45c2-9db9-d0302c5a9a94')) t1 WHERE t1.`__row__` >= 1 AND t1.`__row__` <= 3) t2

SELECT `id` FROM `t_user` WHERE `id` IN ('3b63ddd9-b038-4c24-969e-8b478fe862a5', '2e509ef4-0282-448f-ace0-43501d46ccf4', '70ec5f9f-7e9b-4f57-b2a4-9a35a163bd3e', 'f2bf383e-ee8d-44c5-968d-263191ab058e', 'eda79345-6fbf-4ca6-b9bf-4743a3f991e4')
  • 第一条sql我们看到用来查询返回post信息和对应的categoryNames字段使用 groupJoin 来代替多对多自查
  • 第二条sql我们看到框架使用 patrtition by 让用户可以轻松的返回评论信息前n条
  • 第三条sql我们使用 NaviagteFlat 二次查询杜绝n+1来返回用户信息

到此为止我们的帖子相关的查询已经结束 主要我们实现了框架对一对多 多对一和多对多下如何快速查询并且支持众多开窗函数的隐式使用

最后的最后我非常感谢您能看到这边我相信eq绝对是你不二的orm选择

框架地址 github.com/dromara/eas… 文档地址
www.easy-query.com/easy-query-… 该文章 demo 地址 github.com/xuejmnet/eq…

相关文章

中信泰富&amp;明源云——搭建营销费用管理系统,实现3个在线化

3月23日,中信泰富地产与明源云合作的营销费用管理系统业务解决方案汇报在中信泰富大厦顺利结束。 (会议现场)这是自双方合作以来,继今年1月底项目启动会后的又一重要推进动作,本次汇报得到了双方领导的高度...

Excel VBA 主界面/一步一步带你设计【收费管理系统】02

本文于2023年5月14日首发于本人同名公众号,更多文章案例请关注微信公众号:Excel活学活用!☆本期内容概要☆用户窗体设置:主界面、主页设计上期我们分享了EXCEL读取ACCESS数据库表,实现系...

10个实例小练习,快速入门熟练 Vue3 核心新特性(一)

作者:xuying 全栈修炼转发链接:https://mp.weixin.qq.com/s/_n2seDbbiO5hXQfuUGbUCQ前言Vue3.0 发 beta 版都有一段时间了,正式版也不远了...

程序员开发必会之git常用命令,git配置、拉取、提交、分支管理

整理日常开发过程中经常使用的git命令!git配置SSH刚进入项目开发中,我们首先需要配置git的config、配置SSH方式拉取代码,以后就免输入账号密码了!# 按顺序执行 git config -...

我常在使用的几个 VIM 插件(我常在使用的几个 vim 插件)

今天给你分享几个我觉得还不错的 VIM 插件,也许能给你带来一点「惊喜感」。vim主题插件 你完全可以让你的编辑器按照你喜欢的样子呈现,在 vimcolors 这个网站中,汇集了很多的主题,你可以进去...

web前端入门:css+html5+javascript同时学可以吗?

HTML+CSS+JS是前端基础核心知识,肯定是都要学的。在web前端中,HTML是结构标准,CSS是样式标准,JS是行为标准,三者缺一不可。说的直白点,HTML就是骨架,CSS就是皮肤,JS就是灵魂...