easy-query: A Modern, Type-Safe ORM for Java That Actually Makes Sense
GitHub: https://github.com/dromara/easy-query
Documentation: https://www.easy-query.com/easy-query-doc/en/
Why Another Java ORM?
Let me show you a problem first. Here’s how you query data with traditional Java ORMs:
// JPA Criteria API - Runtime strings, no type safety
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> user = cq.from(User.class);
cq.where(cb.and(
cb.equal(user.get("name"), "John"), // String reference!
cb.greaterThan(user.get("age"), 18) // Typo? Find out at runtime!
));
What if I told you it could look like this instead?
// easy-query - Compile-time safety, fluent API
List<User> users = easyEntityQuery.queryable(User.class)
.where(user -> {
user.name().eq("John"); // Full IntelliSense support
user.age().gt(18); // Typo? Compiler catches it!
})
.toList();
That’s what easy-query brings to Java – a modern, type-safe ORM that feels natural to write and is safe to refactor.
Core Philosophy
easy-query is built on three principles:
- Type Safety First – If it compiles, it works. No runtime surprises.
- Developer Experience – IntelliSense everywhere, readable code, easy refactoring.
- Performance – Smart query optimization, N+1 prevention, efficient SQL generation.
Key Features
1. 🔒 Compile-Time Type Safety
No more string-based field references:
// ❌ Old way - Runtime errors waiting to happen
query.where("userNam = ?", name); // Typo in field name!
// ✅ easy-query - Compiler catches typos
List<User> users = easyEntityQuery.queryable(User.class)
.where(user -> user.userName().eq(name)) // IDE autocomplete!
.toList();
2. 🎯 Fluent Query API
Write queries that read like natural language:
// Complex query that's still readable
List<Order> orders = easyEntityQuery.queryable(Order.class)
.where(order -> {
order.status().eq("ACTIVE");
order.amount().between(100, 1000);
order.createTime().ge(LocalDateTime.now().minusDays(30));
})
.orderBy(order -> order.createTime().desc())
.limit(10)
.toList();
3. 🔗 Smart Navigation Properties
Load related data without N+1 queries:
// Load user with roles and company in efficient batched queries
List<User> users = easyEntityQuery.queryable(User.class)
.include(user -> user.roles()) // Batched query #1
.include(user -> user.company()) // Batched query #2
.toList();
// Executed SQL:
// Query 1: SELECT * FROM user
// Query 2: SELECT * FROM user_role WHERE user_id IN (1,2,3,...)
// Query 3: SELECT * FROM role WHERE id IN (10,11,12,...)
// Query 4: SELECT * FROM company WHERE id IN (100,101,...)
No cartesian products, no manual optimization needed!
4. 📊 Type-Safe Projections
Map to DTOs with full type safety:
@Data
public class UserSummaryDTO {
private String userId;
private String fullName;
private String companyName;
private int roleCount;
}
// Type-safe mapping
List<UserSummaryDTO> summaries = easyEntityQuery.queryable(User.class)
.include(user -> user.roles())
.include(user -> user.company())
.select(user -> new UserSummaryDTOProxy()
.userId().set(user.id())
.fullName().set(user.firstName().concat(" ").concat(user.lastName()))
.companyName().set(user.company().name())
.roleCount().set(user.roles().count())
)
.toList();
5. 📈 Powerful GROUP BY
Aggregation queries with strong typing:
// Monthly sales statistics
List<SalesStatDTO> stats = easyEntityQuery.queryable(Order.class)
.where(order -> order.status().eq("COMPLETED"))
.groupBy(order -> GroupKeys.of(
order.userId(),
order.createTime().format("yyyy-MM")
))
.select(SalesStatDTO.class, group -> Select.of(
group.key1().as(SalesStatDTO::getUserId),
group.key2().as(SalesStatDTO::getMonth),
group.count().as(SalesStatDTO::getOrderCount),
group.sum(s -> s.amount()).as(SalesStatDTO::getTotalAmount),
group.avg(s -> s.amount()).as(SalesStatDTO::getAvgAmount),
group.max(s -> s.amount()).as(SalesStatDTO::getMaxAmount)
))
.having(group -> group.count().gt(5L))
.orderBy(SalesStatDTO::getTotalAmount, false)
.toList();
// Generated SQL:
// SELECT
// user_id,
// DATE_FORMAT(create_time, '%Y-%m') as month,
// COUNT(*) as order_count,
// SUM(amount) as total_amount,
// AVG(amount) as avg_amount,
// MAX(amount) as max_amount
// FROM t_order
// WHERE status = 'COMPLETED'
// GROUP BY user_id, DATE_FORMAT(create_time, '%Y-%m')
// HAVING COUNT(*) > 5
// ORDER BY total_amount DESC
6. 🔄 Change Tracking
Track entity modifications automatically:
try (TrackContext track = easyQueryClient.startTrack()) {
User user = easyEntityQuery.queryable(User.class)
.whereById("1")
.firstOrNull();
user.setName("John Doe");
user.setEmail("john@example.com");
track.saveChanges(); // Only updates changed fields!
}
// Generated SQL:
// UPDATE t_user SET name = ?, email = ? WHERE id = ?
// Only the modified fields are in the UPDATE statement
7. ⚡ Bulk Operations
Efficient batch updates and deletes:
// Bulk update
long updated = easyEntityQuery.updatable(User.class)
.set(user -> {
user.status().set("INACTIVE");
user.updateTime().set(LocalDateTime.now());
})
.where(user -> user.lastLoginTime().lt(LocalDateTime.now().minusDays(90)))
.executeRows();
// Bulk delete
long deleted = easyEntityQuery.deletable(Order.class)
.where(order -> {
order.status().eq("CANCELLED");
order.createTime().lt(LocalDateTime.now().minusYears(2));
})
.executeRows();
8. 🎨 Complex Joins
Multi-table queries with type safety:
// Three-table join with conditions
List<OrderDetailDTO> details = easyEntityQuery.queryable(Order.class)
.leftJoin(User.class, (order, user) ->
order.userId().eq(user.id())
)
.leftJoin(Product.class, (order, user, product) ->
order.productId().eq(product.id())
)
.where((order, user, product) -> {
order.status().eq("COMPLETED");
user.vipLevel().gt(3);
product.category().eq("Electronics");
})
.select((order, user, product) -> new OrderDetailDTOProxy()
.orderId().set(order.id())
.userName().set(user.name())
.productName().set(product.name())
.totalPrice().set(order.quantity().multiply(product.price()))
)
.toList();
Code Generation Magic
easy-query uses annotation processors to generate type-safe proxies at compile time:
// Step 1: Annotate your entity
@Table("t_user")
@EntityProxy // ← This is all you need!
@Data
public class User {
@Column(primaryKey = true)
private String id;
private String name;
private String email;
private Integer age;
@Navigate(value = RelationTypeEnum.ManyToMany, ...)
private List<Role> roles;
}
// Step 2: Proxy is auto-generated
// You get UserProxy with type-safe methods:
// - user.id() → SQLStringTypeColumn<UserProxy>
// - user.name() → SQLStringTypeColumn<UserProxy>
// - user.age() → SQLIntTypeColumn<UserProxy>
// - user.roles() → Navigation methods
// Step 3: Use it!
List<User> adults = easyEntityQuery.queryable(User.class)
.where(user -> user.age().ge(18)) // Full type safety!
.toList();
Zero runtime overhead, full compile-time safety!
Advanced Features
Subqueries
// Find users who have more than 5 completed orders
List<User> activeUsers = easyEntityQuery.queryable(User.class)
.where(user -> {
user.id().in(
easyEntityQuery.queryable(Order.class)
.where(order -> order.status().eq("COMPLETED"))
.groupBy(order -> GroupKeys.of(order.userId()))
.having(group -> group.count().gt(5L))
.select(order -> order.userId())
);
})
.toList();
Conditional Queries
// Build queries dynamically
public List<User> searchUsers(String name, Integer minAge, String city) {
return easyEntityQuery.queryable(User.class)
.where(user -> {
user.name().like(name != null, name); // Only if name provided
user.age().ge(minAge != null, minAge); // Only if minAge provided
user.city().eq(city != null, city); // Only if city provided
})
.toList();
}
Pagination
// Built-in pagination support
EasyPageResult<User> page = easyEntityQuery.queryable(User.class)
.where(user -> user.status().eq("ACTIVE"))
.orderBy(user -> user.createTime().desc())
.toPageResult(1, 20); // page 1, 20 items per page
System.out.println("Total: " + page.getTotal());
System.out.println("Data: " + page.getData());
Table Sharding Support
// Built-in sharding for high-traffic apps!
@Table(value = "t_order", shardingInitializer = MonthTableShardingInitializer.class)
@EntityProxy
public class Order {
@Column(primaryKey = true)
private String id;
@ShardingTableKey // Sharding key
private LocalDateTime createTime;
private BigDecimal amount;
}
// Query automatically routes to correct sharded tables
List<Order> orders = easyEntityQuery.queryable(Order.class)
.where(order ->
order.createTime().between(
LocalDateTime.of(2024, 1, 1, 0, 0),
LocalDateTime.of(2024, 3, 31, 23, 59)
)
)
.toList();
// Executes across multiple tables in parallel:
// t_order_202401, t_order_202402, t_order_202403
Multi-Database Support
Works with all major databases out of the box:
- ✅ MySQL / MariaDB
- ✅ PostgreSQL
- ✅ SQL Server
- ✅ Oracle
- ✅ SQLite
- ✅ H2
- ✅ DuckDB
- ✅ DM (达梦), KingBase, GaussDB
// Easy to switch databases
EasyQueryClient easyQueryClient = EasyQueryBootstrapper
.defaultBuilderConfiguration()
.setDefaultDataSource(dataSource)
.optionConfigure(op -> {
op.setDatabase(DatabaseType.MYSQL); // Just change this!
})
.build();
Performance Comparison
| Feature | easy-query | JPA/Hibernate | MyBatis |
|---|---|---|---|
| Type Safety | ✅ Full | ⚠️ Partial | ❌ None |
| N+1 Prevention | ✅ Built-in | ⚠️ Manual | ⚠️ Manual |
| Refactoring | ✅ Safe | ⚠️ Risky | ❌ Dangerous |
| Learning Curve | ✅ Easy | ⚠️ Steep | ✅ Easy |
| Performance | ✅ Fast | ✅ Good | ✅ Fast |
| SQL Control | ✅ Full | ⚠️ Limited | ✅ Full |
| Code Generation | ✅ Compile-time | ⚠️ Runtime | ❌ None |
Getting Started
1. Add Dependencies
<dependency>
<groupId>com.easy-query</groupId>
<artifactId>sql-springboot-starter</artifactId>
<version>3.1.49</version>
</dependency>
<!-- Annotation processor for code generation -->
<dependency>
<groupId>com.easy-query</groupId>
<artifactId>sql-processor</artifactId>
<version>3.1.49</version>
<scope>provided</scope>
</dependency>
2. Configure
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: password
easy-query:
enable: true
database: mysql
print-sql: true
name-conversion: underlined # camelCase → snake_case
3. Create Entity
@Table("t_user")
@EntityProxy // Enable code generation
@Data
public class User {
@Column(primaryKey = true)
private String id;
private String name;
private String email;
private Integer age;
private LocalDateTime createTime;
}
4. Query!
@Service
public class UserService {
@Resource
private EasyEntityQuery easyEntityQuery;
public List<User> getActiveUsers() {
return easyEntityQuery.queryable(User.class)
.where(user -> {
user.status().eq("ACTIVE");
user.age().ge(18);
})
.orderBy(user -> user.createTime().desc())
.toList();
}
}
Real-World Example
Here’s a complete service class showing various features:
@Service
@RequiredArgsConstructor
public class OrderService {
private final EasyEntityQuery easyEntityQuery;
// Complex query with joins and aggregation
public List<OrderStatDTO> getMonthlyStats(String userId, int year) {
return easyEntityQuery.queryable(Order.class)
.where(order -> {
order.userId().eq(userId);
order.createTime().format("yyyy").eq(String.valueOf(year));
order.status().eq("COMPLETED");
})
.groupBy(order -> GroupKeys.of(
order.createTime().format("yyyy-MM")
))
.select(OrderStatDTO.class, group -> Select.of(
group.key1().as(OrderStatDTO::getMonth),
group.count().as(OrderStatDTO::getOrderCount),
group.sum(s -> s.totalAmount()).as(OrderStatDTO::getTotalSales)
))
.orderBy(OrderStatDTO::getMonth, true)
.toList();
}
// Query with navigation properties
public OrderDetailVO getOrderDetail(String orderId) {
Order order = easyEntityQuery.queryable(Order.class)
.whereById(orderId)
.include(o -> o.orderItems()) // Load items
.include(o -> o.user()) // Load user
.include(o -> o.shipping()) // Load shipping
.firstOrNull();
if (order == null) {
throw new OrderNotFoundException(orderId);
}
return OrderDetailVO.from(order);
}
// Pagination
public EasyPageResult<OrderListDTO> searchOrders(
OrderSearchRequest request, int page, int size
) {
return easyEntityQuery.queryable(Order.class)
.where(order -> {
order.userId().eq(request.getUserId() != null, request.getUserId());
order.status().eq(request.getStatus() != null, request.getStatus());
order.createTime().ge(request.getStartDate() != null, request.getStartDate());
order.createTime().le(request.getEndDate() != null, request.getEndDate());
})
.orderBy(order -> order.createTime().desc())
.select(OrderListDTO.class)
.toPageResult(page, size);
}
// Bulk update
@Transactional
public long cancelExpiredOrders() {
return easyEntityQuery.updatable(Order.class)
.set(order -> {
order.status().set("CANCELLED");
order.cancelTime().set(LocalDateTime.now());
})
.where(order -> {
order.status().eq("PENDING");
order.createTime().lt(LocalDateTime.now().minusHours(24));
})
.executeRows();
}
}
Why Choose easy-query?
✅ For Developers
- Write less boilerplate, more business logic
- Catch errors at compile time, not production
- Refactor with confidence
- IntelliSense makes you productive
✅ For Teams
- Easy to review – code is self-documenting
- New members get productive quickly
- Less debugging, more feature building
- Consistent coding style
✅ For Applications
- Optimized SQL generation
- No N+1 query surprises
- Efficient batch operations
- Built-in sharding for scale
Community & Resources
- 📦 GitHub: https://github.com/dromara/easy-query
- 📚 Documentation: https://www.easy-query.com/easy-query-doc/en/
Star the repo if you find it useful! ⭐
Try It Now
The best way to understand easy-query is to try it. Here’s a minimal example you can run:
// 1. Add dependencies (see above)
// 2. Configure database (see above)
// 3. Create your first entity:
@Table("t_blog")
@EntityProxy
@Data
public class Blog {
@Column(primaryKey = true)
private String id;
private String title;
private String content;
private Integer views;
private LocalDateTime publishTime;
}
// 4. Start querying:
@Service
public class BlogService {
@Resource
private EasyEntityQuery easyEntityQuery;
public List<Blog> getPopularBlogs() {
return easyEntityQuery.queryable(Blog.class)
.where(blog -> blog.views().gt(1000))
.orderBy(blog -> blog.publishTime().desc())
.limit(10)
.toList();
}
}
Final Thoughts
easy-query brings modern, type-safe database access to Java. It’s:
- Simple – Easy to learn, easy to use
- Safe – Compile-time type checking everywhere
- Powerful – All features you need for complex applications
- Fast – Optimized SQL generation and execution
If you’re tired of string-based queries and runtime errors, give easy-query a try. Your future self will thank you! 🚀
What do you think? Have you tried easy-query? What’s your biggest pain point with current Java ORMs? Let’s discuss in the comments! 💬
Found this helpful? Give it a ⭐ on GitHub!