How EzyJPA works

Updated at 1782229999000
EzyJPA is the JPA integration layer in the EzyData ecosystem. It does not try to replace JPA or Hibernate. Instead, it sits on top of them to standardize how an application creates a DataSource, creates an EntityManagerFactory, builds a database context, manages repositories, auto-generates repository implementations, and maps query results to DTOs.

Overall Architecture

EzyJPA is built around a database context. This context keeps three main groups of information: repositories, registered queries, and query result deserializers. During application startup, the builder scans packages, collects entities, repositories, named queries, and result types, then creates a ready-to-use context.
flowchart TD
    A[Application configures EzyJPA] --> B[Create DataSource]
    B --> C[Create EntityManagerFactory]
    C --> D[Build Database Context]
    D --> E[Scan repositories and queries]
    E --> F[Auto-generate repository implementations]
    F --> G[Application gets repository from context]
    G --> H[Call CRUD or custom query]
    H --> I[JPA EntityManager executes query]

Connection Initialization

EzyJPA provides a dedicated loader for creating a DataSource. The loader receives Properties, converts configuration names from snake_case to camelCase, and maps them into a HikariCP configuration object. This allows external configuration to stay readable while the runtime still uses the correct Hikari configuration fields.
After that, the EntityManagerFactory is created in two steps. EzyJPA first tries to use Hibernate by building a dynamic PersistenceUnitInfo, which contains the datasource, transaction type, JPA version, mapping files, and managed entity classes. If Hibernate creation fails, it falls back to the default JPA mechanism using the persistence unit name.
flowchart LR
    A[Properties] --> B[DataSource Loader]
    B --> C[Hikari DataSource]
    C --> D[EntityManagerFactory Loader]
    E[Entity packages or entity classes] --> D
    F[JPA properties] --> D
    D --> G{Can Hibernate create it?}
    G -->|Yes| H[EntityManagerFactory from Hibernate]
    G -->|No| I[Default JPA fallback]
Entities can be supplied directly or scanned from packages. When scanning packages, EzyJPA looks for classes annotated with JPA @Entity and adds them to the dynamic persistence unit.

Database Context

The database context is the runtime center. It provides createEntityManager(), keeps the repository registry, stores the query manager, and holds result deserializers.
When the context is built, EzyJPA performs these main steps:
sequenceDiagram
    participant App as Application
    participant Builder as Context Builder
    participant Scanner as Scanner
    participant Query as Query Manager
    participant Impl as Repository Implementer
    participant Context as Database Context

    App->>Builder: configure scan, query, repository, EntityManagerFactory
    Builder->>Scanner: scan packages
    Scanner-->>Builder: repositories, named queries, result types
    Builder->>Query: register queries
    Builder->>Impl: generate repository implementations
    Impl-->>Builder: repository instances
    Builder->>Context: attach repositories, queries, deserializers
    Context-->>App: ready-to-use context
The context allows repositories to be retrieved by class or by name. Repositories come from two sources: concrete repository classes written by the user, or repository interfaces auto-implemented by EzyJPA.

How Repositories Work

The base EzyJPA repository provides common CRUD operations such as save, find by id, find by field, find all, count, and delete. Each call creates a new EntityManager, executes the operation, and closes the EntityManager in a finally block.
For write operations such as save, delete, or custom update/delete methods, EzyJPA opens a transaction, commits it on success, and rolls it back on failure.
sequenceDiagram
    participant Service
    participant Repo as Repository
    participant Ctx as EzyJPA Context
    participant EM as EntityManager
    participant DB as Database

    Service->>Repo: save(entity)
    Repo->>Ctx: createEntityManager()
    Ctx-->>Repo: EntityManager
    Repo->>EM: begin transaction
    Repo->>EM: merge(entity)
    EM->>DB: INSERT or UPDATE
    Repo->>EM: commit
    Repo->>EM: close
One useful detail is how EzyJPA handles generated identifiers. After merge, JPA may return a managed entity instance that is different from the original object. EzyJPA reads the @Id value from the merged result and sets it back into the original entity. This helps the caller receive the database-generated or provider-generated id on the object they passed in.

Auto-Generated Repository Implementations

EzyJPA supports repository interfaces. An interface only needs to extend the base repository and be marked for auto implementation. During context build, EzyJPA uses Javassist to generate a runtime class that extends the base JPA repository, implements the user interface, and receives the database context.
Custom methods work in two ways.
The first way is to declare an explicit query through an annotation. A method can use JPQL or native SQL. If it is a native query and the result type is the entity type, the query is created with the entity class. If the result type is a DTO, EzyJPA retrieves the raw result and uses a result deserializer to map it.
The second way is to infer a query from the method name. For example:
findByEmailAndStatus(String email, String status)
is converted into JPQL like:
SELECT e FROM Entity e WHERE e.email = ?0 AND e.status = ?1
Supported method-name operators include the default equality operator, In, Gt, Gte, Lt, and Lte. And conditions are grouped together, while Or creates a separate condition group.
flowchart TD
    A[Repository interface method] --> B{Has query annotation?}
    B -->|Yes| C[Use declared query]
    B -->|No| D[Parse method name]
    D --> E[Create query method model]
    E --> F[Convert to JPQL]
    C --> G[Generate method bytecode]
    F --> G
    G --> H[Runtime repository class]

Queries and Pagination

For methods returning a list, the repository calls getResultList(). If the last parameter is an EzyFox pagination object, EzyJPA treats the method as a pagination method and sets firstResult and maxResults.
For methods returning a single object, EzyJPA sets maxResults(1), retrieves the result list, and returns the first item or null. If the return type is Optional, the result is wrapped with Optional.ofNullable.
For count methods, the result is retrieved with getSingleResult() and converted to int or long. For update or delete methods, EzyJPA requires the return type to be int or void, then calls executeUpdate() inside a transaction.

Query Result Mapping

EzyJPA does not only return entities. For queries that return DTOs or projections, the context uses result deserializers to convert raw query results into the requested type. There are two paths: the user can provide a custom deserializer, or EzyJPA can create a default deserializer based on the binding context.
flowchart LR
    A[Raw query result] --> B{Custom deserializer exists?}
    B -->|Yes| C[Use custom deserializer]
    B -->|No| D[Use binding unmarshaller]
    C --> E[Result DTO]
    D --> E
This is especially useful for native queries that select only a few columns, or for JPQL queries that return projections instead of full entities.

Named Queries

When scanning packages, EzyJPA collects both EzyData named queries and standard JPA named queries such as @NamedQuery and @NamedNativeQuery. These queries are added to the query manager and can be referenced from repository methods.
This lets queries live near entity or result classes, or be added directly when building the context, while repository methods simply reference the query name.

Important Limitations

EzyJPA creates a new EntityManager for each repository call and closes it immediately after use. This design is simple and predictable, but it is not a long-lived session model or a shared persistence context across multiple repository calls.
By default, transaction handling is performed directly for generated or built-in write operations. Transaction annotations on methods do not automatically become transaction interceptors in the default implementation, unless the application provides a wrapper or separate integration layer.
The default JPA fallback only calls JPA by persistence unit name. Therefore, dynamic configuration such as datasource and scanned entity classes built for the Hibernate path may not necessarily apply if runtime falls back to the default JPA path.

Conclusion

EzyJPA works as a bridge between JPA/Hibernate and the EzyData repository model. It keeps JPA as the core persistence engine, while adding practical conveniences: datasource loading, entity scanning, context building, repository registry, repository interface auto-implementation, method-name query inference, named query support, native query support, pagination, and DTO result mapping.
The core design is straightforward: the context manages runtime resources, repositories open an EntityManager, queries come from annotations or method names, and results are returned directly or deserialized into the type the application expects.

Table Of Contents