
Choose a persistence tool by the work your application performs and the control your team needs over it. JDBC exposes SQL and row handling directly; a SQL mapper reduces repetitive mapping while keeping queries visible; an ORM coordinates mapped entities and their persistence lifecycle. The best fit emerges from a small representative workload and clear ownership of transactions and schema changes.
Compare the responsibilities you will own
On a narrow screen, scroll the table horizontally. Keyboard users can focus the table and use the arrow keys.
| Approach | What it helps with | Responsibility to keep visible |
|---|---|---|
| JDBC | Driver-based access, explicit SQL and direct result processing | Mapping, resource lifetime, transaction boundaries and repeated plumbing. |
| SQL mapper, such as MyBatis | Mapping query parameters and results to Java types | SQL quality, dynamic-query rules and transaction integration. |
| ORM using Jakarta Persistence | Entity mappings, persistence context and lifecycle operations | Generated queries, fetch plans, entity state and transaction boundaries. |
All three still depend on database behavior. A mapper or ORM can reduce repetitive Java work while leaving indexing, constraints, isolation and data volume as central design issues. Generated code also needs review when the schema changes.
The JDBC API defines the direct access layer. MyBatis mapper documentation shows parameter and result mapping with explicit SQL. The Jakarta Persistence specification defines entity lifecycle, persistence contexts and transaction-related behavior. Use these contracts to identify what each layer actually provides.
Match the approach to a concrete task
For a reporting service built around carefully tuned SQL, direct JDBC or a SQL mapper gives the team straightforward control over joins, projections and database-specific features. JDBC can be enough for a small number of queries; a mapper becomes attractive when repeated parameter and row mapping consumes attention.
For an application with an established object model and many entity lifecycle operations, an ORM can centralize mappings and coordinate changes within a persistence context. The team must understand when data is fetched, when pending changes are flushed, and how detached objects are handled. Entity convenience should be evaluated alongside the SQL it causes.
These are starting hypotheses, not universal rankings. A single system can deliberately use ORM for entity updates and explicit SQL for reporting. Keep a common transaction policy and avoid two layers unexpectedly modifying the same cached entity state. Document where each approach is allowed and why.
Evaluate one representative order-summary page
Use a synthetic scenario: show 20 orders, each with its customer name and line-item count, sorted by date and ID. Include an order with no lines, two orders with the same date and a customer with several orders. Require one output row per order and stable pagination. Those cases expose mistakes that a one-row demonstration hides.
Download the persistence decision worksheet (Markdown)
- Fix the schema, input data, selected fields, sort order and expected result.
- Implement that same result with the approaches under consideration.
- Observe the SQL statements, rows returned and database execution plans.
- Check a failed write and rollback, then test concurrent updates relevant to the application.
- Record development and maintenance friction alongside performance evidence.
A grouped query is one possible design for the count projection:
SELECT o.id, c.name, COUNT(l.id) AS line_count
FROM orders o
JOIN customers c ON c.id = o.customer_id
LEFT JOIN order_lines l ON l.order_id = o.id
GROUP BY o.id, c.name, o.created_at
ORDER BY o.created_at DESC, o.id DESC
FETCH FIRST 20 ROWS ONLYThe left join retains zero-line orders. Counting the line's non-null identifier yields zero for that case; COUNT(*) would count the retained joined row. Aggregation happens before limiting the result here, so the page contains order summaries. For more complex fetching, another option is to fetch the page of order IDs and then retrieve related information for those IDs. A join that limits individual line rows can split orders across pages.
This query illustrates the required result, not a claim that every database will execute it optimally. Check its plan and indexes on the target database. For a runnable transaction baseline, use the companion JDBC stock-bin project.
Find hidden query multiplication
Those counts describe round trips, not elapsed-time measurements. A single query that returns a huge duplicated join can cost more than two well-shaped queries. Inspect row volume, plans, indexes, caching and network latency before choosing a fetch strategy.
In an ORM, log the SQL for the actual endpoint, including accesses made while rendering the response. Avoid discovering an unplanned relationship load only after leaving the intended transaction boundary. Query projections can be a useful fit for read-only summaries; entity graphs and fetch joins require their own pagination and cardinality checks. Verify behavior against the selected provider and version.
Keep transactions, schema and concurrency explicit
Assign one layer responsibility for the transaction that represents a business operation. A stock transfer, for example, needs both changes to succeed together; committing inside each repository method can defeat that boundary. A framework-managed connection also has a different ownership contract from a locally created JDBC connection.
Use database constraints to enforce durable rules, and versioned migrations to evolve the schema. Decide how a deployment behaves while old and new application versions coexist. Automatic schema generation can help a disposable prototype, but production changes need a reviewed migration and recovery plan.
For concurrent writes, choose a policy such as optimistic version checking or database locking based on the operation's semantics. Define what the user sees on a conflict and when a retry is valid. Exercise it with two competing operations against the actual database; a single-threaded happy-path test leaves the central question unanswered.
Write a decision someone else can revisit
Record exact JDK, driver, database and framework versions, the workload, the expected results and the commands needed to repeat the comparison. State the selected approach and the requirements that drove it. Preserve known costs: query verbosity, mapping maintenance, provider-specific behavior or training needs.
Revisit the choice when query patterns, relationship depth, database features or team ownership change. Prefer a small measured prototype over a promise that one tool makes every application faster. The worksheet keeps correctness, operational visibility and maintenance in the same decision record.