DEV Community

Li
Li

Posted on

easy-query: The Entity Framework Core for Java Developers

GitHub: easy-query | Stars: 687+ | License: Apache 2.0

Documentation: Official Docs

TL;DR

If you've used Entity Framework Core in .NET and wish Java had something similar, easy-query might be what you're looking for. It's a type-safe, strongly-typed ORM that brings the best of EF Core's API design to the Java ecosystem.


The Problem with Traditional Java ORMs

Let's be honest - while JPA/Hibernate is powerful, it has some pain points:

// Traditional JPA/Hibernate
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> user = cq.from(User.class);
cq.select(user)
  .where(cb.and(
    cb.equal(user.get("name"), "John"),
    cb.greaterThan(user.get("age"), 18)
  ));
List<User> results = em.createQuery(cq).getResultList();
Enter fullscreen mode Exit fullscreen mode

Issues:

  • ❌ String-based field references ("name", "age") - no compile-time safety
  • ❌ Verbose and hard to read
  • ❌ No IntelliSense support
  • ❌ Refactoring nightmare

Enter easy-query: The Java Answer to EF Core

easy-query brings the fluent, type-safe API style that .NET developers love:

// easy-query - Strongly Typed!
List<User> users = easyEntityQuery.queryable(User.class)
    .where(user -> {
        user.name().eq("John");
        user.age().gt(18);
    })
    .toList();
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Compile-time type safety - No more string magic
  • IntelliSense everywhere - Your IDE actually helps you
  • Refactoring friendly - Rename works as expected
  • Clean, readable code - Looks like modern Java

Real-World Comparison

Scenario: Fetch users with their roles and company, sorted by creation date

JPA/Hibernate Way:

String jpql = "SELECT DISTINCT u FROM User u " +
              "LEFT JOIN FETCH u.roles r " +
              "LEFT JOIN FETCH u.company c " +
              "WHERE u.status = :status " +
              "ORDER BY u.createTime DESC";

List<User> users = em.createQuery(jpql, User.class)
    .setParameter("status", 1)
    .getResultList();
Enter fullscreen mode Exit fullscreen mode

easy-query Way:

List<User> users = easyEntityQuery.queryable(User.class)
    .where(user -> user.status().eq(1))
    .include(user -> user.roles())      // Eager loading
    .include(user -> user.company())
    .orderBy(user -> user.createTime().desc())
    .toList();
Enter fullscreen mode Exit fullscreen mode

Much cleaner, right?


Feature Highlights

1. Navigation Properties (Like EF Core's Include)

// Load user with related data
List<User> users = easyEntityQuery.queryable(User.class)
    .include(user -> user.roles())           // Load roles
    .include(user -> user.company())         // Load company
    .include(user -> user.orders(), order -> {
        order.where(o -> o.status().eq("COMPLETED"));
        order.orderBy(o -> o.createTime().desc());
    })
    .toList();

// Avoids N+1 queries automatically!
// SQL 1: SELECT * FROM user
// SQL 2: SELECT * FROM user_role WHERE user_id IN (...)
// SQL 3: SELECT * FROM role WHERE id IN (...)
// SQL 4: SELECT * FROM company WHERE id IN (...)
// SQL 5: SELECT * FROM order WHERE user_id IN (...) AND status = 'COMPLETED'
Enter fullscreen mode Exit fullscreen mode

2. DTO Projections (Similar to EF Core's Select)

// Entity
@Data
@EntityProxy
public class User {
    private String id;
    private String name;
    @Navigate(...)
    private List<Role> roles;
    @Navigate(...)
    private Company company;
}

// DTO with different property names
@Data
public class UserDTO {
    private String userId;
    private String userName;
    private String companyName;
    private List<Role> roleList;  // Different name!
}

// Query with mapping
List<UserDTO> dtos = easyEntityQuery.queryable(User.class)
    .include(user -> user.roles())
    .include(user -> user.company())
    .select(user -> new UserDTOProxy()
        .userId().set(user.id())
        .userName().set(user.name())
        .companyName().set(user.company().name())
        .roleList().set(user.roles())  // Map roles → roleList
    )
    .toList();
Enter fullscreen mode Exit fullscreen mode

3. Group By with Strong Typing

// Group by and aggregate
List<OrderStatDTO> stats = easyEntityQuery.queryable(Order.class)
    .where(order -> order.status().eq("COMPLETED"))
    .groupBy(order -> GroupKeys.of(
        order.userId(),
        order.createTime().format("yyyy-MM")
    ))
    .select(OrderStatDTO.class, group -> Select.of(
        group.key1().as(OrderStatDTO::getUserId),
        group.key2().as(OrderStatDTO::getMonth),
        group.count().as(OrderStatDTO::getOrderCount),
        group.sum(s -> s.amount()).as(OrderStatDTO::getTotalAmount),
        group.avg(s -> s.amount()).as(OrderStatDTO::getAvgAmount)
    ))
    .having(group -> group.count().gt(5L))
    .toList();

// SQL:
// SELECT 
//     user_id,
//     DATE_FORMAT(create_time, '%Y-%m'),
//     COUNT(*),
//     SUM(amount),
//     AVG(amount)
// FROM t_order
// WHERE status = 'COMPLETED'
// GROUP BY user_id, DATE_FORMAT(create_time, '%Y-%m')
// HAVING COUNT(*) > 5
Enter fullscreen mode Exit fullscreen mode

4. Multi-Database Support

easy-query supports all major databases out of the box:

  • MySQL / MariaDB
  • PostgreSQL
  • SQL Server
  • Oracle
  • SQLite
  • H2
  • DuckDB
  • DM (达梦), KingBase, GaussDB (Chinese databases)
// Switch database dialects easily
easy-queryClient easyQueryClient = easy-queryBootstrapper.defaultBuilderConfiguration()
    .setDefaultDataSource(dataSource)
    .optionConfigure(op -> {
        op.setDatabase(DatabaseType.MYSQL);  // or POSTGRESQL, SQLSERVER, etc.
    })
    .build();
Enter fullscreen mode Exit fullscreen mode

Why Choose easy-query Over Traditional ORMs?

Feature easy-query JPA/Hibernate MyBatis
Type Safety ✅ Full ⚠️ Partial (Criteria API) ❌ None (XML/String)
IntelliSense ✅ Excellent ⚠️ Limited ❌ Minimal
Learning Curve ✅ Easy ⚠️ Steep ✅ Easy
N+1 Prevention ✅ Built-in (include) ⚠️ Manual (fetch join) ⚠️ Manual
DTO Mapping ✅ Native ⚠️ External tool needed ✅ Native
Refactoring ✅ Safe ⚠️ Risky ❌ Very Risky
Performance ✅ Optimized ✅ Good ✅ Excellent

Code Generation for Zero Boilerplate

easy-query uses annotation processors to generate type-safe proxies:

// Your entity
@Table("t_user")
@EntityProxy  // ← This triggers code generation
@Data
public class User {
    @Column(primaryKey = true)
    private String id;
    private String name;
    private Integer age;
}

// Generated proxy (automatic)
public class UserProxy extends ProxyEntity<UserProxy, User> {
    public SQLStringTypeColumn<UserProxy> id() { ... }
    public SQLStringTypeColumn<UserProxy> name() { ... }
    public SQLIntTypeColumn<UserProxy> age() { ... }
}

// Now you have full type safety!
Enter fullscreen mode Exit fullscreen mode

Advanced Features

Change Tracking (Like EF Core's ChangeTracker)

// Track entity changes
try (TrackContext track = easyQueryClient.startTrack()) {
    User user = easyEntityQuery.queryable(User.class)
        .whereById("1")
        .firstOrNull();

    user.setName("New Name");  // Track the change
    user.setAge(30);

    track.saveChanges();  // Auto-generates UPDATE SQL
}

// Only modified fields are updated!
// UPDATE t_user SET name = ?, age = ? WHERE id = ?
Enter fullscreen mode Exit fullscreen mode

Bulk Operations

// Bulk delete
long deleted = easyEntityQuery.deletable(User.class)
    .where(user -> user.age().lt(18))
    .executeRows();

// Bulk update
long updated = easyEntityQuery.updatable(User.class)
    .set(user -> user.status().set(0))
    .where(user -> user.loginTime().lt(LocalDateTime.now().minusDays(30)))
    .executeRows();
Enter fullscreen mode Exit fullscreen mode

Subqueries

// Find users with more than 5 orders
List<User> users = 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();
Enter fullscreen mode Exit fullscreen mode

Sharding Support (Advanced Feature!)

easy-query has built-in sharding support for both table sharding and database sharding - a feature rarely seen in Java ORMs!

// Table Sharding by Month
@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
LocalDateTime start = LocalDateTime.of(2024, 1, 1, 0, 0);
LocalDateTime end = LocalDateTime.of(2024, 3, 31, 23, 59);

List<Order> orders = easyEntityQuery.queryable(Order.class)
    .where(order -> order.createTime().between(start, end))
    .toList();

// Executes in parallel across multiple tables:
// t_order_202401, t_order_202402, t_order_202403
Enter fullscreen mode Exit fullscreen mode

This is huge for high-traffic applications! No need for external sharding middleware like ShardingSphere.


Performance Considerations

Include vs Select (N+1 vs JOIN)

// Approach 1: Include (Multiple queries, avoids cartesian product)
List<User> users = easyEntityQuery.queryable(User.class)
    .include(user -> user.roles())  // Separate query
    .toList();
// SQL 1: SELECT * FROM user
// SQL 2: SELECT * FROM user_role WHERE user_id IN (...)
// SQL 3: SELECT * FROM role WHERE id IN (...)

// Approach 2: Select with JOIN (Single query, may have cartesian product)
List<UserDTO> dtos = easyEntityQuery.queryable(User.class)
    .leftJoin(UserRole.class, (user, userRole) -> user.id().eq(userRole.userId()))
    .leftJoin(Role.class, (user, userRole, role) -> userRole.roleId().eq(role.id()))
    .select((user, userRole, role) -> new UserDTOProxy()
        .id().set(user.id())
        .roleName().set(role.name())
    )
    .toList();
// SQL: SELECT u.*, r.* FROM user u LEFT JOIN user_role ur ... LEFT JOIN role r ...
Enter fullscreen mode Exit fullscreen mode

Rule of thumb:

  • Use include for one-to-many/many-to-many relationships
  • Use select + join for one-to-one or when you need specific columns

Getting Started

Maven Dependency

<dependency>
    <groupId>com.easy-query</groupId>
    <artifactId>sql-springboot-starter</artifactId>
    <version>3.1.49</version>  <!-- Check latest version on Maven Central -->
</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>
Enter fullscreen mode Exit fullscreen mode

Latest version: Check Maven Central or GitHub Releases for the most recent version.

Spring Boot Configuration

# 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
Enter fullscreen mode Exit fullscreen mode

First Query

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@Service
public class UserService {
    @Resource
    private EasyEntityQuery easyEntityQuery;

    public List<User> getActiveUsers() {
        return easyEntityQuery.queryable(User.class)
            .where(user -> user.status().eq(1))
            .include(user -> user.roles())
            .toList();
    }
}
Enter fullscreen mode Exit fullscreen mode

Community & Resources


Comparison with Other Modern Java ORMs

vs. jOOQ

  • jOOQ: Requires code generation from database schema (DB-first)
  • easy-query: Code-first approach, generate schema from entities

vs. QueryDSL

  • QueryDSL: Requires APT processor, more verbose API
  • easy-query: Similar approach but cleaner syntax, inspired by EF Core

vs. Exposed (Kotlin)

  • Exposed: Kotlin-specific DSL
  • easy-query: Java-first with Kotlin support

Final Thoughts

If you're a Java developer who's envious of C# developers using Entity Framework Core, give easy-query a try. It brings:

Type safety without sacrificing readability

Modern API design inspired by the best ORMs

Powerful features like navigation properties and change tracking

Great performance with smart query optimization

The project is actively maintained and growing. The developer is very responsive to issues and feature requests.


Try It Yourself

Here's a complete working example you can run:

@EntityProxy
@Data
@Table("t_blog")
public class Blog {
    @Column(primaryKey = true)
    private String id;
    private String title;
    private String content;
    private Integer stars;
    private LocalDateTime createTime;
}

// Query examples
public class BlogService {
    @Resource
    private EasyEntityQuery easyEntityQuery;

    // Simple query
    public List<Blog> getPopularBlogs() {
        return easyEntityQuery.queryable(Blog.class)
            .where(blog -> blog.stars().gt(100))
            .orderBy(blog -> blog.createTime().desc())
            .toList();
    }

    // Complex query with pagination
    public EasyPageResult<Blog> searchBlogs(String keyword, int page, int size) {
        return easyEntityQuery.queryable(Blog.class)
            .where(blog -> {
                blog.title().like(keyword);
                blog.or(() -> {
                    blog.content().like(keyword);
                });
            })
            .orderBy(blog -> blog.stars().desc())
            .toPageResult(page, size);
    }

    // DTO projection
    public List<BlogSummary> getBlogSummaries() {
        return easyEntityQuery.queryable(Blog.class)
            .select(blog -> new BlogSummaryProxy()
                .title().set(blog.title())
                .starCount().set(blog.stars())
                .publishDate().set(blog.createTime().format("yyyy-MM-dd"))
            )
            .toList();
    }
}
Enter fullscreen mode Exit fullscreen mode

What Do You Think?

Have you tried easy-query? Are there features from EF Core you'd like to see in the Java ecosystem?

Discussion points:

  • How does this compare to your current ORM?
  • Would you consider switching from JPA/Hibernate?
  • What other .NET features would you like to see in Java?

Let's discuss in the comments! 💬


Useful Links


Found this helpful? Give it a ⭐ on GitHub and share with your Java developer friends!

Disclaimer: I'm not affiliated with the project, just a developer who found this tool valuable and wanted to share with the community.

Top comments (0)