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!

 
		
