作为一名长期在.NET和Java双栖开发的程序员,我深知ORM框架在项目中的重要性。最近公司因业务需要将部分C#项目迁移到Java技术栈,最让我头疼的就是如何找到一个能媲美EF Core、SqlSugar或FreeSql的Java ORM工具。经过多方调研和实际项目验证,Easy-Query成为了我的首选解决方案。
Easy-Query不仅完美复现了C#主流ORM的核心功能,更在查询表达力和性能优化上有所突破。特别是其独创的隐式Group(GroupJoin)功能,能自动合并相同子查询,这在复杂业务场景下能显著提升查询效率。下面我将通过银行账户系统的实际案例,带你全面了解这个强大的Java ORM框架。
首先需要在pom.xml中添加Easy-Query的核心依赖:
xml复制<dependency>
<groupId>com.easy-query</groupId>
<artifactId>easy-query-core</artifactId>
<version>最新版本</version>
</dependency>
对于Spring Boot项目,建议使用starter包简化配置:
xml复制<dependency>
<groupId>com.easy-query</groupId>
<artifactId>easy-query-spring-boot-starter</artifactId>
<version>最新版本</version>
</dependency>
Easy-Query支持JPA注解,但更推荐使用其特有的@EntityProxy注解实现动态代理。以下是一个银行系统的基础实体定义:
java复制@EntityProxy
@Data
public class SysUser {
private String id;
private String name;
// 一对多关联银行卡
private List<BankCard> bankCards;
}
@EntityProxy
@Data
public class BankCard {
private String id;
private String uid; // 用户ID
private String code; // 卡号
private String type; // 卡类型:储蓄卡/信用卡
private LocalDateTime openTime; // 开户时间
private String bankId; // 所属银行ID
// 多对一关联银行
private Bank bank;
}
提示:@EntityProxy注解会为实体类生成动态代理,这是实现Lambda表达式查询的关键。实际开发中建议开启Lombok的@Builder注解以便链式调用。
查询用户信息及其最早开户的银行卡信息,这是典型的1:N关联查询场景:
java复制List<UserDTO> list = easyEntityQuery.queryable(SysUser.class)
.select(user -> {
// 获取最早开户的银行卡
BankCard firstCard = user.bankCards()
.orderBy(bankCard -> bankCard.openTime().asc())
.first();
return new UserDTO(
user.id(),
user.name(),
firstCard.code(),
firstCard.type(),
firstCard.openTime(),
firstCard.bank().name()
);
}).toList();
生成的SQL使用了窗口函数确保获取每个用户的第一张银行卡:
sql复制SELECT t.`id`, t.`name`, t3.`code`, t3.`type`, t3.`open_time`, t4.`name`
FROM `t_sys_user` t
LEFT JOIN (
SELECT t1.*, ROW_NUMBER() OVER (PARTITION BY t1.`uid` ORDER BY t1.`open_time` ASC) AS `__row__`
FROM `t_bank_card` t1
) t3 ON t3.`uid` = t.`id` AND t3.`__row__` = 1
INNER JOIN `t_bank` t4 ON t4.`id` = t3.`bank_id`
查询至少有5张储蓄卡且没有信用卡的用户信息及其第4张储蓄卡信息:
java复制@Data
@EntityProxy
public class UserDTO2 {
private String id;
private String name;
private String thirdCardType;
private String thirdCardCode;
private String thirdCardBankName;
}
List<UserDTO2> result = easyEntityQuery.queryable(SysUser.class)
.where(user -> {
// 至少有5张储蓄卡
user.bankCards().where(c -> c.type().eq("储蓄卡")).count().gt(4L);
// 没有信用卡
user.bankCards().where(c -> c.type().eq("信用卡")).none();
})
.select(user -> {
BankCard thirdCard = user.bankCards()
.where(c -> c.type().eq("储蓄卡"))
.orderBy(bankCard -> bankCard.openTime().asc())
.element(3); // 获取第4张卡(索引从0开始)
return new UserDTO2Proxy()
.id().set(user.id())
.name().set(user.name())
.thirdCardType().set(thirdCard.type())
.thirdCardCode().set(thirdCard.code())
.thirdCardBankName().set(thirdCard.bank().name());
}).toList();
生成的SQL会包含两个子查询:
sql复制SELECT t.*, t5.*, t6.`name`
FROM `t_sys_user` t
LEFT JOIN (
SELECT t3.*, ROW_NUMBER() OVER (PARTITION BY t3.`uid` ORDER BY t3.`open_time` ASC) AS `__row__`
FROM `t_bank_card` t3
WHERE t3.`type` = '储蓄卡'
) t5 ON t5.`uid` = t.`id` AND t5.`__row__` = 4
INNER JOIN `t_bank` t6 ON t6.`id` = t5.`bank_id`
WHERE (
SELECT COUNT(*) FROM `t_bank_card` t1
WHERE t1.`uid` = t.`id` AND t1.`type` = '储蓄卡'
) > 4
AND NOT EXISTS (
SELECT 1 FROM `t_bank_card` t2
WHERE t2.`uid` = t.`id` AND t2.`type` = '信用卡'
LIMIT 1
)
上述复杂查询存在性能问题:相同表被多次扫描(t_bank_card)。Easy-Query的隐式Group(GroupJoin)技术可以自动合并相同子查询:
java复制List<UserDTO2> result = easyEntityQuery.queryable(SysUser.class)
.configure(s -> s.getBehavior().add(EasyBehaviorEnum.ALL_SUB_QUERY_GROUP_JOIN))
.where(user -> {
user.bankCards().where(c -> c.type().eq("储蓄卡")).count().gt(4L);
user.bankCards().where(c -> c.type().eq("信用卡")).none();
})
.select(user -> {
BankCard thirdCard = user.bankCards()
.where(c -> c.type().eq("储蓄卡"))
.orderBy(bankCard -> bankCard.openTime().asc())
.element(3);
return new UserDTO2Proxy()
.id().set(user.id())
.name().set(user.name())
.thirdCardType().set(thirdCard.type())
.thirdCardCode().set(thirdCard.code())
.thirdCardBankName().set(thirdCard.bank().name());
}).toList();
优化后的SQL使用CASE WHEN和GROUP BY合并查询:
sql复制SELECT t.*, t5.*, t6.`name`
FROM `t_sys_user` t
LEFT JOIN (
SELECT t1.`uid`,
COUNT(CASE WHEN t1.`type` = '储蓄卡' THEN 1 ELSE NULL END) AS `__count2__`,
COUNT(CASE WHEN t1.`type` = '信用卡' THEN 1 ELSE NULL END) <= 0 AS `__none3__`
FROM `t_bank_card` t1
GROUP BY t1.`uid`
) t2 ON t2.`uid` = t.`id`
LEFT JOIN (
SELECT t3.*, ROW_NUMBER() OVER (PARTITION BY t3.`uid` ORDER BY t3.`open_time` ASC) AS `__row__`
FROM `t_bank_card` t3
WHERE t3.`type` = '储蓄卡'
) t5 ON t5.`uid` = t.`id` AND t5.`__row__` = 4
INNER JOIN `t_bank` t6 ON t6.`id` = t5.`bank_id`
WHERE IFNULL(t2.`__count2__`, 0) > 4
AND IFNULL(t2.`__none3__`, true) = true
在10万用户数据的测试环境中:
性能提升主要来自:
Easy-Query支持灵活的动态条件组合:
java复制public List<SysUser> searchUsers(String name, String cardType, LocalDate startDate) {
return easyEntityQuery.queryable(SysUser.class)
.where(user -> {
if (StringUtils.isNotBlank(name)) {
user.name().like(name + "%");
}
if (StringUtils.isNotBlank(cardType)) {
user.bankCards().where(card -> card.type().eq(cardType)).any();
}
if (startDate != null) {
user.bankCards()
.where(card -> card.openTime().ge(startDate.atStartOfDay()))
.any();
}
}).toList();
}
对于大数据量分页,推荐使用游标分页而非传统LIMIT:
java复制// 传统分页(适合小数据量)
Page<SysUser> page1 = easyEntityQuery.queryable(SysUser.class)
.where(o -> o.bankCards().count().gt(0))
.toPage(1, 20);
// 游标分页(大数据量推荐)
CursorPage<SysUser> page2 = easyEntityQuery.queryable(SysUser.class)
.orderBy(o -> o.createTime().desc())
.toCursorPage(20, "lastId");
Easy-Query提供了高效的批量操作API:
java复制// 批量插入
easyEntityQuery.insertable(BankCard.class)
.batch(list)
.executeRows();
// 批量更新
easyEntityQuery.updatable(BankCard.class)
.batch(list)
.setColumns(card -> card.type().as("新卡类型"))
.executeRows();
// 批量删除
easyEntityQuery.deletable(BankCard.class)
.where(o -> o.id().in(cardIds))
.executeRows();
虽然Easy-Query默认会优化关联查询,但某些场景仍需注意:
java复制// 错误示例:会导致N+1查询
List<SysUser> users = easyEntityQuery.queryable(SysUser.class).toList();
users.forEach(user -> {
System.out.println(user.bankCards().size()); // 每次调用都会触发查询
});
// 正确做法:预先加载关联数据
List<SysUser> users = easyEntityQuery.queryable(SysUser.class)
.include(o -> o.bankCards()) // 显式声明需要加载的关联
.toList();
在Spring环境中,Easy-Query与@Transactional注解无缝集成:
java复制@Service
public class UserService {
@Transactional
public void transfer(String fromCard, String toCard, BigDecimal amount) {
// 扣款
easyEntityQuery.updatable(BankCard.class)
.where(o -> o.code().eq(fromCard))
.set(o -> o.balance().subtract(amount))
.executeRows();
// 存款
easyEntityQuery.updatable(BankCard.class)
.where(o -> o.code().eq(toCard))
.set(o -> o.balance().add(amount))
.executeRows();
}
}
对于特别复杂的查询,可以开启SQL日志:
yaml复制# application.yml
easy-query:
log:
sql: true
parameters: true
或者临时获取SQL字符串进行调试:
java复制String sql = easyEntityQuery.queryable(SysUser.class)
.where(o -> o.bankCards().count().gt(5))
.toSQL();
System.out.println(sql);
从C# ORM迁移到Easy-Query时,我有几点实用建议:
Lambda表达式差异:
=>,Java中使用->user.name())导航属性处理:
分页差异:
事务管理:
在实际项目迁移过程中,我总结出一个有效的工作流程:
Easy-Query最让我惊喜的是它对复杂查询的处理能力。在最近的一个金融项目中,我们有个涉及7个表关联的统计报表查询,从EF Core迁移到Easy-Query后,通过合理使用隐式Group和索引提示,查询时间从原来的8秒优化到了1.2秒。这充分证明了它在OLAP场景下的强大实力。