Back to JPA Fundamentals
Q1Question 1 of 5

What is JPA vs Hibernate?

Key Concepts
  • JPA is a specification (interfaces and annotations) that defines how Java objects map to database tables
  • Hibernate is the most popular JPA implementation that generates SQL, manages the persistence context, and performs dirty checking
  • EntityManager (JPA) and Session (Hibernate) are the core interfaces for CRUD operations on entities
  • SessionFactory is heavyweight and thread-safe, created once per application; Session is lightweight and per-transaction
  • A Dialect tells Hibernate how to generate database-specific SQL for PostgreSQL, Oracle, or MySQL
  • Spring Boot auto-detects the Dialect from the JDBC URL, so manual configuration is rarely needed
  • You can unwrap EntityManager to Session to access Hibernate-native features like batch size hints and natural ID lookups

Definition

JPA is a specification that defines standard interfaces and annotations for ORM, while Hibernate is the most popular implementation that generates SQL, manages the persistence context, and talks to JDBC.
  • JPA (Jakarta Persistence API) – a specification that defines interfaces (EntityManager, EntityTransaction) and annotations (@Entity, @Id, @OneToMany) for mapping Java objects to database tables. It defines what must happen, not how.
  • Hibernate – the most popular JPA implementation. It provides the real classes that generate SQL, manage the persistence context, perform dirty checking, and communicate with JDBC.
  • EntityManager / Session – the core interface for CRUD operations. EntityManager is the JPA standard; Hibernate's Session extends it with native features like @BatchSize and custom caching.
  • SessionFactory / EntityManagerFactory – heavyweight, thread-safe factory created once per application. It parses mappings, builds the metamodel, and creates Session or EntityManager instances.
  • Dialect – tells Hibernate how to generate SQL for a specific database. PostgreSQLDialect generates LIMIT/OFFSET; OracleDialect generates FETCH FIRST and SEQUENCE.

Think of JPA as the RBI banking regulations that every bank in India must follow. Hibernate is a specific bank like National Bank of India that implements those regulations. Both handle the same customer operations (deposits, withdrawals, transfers), but National Bank of India offers extra services (priority banking, locker facility) beyond what RBI mandates. The Dialect is the local language the bank uses to talk to its vault system – PostgreSQL speaks one dialect, Oracle speaks another.

Understanding the Concepts

What is JPA?

JPA stands for Jakarta Persistence API (formerly javax.persistence, now jakarta.persistence). It is a specification – a set of interfaces and annotations that define how Java objects map to database tables. JPA provides EntityManager, @Entity, @Id, @OneToMany, @ManyToOne, and the JPQL query language. A specification is a contract: it describes what must happen, not how it happens. On its own, JPA does nothing – you need an implementation.

In our banking system at National Bank of India, Rajesh annotates the Account entity with JPA annotations. These annotations are portable across any JPA provider.

JAVA
package com.bank.model;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "accounts")
public class Account {
    @Id
    @Column(name = "account_id")
    private String accountId;  // SAV2001

    @Column(name = "holder_name", nullable = false)
    private String holderName;  // Rajesh Kumar

    @Column(precision = 15, scale = 2)
    private BigDecimal balance;  // always BigDecimal for money

    // getters, setters
}

These annotations are JPA standard. They work with Hibernate, EclipseLink, or any other JPA implementation without changes.

What is Hibernate?

Hibernate is the most popular implementation of the JPA specification. It provides the real classes that honor the JPA contract. When you call entityManager.persist(account), Hibernate is the code that generates the INSERT SQL, manages the persistence context, performs dirty checking, and communicates with JDBC.

Hibernate also provides features beyond the JPA specification:

  • @BatchSize for batched lazy loading
  • @Formula for computed columns
  • @NaturalId for business key lookups
  • Second-level cache with configurable providers (Ehcache, Caffeine)
  • Custom types and user types
JAVA
package com.bank.model;

import jakarta.persistence.*;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.NaturalId;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "accounts")
public class Account {
    @Id
    @Column(name = "account_id")
    private String accountId;

    @NaturalId  // Hibernate-specific: business key lookup
    @Column(name = "pan_number", unique = true)
    private String panNumber;

    @Column(name = "holder_name")
    private String holderName;

    @Column(precision = 15, scale = 2)
    private BigDecimal balance;

    @OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
    @BatchSize(size = 50)  // Hibernate-specific: batch loading
    private List<Transaction> transactions = new ArrayList<>();
}

How is JPA different from Hibernate?

JPA and Hibernate serve different roles. JPA defines the rules; Hibernate executes them. The JDBC analogy helps: JPA is like the JDBC specification (Connection, Statement, ResultSet), while Hibernate is like the PostgreSQL JDBC driver – the concrete implementation. You can swap Hibernate for EclipseLink without changing your JPA code, just as you can swap PostgreSQL driver for MySQL driver without changing JDBC code.

| Aspect | JPA | Hibernate |
|--------|-----|----------|
| Type | Specification (interfaces) | Implementation (classes) |
| Package | jakarta.persistence | org.hibernate |
| Core interface | EntityManager | Session (extends EntityManager) |
| Factory | EntityManagerFactory | SessionFactory |
| Query language | JPQL | HQL (superset of JPQL) |
| Caching | Not specified | First-level + second-level cache |
| Batch loading | Not specified | @BatchSize, @Fetch(SUBSELECT) |
| Portability | Swap any provider | Locked to Hibernate-specific features |

JAVA
package com.bank.service;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.hibernate.Session;
import com.bank.model.Account;

public class AccountService {

    @PersistenceContext
    private EntityManager em;  // JPA standard

    public void demonstrateBothApproaches() {
        // JPA standard: portable across providers
        Account account = em.find(Account.class, "SAV2001");

        // Hibernate native: unwrap to Session
        Session session = em.unwrap(Session.class);
        Account byPan = session.byNaturalId(Account.class)
            .using("panNumber", "ABCDE1234F")
            .load();
    }
}

What are the core Hibernate interfaces?

Hibernate has four core interfaces that every developer should understand.

  • SessionFactory – heavyweight, thread-safe, created once per application. It parses mappings, builds the metamodel, and manages the second-level cache. The JPA equivalent is EntityManagerFactory.
  • Session – lightweight, not thread-safe, created per transaction or unit of work. It holds the first-level cache (persistence context) and tracks managed entities. The JPA equivalent is EntityManager.
  • Transaction – controls commit and rollback. The JPA equivalent is EntityTransaction.
  • Query – represents a JPQL, HQL, or native SQL query.
JAVA
package com.bank.config;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import com.bank.model.Account;
import java.math.BigDecimal;

public class HibernateDemo {

    public void coreInterfacesDemo(SessionFactory sessionFactory) {
        // SessionFactory: created ONCE at startup (expensive)
        // Session: created per transaction (lightweight)
        Session session = sessionFactory.openSession();
        Transaction tx = session.beginTransaction();

        Account account = new Account();
        account.setAccountId("SAV2001");
        account.setHolderName("Rajesh Kumar");
        account.setBalance(new BigDecimal("500000.00"));

        session.persist(account);  // queues INSERT
        tx.commit();               // flushes and commits
        session.close();           // releases resources
    }
}

What is the difference between EntityManager and Session?

EntityManager is the JPA standard interface. Session is Hibernate's extended version that adds features like natural ID lookups, JDBC batch size control, and direct cache management. In Spring Boot, you inject EntityManager and unwrap to Session only when needed.

| Feature | EntityManager (JPA) | Session (Hibernate) |
|---------|-------------------|---------------------|
| Standard | JPA specification | Hibernate-specific |
| Inject | @PersistenceContext | em.unwrap(Session.class) |
| Find by ID | em.find(Class, id) | session.get(Class, id) |
| Lazy proxy | em.getReference(Class, id) | session.load(Class, id) |
| Natural ID | Not supported | session.byNaturalId(Class) |
| Batch hints | Not directly | session.setJdbcBatchSize(50) |

JAVA
package com.bank.service;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.hibernate.Session;
import com.bank.model.Account;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountLookupService {

    @PersistenceContext
    private EntityManager em;

    @Transactional(readOnly = true)
    public Account findByJpa(String accountId) {
        // JPA standard: works with any provider
        return em.find(Account.class, accountId);
    }

    @Transactional(readOnly = true)
    public Account findByNaturalId(String panNumber) {
        // Hibernate native: unwrap for natural ID lookup
        Session session = em.unwrap(Session.class);
        return session.byNaturalId(Account.class)
            .using("panNumber", panNumber)
            .load();
        // Generates: SELECT ... WHERE pan_number = ?
    }
}

What is a Hibernate Dialect?

A Dialect tells Hibernate how to generate SQL for a specific database. Different databases use different syntax for pagination, auto-increment columns, data types, and built-in functions.

  • PostgreSQLDialect generates SERIAL, LIMIT/OFFSET, TEXT type
  • OracleDialect generates SEQUENCE, FETCH FIRST N ROWS, CLOB type
  • MySQLDialect generates AUTO_INCREMENT, LIMIT, LONGTEXT type

Spring Boot auto-detects the Dialect from the JDBC URL. You rarely need to configure it manually.

YAML
# application.yml: Spring Boot auto-detects dialect from URL
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/national_bank
    username: rajesh
    password: ${DB_PASSWORD}
  jpa:
    # Dialect auto-detected: PostgreSQLDialect
    # Override only if auto-detection fails:
    # properties:
    #   hibernate:
    #     dialect: org.hibernate.dialect.PostgreSQLDialect
JAVA
package com.bank.model;

import jakarta.persistence.*;

@Entity
@Table(name = "bank_transactions")
public class BankTransaction {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    // PostgreSQL: SERIAL (auto-increment)
    // Oracle: SEQUENCE + trigger
    // MySQL: AUTO_INCREMENT
    // The Dialect handles the difference!
    private Long id;

    @Column(name = "description", length = 500)
    // PostgreSQL: VARCHAR(500)
    // Oracle: VARCHAR2(500)
    private String description;
}

What are common mistakes when using JPA vs Hibernate?

The most common mistake is using Hibernate-native APIs everywhere without understanding the portability cost.

JAVA
package com.bank.service;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.hibernate.Session;
import com.bank.model.Account;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountMistakeDemo {

    @PersistenceContext
    private EntityManager em;

    // WRONG: using Hibernate Session for basic CRUD
    @Transactional
    public void wrongApproach(String accountId) {
        Session session = em.unwrap(Session.class);
        Account account = session.get(Account.class, accountId);
        // Unnecessary unwrap for a basic find operation!
        // If you switch to EclipseLink, this code breaks.
    }

    // CORRECT: use JPA standard for basic CRUD
    @Transactional
    public void correctApproach(String accountId) {
        // JPA standard: works with any provider
        Account account = em.find(Account.class, accountId);
        account.setHolderName("Priya Sharma");
        // Dirty checking handles the UPDATE automatically

        // Unwrap to Session ONLY for Hibernate-specific features
        Session session = em.unwrap(Session.class);
        Account byPan = session.byNaturalId(Account.class)
            .using("panNumber", "XYZAB5678P")
            .load();
    }
}

When should you use Hibernate-native features vs JPA standard?

Use JPA standard for all common operations. Reach for Hibernate-native features only when JPA cannot express what you need.

  • Use JPA when:
  • Basic CRUD operations (persist, find, merge, remove)
  • JPQL queries and derived query methods
  • Entity lifecycle callbacks (@PrePersist, @PostLoad)
  • Standard annotations (@Entity, @Id, @OneToMany)
  • Use Hibernate-native when:
  • Natural ID lookups (session.byNaturalId())
  • Batch size configuration (@BatchSize, session.setJdbcBatchSize())
  • Second-level cache (@Cache)
  • Computed columns (@Formula)
  • Multitenancy support
JAVA
package com.bank.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.bank.model.Account;
import java.math.BigDecimal;
import java.util.List;

// Spring Data JPA: combines JPA standard with Hibernate underneath
public interface AccountRepository extends JpaRepository<Account, String> {

    // Derived query: JPA standard
    List<Account> findByHolderName(String name);

    // JPQL: JPA standard
    @Query("SELECT a FROM Account a WHERE a.balance > :min ORDER BY a.balance DESC")
    List<Account> findHighValueAccounts(@Param("min") BigDecimal minBalance);

    // Native SQL: when JPQL cannot express the query
    @Query(value = "SELECT * FROM accounts WHERE balance > :min "
         + "AND created_at > NOW() - INTERVAL '30 days'",
           nativeQuery = true)
    List<Account> findRecentHighValueAccounts(@Param("min") BigDecimal minBalance);
}
TEXT
Expected output (findHighValueAccounts with min=100000):
Account{accountId='SAV2001', holderName='Rajesh Kumar', balance=500000.00}
Account{accountId='SAV2042', holderName='Priya Sharma', balance=250000.00}

Visual Overview

DIAGRAM
  ┌───────────────────────────────────────────────┐
  │              Your Java Code                  │
  │  @Entity, @Id, @OneToMany, EntityManager      │
  └───────────────────────┬───────────────────────┘
                         │
                   JPA Standard
                   (Interfaces)
                         │
                         ▼
  ┌───────────────────────────────────────────────┐
  │          Hibernate (Implementation)            │
  │                                                 │
  │  ┌─────────────┐  ┌────────────┐  ┌─────────┐  │
  │  │ Session     │  │ Dirty       │  │ Query   │  │
  │  │ (Persist.   │  │ Checking    │  │ Engine  │  │
  │  │  Context)   │  │ (Snapshots) │  │ (JPQL)  │  │
  │  └─────────────┘  └────────────┘  └─────────┘  │
  │                                                 │
  │  ┌───────────────────────────────────────────┐  │
  │  │          Dialect (SQL Generation)           │  │
  │  │  PostgreSQL │ Oracle │ MySQL │ H2          │  │
  │  └───────────────────────────────────────────┘  │
  └───────────────────────┬───────────────────────┘
                         │
                       JDBC
                         │
                         ▼
  ┌───────────────────────────────────────────────┐
  │              Database                          │
  │  PostgreSQL / Oracle / MySQL                   │
  └───────────────────────────────────────────────┘

Key Takeaway

JPA is the specification that defines standard annotations and interfaces for object-relational mapping in Java. It provides EntityManager, @Entity, @Id, @OneToMany, and JPQL. Hibernate is the most popular implementation of JPA. It does the real work: generating SQL, managing the persistence context, performing dirty checking, and communicating with JDBC through the appropriate Dialect.

In our banking system at National Bank of India, Rajesh writes all entity annotations using JPA standard from jakarta.persistence. The Account entity uses @Entity, @Id, and @Column that work with any JPA provider. Spring Data JPA uses JPA's EntityManager with Hibernate underneath. For everyday CRUD operations, Rajesh never touches Hibernate-specific code. When he needs batch loading for the account statement service or natural ID lookups by PAN number, he unwraps EntityManager to Hibernate's Session.

The Dialect is Hibernate's bridge to database-specific SQL. PostgreSQLDialect generates LIMIT/OFFSET for pagination and SERIAL for auto-increment columns. OracleDialect generates FETCH FIRST and SEQUENCE for the same operations. Spring Boot auto-detects the Dialect from the JDBC URL, so configuration is rarely needed.

Remember one sentence: JPA defines the rules, Hibernate follows them, and the Dialect translates them into your database's language.

---

Share
Use ← → arrow keys to navigate