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

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

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

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

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

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

建模

实体关系如下:

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

entity-relation

点击查看实体代码
@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 -> {
//                    if(EasyStringUtil.isNotBlank(request.getTitle())){
//                        t_post.title().contains(request.getTitle());
//                    }
                    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;//这个是查询Post内的属性字段
         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());
            })
            //这个request.getOrders()!=null为true才会执行后续的方法也可以使用if包裹
            //当然如果你能确保request.getOrders()肯定不等于null的那么不需要加这个判断
            .orderBy(request.getOrders()!=null,t_post -> {
                for (PostPage3Request.InternalOrder order : request.getOrders()) {
                    //anyColumn表示需要排序的字段,orderBy表示使用正序还是倒序
                    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界面

navigate2

  • 第二步设置关联关系

navigate-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

/**
 * create time 2025/8/6 22:45
 * {@link com.eq.doc.domain.Post} ①
 *
 * @author xuejiaming
 */
@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)//查询post的全字段
                        .selectIgnores(t_post.title())//排除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来代替数据库对象在使用selectAutoIncludeapi

结构化dto

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

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

在dto的package处右键选择CreateStructDTO

csdto1

第二步选择要返回的对象

csdto2

第三步勾选要返回的字段

csdto3

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


/**
 * this file automatically generated by easy-query struct dto mapping
 * 当前文件是easy-query自动生成的 结构化dto 映射
 * {@link com.eq.doc.domain.Post }
 *
 * @author xuejiaming
 * @easy-query-dto schema: normal
 */
@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;


    /**
     * {@link com.eq.doc.domain.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的问题实现结构化的对象返回,框架支持任意结构化对象返回包括结构化对象扁平化

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

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

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

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


/**
 * create time 2025/8/6 22:45
 * {@link com.eq.doc.domain.Post} ①
 *
 * @author xuejiaming
 */
@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

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


/**
 * create time 2025/8/6 22:45
 * {@link com.eq.doc.domain.Post}
 *
 * @author xuejiaming
 */
@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;

    /**
     * {@link Comment}
     **/
    @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如出一辙的返回了查询结果

-- 第1条sql数据
 SELECT
        t.`id`,
        t.`title`,
        t.`content`,
        t.`user_id`,
        t.`publish_at` 
    FROM
        `t_post` t

-- 第2条sql数据
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
-- 第3条sql数据
   SELECT
        `id` 
    FROM
        `t_user` 
    WHERE
        `id` IN (?, ?, ?, ?, ?)

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

limit+partition by

springboot的application.yml增加配置项

easy-query:
  #支持的数据库
  database: mysql
  #对象属性和数据库列名的转换器
  name-conversion: underlined
  default-track: true
  include-limit-mode: partition
  • include-limit-mode: partition这句话让原先的union all变成partition(后续partition可能会变成默认)

接下来我们继续请求


-- 第1条sql数据

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


-- 第2条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.`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



-- 第3条sql数据

    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

extra-include-tip

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

最终我们的返回dto如下


/**
 * create time 2025/8/6 22:45
 * {@link com.eq.doc.domain.Post}
 *
 * @author xuejiaming
 */
@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;

    /**
     * {@link Comment}
     **/
    @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


/**
 * create time 2025/8/6 22:45
 * {@link com.eq.doc.domain.Post}
 *
 * @author xuejiaming
 */
@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;

    /**
     * {@link Comment}
     **/
    @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"
            }
        ]
    }

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


-- 第1条sql数据

    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`
-- 第2条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 ('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
-- 第3条sql数据

    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选择

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

posted @ 2025-08-08 08:12  薛家明  阅读(1704)  评论(29)    收藏  举报