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:

  1. Type Safety First – If it compiles, it works. No runtime surprises.
  2. Developer Experience – IntelliSense everywhere, readable code, easy refactoring.
  3. 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

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!

Similar Posts