Our Roadmaps
Backend DevelopmentDatabasesORMs

Learn Hibernate and JPA

A comprehensive Hibernate and JPA course

Welcome to the comprehensive Hibernate and JPA course. Hibernate is the most popular ORM implementation for Java.

Table of contents

What is JPA and Hibernate?

JPA (Java Persistence API)

JPA is a specification (interface) for ORM in Java. It defines a set of interfaces and annotations.

Hibernate

Hibernate is the most popular implementation of the JPA specification. It provides additional features beyond JPA.

Key Differences

FeatureJPAHibernate
TypeSpecificationImplementation
Annotationsjavax.persistenceorg.hibernate.annotations
PortabilityDatabase agnosticAdditional features

Why Hibernate?

1. Industry Standard

Most Java enterprise applications use Hibernate.

2. Productivity

Focus on business logic, not SQL.

3. Database Agnostic

Switch databases easily.

4. Performance

Built-in caching and optimizations.

5. Community

Strong support and documentation.

Installation and Setup

Maven Dependencies

<!-- JPA API -->
<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.1.0</version>
</dependency>

<!-- Hibernate Core -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.2.0</version>
</dependency>

<!-- Hibernate for Jakarta EE -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.2.0</version>
    <classifier>jakarta</classifier>
</dependency>

<!-- Database Driver -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.6.0</version>
</dependency>

persistence.xml

<!-- META-INF/persistence.xml -->
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             version="3.0">

    <persistence-unit name="myapp" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <class>com.example.entity.User</class>
        <class>com.example.entity.Order</class>

        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="org.postgresql.Driver"/>
            <property name="jakarta.persistence.jdbc.url"
                     value="jdbc:postgresql://localhost:5432/mydb"/>
            <property name="jakarta.persistence.jdbc.user" value="user"/>
            <property name="jakarta.persistence.jdbc.password" value="password"/>

            <property name="hibernate.dialect"
                     value="org.hibernate.dialect.PostgreSQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Programmatic Setup

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp");
EntityManager em = emf.createEntityManager();

Entities

@Entity and @Table

@Entity
@Table(name = "users")
public class User {
    // Fields and methods
}

@Id and @GeneratedValue

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_name", length = 100, nullable = false)
    private String name;
}

Field vs Property Access

// Field access (default with @Id on field)
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;
}

// Property access (with @Id on getter)
@Entity
public class User {
    private Long id;

    @Id
    @GeneratedValue
    public Long getId() { return id; }
}

Primary Keys

Auto-Generated

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// Or using GenerationType.SEQUENCE
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "user_id_seq")
private Long id;

Assigned Keys

@Id
private String isbn;

Composite Keys

// Using @IdClass
@IdClass(OrderItemId.class)
@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id
    private Long orderId;

    @Id
    private Long productId;

    private int quantity;
}

public class OrderItemId implements Serializable {
    private Long orderId;
    private Long productId;
    // equals, hashCode
}

// Using @EmbeddedId
@Embeddable
public class OrderItemId implements Serializable {
    private Long orderId;
    private Long productId;
}

@Entity
public class OrderItem {
    @EmbeddedId
    private OrderItemId id;
    private int quantity;
}

Basic Mappings

@Column

@Column(name = "email", length = 255, nullable = false, unique = true)
private String email;

@Column(columnDefinition = "VARCHAR(100) DEFAULT 'Unknown'")
private String nickname;

@Temporal

@Temporal(TemporalType.DATE)
private Date birthDate;

@Temporal(TemporalType.TIME)
private Date alarmTime;

@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;

@Enumerated

public enum Status { ACTIVE, INACTIVE, PENDING }

@Enumerated(EnumType.STRING)  // Or ORDINAL
private Status status;

@Lob

@Lob
private String description;  // CLOB

@Lob
private byte[] imageData;  // BLOB

@Transient

@Transient
private int age;  // Not persisted

@Formula

@Formula("price * quantity")
private double total;

Entity Manager

Obtaining EntityManager

// Container-managed (CDI)
@PersistenceContext
private EntityManager em;

// Application-managed
EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp");
EntityManager em = emf.createEntityManager();

Basic Operations

// Find
User user = em.find(User.class, 1L);

// Persist
em.persist(user);

// Merge
em.merge(user);

// Remove
em.remove(user);

// Refresh
em.refresh(user);

// Detach
em.detach(user);

EntityTransaction

EntityTransaction tx = em.getTransaction();
tx.begin();
try {
    // Operations
    em.persist(new User("John"));
    tx.commit();
} catch (Exception e) {
    tx.rollback();
    throw e;
}

CRUD Operations

Create

User user = new User();
user.setName("John");
user.setEmail("john@example.com");

em.persist(user);  // Returns void
// Or
User saved = em.merge(user);  // Returns merged entity

Read

// By primary key
User user = em.find(User.class, 1L);

// By reference (doesn't hit DB until used)
User userRef = em.getReference(User.class, 1L);

Update

// If managed, changes auto-saved on flush
User user = em.find(User.class, 1L);
user.setName("Updated Name");
// No explicit update needed

// Merge detached entity
em.merge(detachedUser);

Delete

User user = em.find(User.class, 1L);
em.remove(user);

JPQL

Basic Queries

// Get all
TypedQuery<User> query = em.createQuery("SELECT u FROM User u", User.class);
List<User> users = query.getResultList();

// With WHERE
TypedQuery<User> query = em.createQuery(
    "SELECT u FROM User u WHERE u.active = true AND u.name LIKE :name",
    User.class);
query.setParameter("name", "John%");
List<User> users = query.getResultList();

// Single result
Query query = em.createQuery("SELECT COUNT(u) FROM User u");
Long count = (Long) query.getSingleResult();

Projections

// Single column
TypedQuery<String> query = em.createQuery(
    "SELECT u.name FROM User u", String.class);

// Multiple columns - Object[]
TypedQuery<Object[]> query = em.createQuery(
    "SELECT u.name, u.email FROM User u", Object[].class);
List<Object[]> results = query.getResultList();

// Constructor expression
TypedQuery<UserDTO> query = em.createQuery(
    "SELECT new com.example.UserDTO(u.name, u.email) FROM User u",
    UserDTO.class);

JOINs

// Implicit join (creates Cartesian product)
List<User> users = em.createQuery(
    "SELECT u FROM User u WHERE u.address.city = 'NYC'",
    User.class).getResultList();

// Explicit join
List<User> users = em.createQuery(
    "SELECT DISTINCT u FROM User u JOIN u.orders o WHERE o.total > 100",
    User.class).getResultList();

// LEFT JOIN
List<User> users = em.createQuery(
    "SELECT u FROM User u LEFT JOIN u.profile p WHERE p IS NULL",
    User.class).getResultList();

Aggregate Functions

SELECT COUNT(u) FROM User u
SELECT AVG(o.total) FROM Order o
SELECT SUM(o.total) FROM Order o
SELECT MIN(u.createdAt) FROM User u
SELECT MAX(u.createdAt) FROM User u
SELECT COUNT(DISTINCT u.country) FROM User u

GROUP BY and HAVING

TypedQuery<Object[]> query = em.createQuery(
    "SELECT u.country, COUNT(u) FROM User u GROUP BY u.country HAVING COUNT(u) > 10",
    Object[].class);

Subqueries

// In WHERE
SELECT u FROM User u WHERE u.orders.size > 5

// Correlated subquery
SELECT u FROM User u WHERE EXISTS (
    SELECT o FROM Order o WHERE o.user = u AND o.total > 1000
)

Case Expressions

SELECT CASE u.status
    WHEN 'ACTIVE' THEN 'Enabled'
    ELSE 'Disabled'
END FROM User u

SELECT CASE
    WHEN u.orders.size = 0 THEN 'No orders'
    WHEN u.orders.size < 5 THEN 'Few orders'
    ELSE 'Many orders'
END FROM User u

Criteria API

Basic Queries

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> user = cq.from(User.class);
cq.select(user);
List<User> users = em.createQuery(cq).getResultList();

WHERE Clause

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("active"), true),
        cb.like(user.get("name"), "John%")
    )
);

List<User> users = em.createQuery(cq).getResultList();

Predicates

// Equal
cb.equal(user.get("name"), "John");

// Not equal
cb.notEqual(user.get("status"), "INACTIVE");

// Greater than
cb.gt(user.get("age"), 18);

// Like
cb.like(user.get("name"), "%John%");

// In
cb.in(user.get("status")).value("ACTIVE").value("PENDING");

// Between
cb.between(user.get("createdAt"), startDate, endDate);

// Is null
cb.isNull(user.get("profile"));

// Is empty
cb.isEmpty(user.get("orders"));

Projections

CriteriaQuery<String> cq = cb.createQuery(String.class);
Root<User> user = cq.from(User.class);
cq.select(user.get("name"));
List<String> names = em.createQuery(cq).getResultList();

// Multiple columns
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<User> user = cq.from(User.class);
cq.multiselect(user.get("name"), user.get("email"));
List<Object[]> results = em.createQuery(cq).getResultList();

Joins

CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> user = cq.from(User.class);
Join<User, Order> orders = user.join("orders");
cq.where(cb.gt(orders.get("total"), 1000));
cq.distinct(true);

ORDER BY

cq.orderBy(
    cb.asc(user.get("name")),
    cb.desc(user.get("createdAt"))
);

Group By

CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<User> user = cq.from(User.class);
Join<User, String> country = user.get("country");
cq.multiselect(country, cb.count(user));
cq.groupBy(country);
cq.having(cb.gt(cb.count(user), 10));

Metamodel

CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> user = cq.from(User.class);
cq.where(cb.equal(user.get(User_.name), "John"));

Native Queries

Basic Native Query

Query query = em.createNativeQuery(
    "SELECT * FROM users WHERE active = true", User.class);
List<User> users = query.getResultList();

With Parameters

Query query = em.createNativeQuery(
    "SELECT * FROM users WHERE id = ?", User.class);
query.setParameter(1, 1L);

Query query = em.createNativeQuery(
    "SELECT * FROM users WHERE name = :name", User.class);
query.setParameter("name", "John");

Scalar Results

Query query = em.createNativeQuery(
    "SELECT COUNT(*) FROM users");
Number count = (Number) query.getSingleResult();

Named Native Queries

@NamedNativeQueries({
    @NamedNativeQuery(
        name = "User.findByEmail",
        query = "SELECT * FROM users WHERE email = :email",
        resultClass = User.class
    )
})
@Entity
public class User { }

Query query = em.createNamedQuery("User.findByEmail");
query.setParameter("email", "john@example.com");

Result Set Mapping

@SqlResultSetMapping(
    name = "UserResult",
    entities = @EntityResult(entityClass = User.class)
)
@Entity
public class User { }

// Or with column mapping
@SqlResultSetMapping(
    name = "UserNameResult",
    columns = @ColumnResult(name = "user_name")
)

Relationships

@OneToOne

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    @OneToOne
    @JoinColumn(name = "profile_id")
    private UserProfile profile;
}

// Bidirectional
@Entity
public class UserProfile {
    @Id
    @GeneratedValue
    private Long id;

    @OneToOne(mappedBy = "profile")
    private User user;
}

@OneToMany

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Order> orders = new ArrayList<>();
}

// Unidirectional (uses join table)
@OneToMany
@JoinColumn(name = "user_id")  // Foreign key in Order table
private List<Order> orders;

@ManyToOne

@Entity
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
}

@ManyToMany

@Entity
public class Student {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private List<Course> courses = new ArrayList<>();
}

@Entity
public class Course {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(mappedBy = "courses")
    private List<Student> students = new ArrayList<>();
}

Cascade Types

Cascade Options

@OneToMany(cascade = CascadeType.ALL)  // All operations
@OneToMany(cascade = CascadeType.PERSIST)  // Only persist
@OneToMany(cascade = CascadeType.MERGE)  // Only merge
@OneToMany(cascade = CascadeType.REMOVE)  // Only remove
@OneToMany(cascade = CascadeType.REFRESH)  // Only refresh
@OneToMany(cascade = CascadeType.DETACH)  // Only detach

Example

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();

// When persisting user, orders are also persisted
User user = new User();
user.addOrder(new Order());
em.persist(user);

// When removing user, orders are also removed
em.remove(user);

Fetching Strategies

Eager vs Lazy

@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)  // Default
private List<Order> orders;

@OneToOne(fetch = FetchType.EAGER)  // Not recommended
private UserProfile profile;

@Fetch(FetchMode)

@OneToMany(mappedBy = "user")
@Fetch(FetchMode.SUBSELECT)  // SUBSELECT, SELECT, JOIN
private List<Order> orders;

Entity Graph

@NamedEntityGraph(
    name = "User.withOrders",
    attributeNodes = @NamedAttributeNode("orders")
)
@Entity
public class User { }

// Usage
EntityGraph<?> graph = em.getEntityGraph("User.withOrders");
List<User> users = em.createQuery("SELECT u FROM User u")
    .setHint("jakarta.persistence.fetchgraph", graph)
    .getResultList();

// Dynamic entity graph
EntityGraph<User> graph = em.createEntityGraph(User.class);
graph.addAttributeNodes("orders");

Inheritance Mapping

@Inheritance

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)  // Default
@Inheritance(strategy = InheritanceType.JOINED)
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Entity
public class BillingDetails { }

Single Table (Default)

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Entity
public abstract class BillingDetails {
    @Id
    @GeneratedValue
    private Long id;

    @Enumerated(EnumType.STRING)
    @Column(name = "type")
    private BillingType type;
}

@Entity
public class CreditCard extends BillingDetails {
    private String cardNumber;
}

@Entity
public class BankAccount extends BillingDetails {
    private String accountNumber;
}

Joined Table

@Inheritance(strategy = InheritanceType.JOINED)
@Entity
public abstract class BillingDetails {
    @Id
    @GeneratedValue
    private Long id;
}

@Entity
public class CreditCard extends BillingDetails {
    private String cardNumber;
}

Transactions

Container-Managed (CDI)

@Stateless
public class UserService {
    @PersistenceContext
    private EntityManager em;

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void saveUser(User user) {
        em.persist(user);
    }
}

Application-Managed

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp");
EntityManager em = emf.createEntityManager();

em.getTransaction().begin();
try {
    em.persist(new User("John"));
    em.getTransaction().commit();
} catch (Exception e) {
    em.getTransaction().rollback();
    throw e;
}

REQUIRED vs REQUIRES_NEW

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)  // New transaction
public void newTransaction() {
    // Runs in separate transaction
}

Locking

Optimistic Locking

@Version
private Long version;

// On conflict, throws OptimisticLockException

Pessimistic Locking

// Lock mode types
User user = em.find(User.class, 1L,
    LockModeType.PESSIMISTIC_WRITE);  // Exclusive lock
// Or PESSIMISTIC_READ

// Using query
TypedQuery<User> query = em.createQuery("SELECT u FROM User u", User.class);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);

Lock Modes

ModeDescription
PESSIMISTIC_READShared lock, prevents updates
PESSIMISTIC_WRITEExclusive lock
OPTIMISTICVersion check on commit
OPTIMISTIC_FORCE_INCREMENTForce version increment

Entity Lifecycle

States

new -> (persist) -> managed
     -> (merge)  -> managed

managed -> (remove) -> removed
        -> (clear/detach) -> detached

detached -> (merge) -> managed
         -> (remove) -> removed

Lifecycle Callbacks

@Entity
public class User {
    @PrePersist
    public void beforeInsert() {
        createdAt = LocalDateTime.now();
    }

    @PostPersist
    public void afterInsert() {
        System.out.println("User created: " + id);
    }

    @PreUpdate
    public void beforeUpdate() {
        updatedAt = LocalDateTime.now();
    }

    @PostUpdate
    public void afterUpdate() {
        System.out.println("User updated: " + id);
    }

    @PreRemove
    public void beforeDelete() {
        System.out.println("Deleting user");
    }

    @PostRemove
    public void afterDelete() {
        System.out.println("User deleted");
    }

    @PostLoad
    public void afterLoad() {
        System.out.println("User loaded");
    }
}

Callbacks and Listeners

Entity Listeners

public class AuditListener {
    @PrePersist
    public void prePersist(Object entity) {
        if (entity instanceof Auditable) {
            ((Auditable) entity).setCreatedAt(Instant.now());
        }
    }
}

@Entity
@EntityListeners(AuditListener.class)
public class User implements Auditable {
    private Instant createdAt;
}

Interface-based Callbacks

public interface Auditable {
    Instant getCreatedAt();
    void setCreatedAt(Instant createdAt);
}

Performance

Batch Insert/Update

// properties
<property name="hibernate.jdbc.batch_size" value="50"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.order_updates" value="true"/>

// Code
for (int i = 0; i < users.size(); i++) {
    em.persist(users.get(i));
    if (i % 50 == 0) {
        em.flush();
        em.clear();
    }
}

@BatchSize

@OneToMany(mappedBy = "user")
@BatchSize(size = 25)
private List<Order> orders;

Statistics

<property name="hibernate.generate_statistics" value="true"/>

SessionFactory sf = emf.unwrap(SessionFactory.class);
Statistics stats = sf.getStatistics();
stats.setStatisticsEnabled(true);

Caching

First-Level Cache

// Enabled by default, scoped to EntityManager
User u1 = em.find(User.class, 1L);  // DB query
User u2 = em.find(User.class, 1L);  // Cache hit

Second-Level Cache

// Enable in properties
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.region.factory_class"
          value="org.hibernate.cache.jcache.JCacheRegionFactory"/>

// Annotate entities
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Entity
public class User { }

// Or collection
@OneToMany(mappedBy = "user")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private List<Order> orders;

Query Cache

<property name="hibernate.cache.use_query_cache" value="true"/>

Query query = em.createQuery("SELECT u FROM User u WHERE u.active = true");
query.setHint("jakarta.persistence.cache.store", "CACHE");

Spring Data JPA

Repository

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);

    List<User> findByActiveTrue();

    @Query("SELECT u FROM User u WHERE u.name LIKE %:name%")
    List<User> searchByName(@Param("name") String name);

    @Modifying
    @Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :date")
    int deactivateUsers(LocalDateTime date);
}

Custom Repository

public interface UserRepositoryCustom {
    List<User> findUsersWithOrders();
}

public class UserRepositoryImpl implements UserRepositoryCustom {
    @PersistenceContext
    private EntityManager em;

    public List<User> findUsersWithOrders() {
        // Custom implementation
    }
}

public interface UserRepository
    extends JpaRepository<User, Long>, UserRepositoryCustom { }

JPA Specifications

public class UserSpecifications {
    public static Specification<User> hasName(String name) {
        return (root, query, cb) ->
            cb.like(root.get("name"), "%" + name + "%");
    }

    public static Specification<User> isActive() {
        return (root, query, cb) -> cb.isTrue(root.get("active"));
    }
}

// Usage
List<User> users = userRepository.findAll(
    UserSpecifications.hasName("John")
        .and(UserSpecifications.isActive())
);

Best Practices

Use Derived Queries Wisely

// Avoid complex queries with method names
// Use @Query for complex cases

Use Projections

// For read-only DTOs
@Query("SELECT new UserDTO(u.name, u.email) FROM User u")
List<UserDTO> findAllUserDTOs();

Avoid N+1 Queries

// Use JOIN FETCH
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders();

// Or use EntityGraph
@NamedEntityGraph(name = "User.withOrders",
    attributeNodes = @NamedAttributeNode("orders"))

Close EntityManager

try {
    EntityManager em = emf.createEntityManager();
    // Use em
} finally {
    em.close();
}

// Or use @PersistenceContext with CDI (container manages lifecycle)

Next Steps

Now that you know Hibernate fundamentals:

  • Learn Spring Data JPA for simplified data access
  • Explore QueryDSL for type-safe queries
  • Study database-specific features
  • Learn about data audit with Envers
  • Explore performance tuning techniques

References

On this page

Table of contentsWhat is JPA and Hibernate?JPA (Java Persistence API)HibernateKey DifferencesWhy Hibernate?1. Industry Standard2. Productivity3. Database Agnostic4. Performance5. CommunityInstallation and SetupMaven Dependenciespersistence.xmlProgrammatic SetupEntities@Entity and @Table@Id and @GeneratedValueField vs Property AccessPrimary KeysAuto-GeneratedAssigned KeysComposite KeysBasic Mappings@Column@Temporal@Enumerated@Lob@Transient@FormulaEntity ManagerObtaining EntityManagerBasic OperationsEntityTransactionCRUD OperationsCreateReadUpdateDeleteJPQLBasic QueriesProjectionsJOINsAggregate FunctionsGROUP BY and HAVINGSubqueriesCase ExpressionsCriteria APIBasic QueriesWHERE ClausePredicatesProjectionsJoinsORDER BYGroup ByMetamodelNative QueriesBasic Native QueryWith ParametersScalar ResultsNamed Native QueriesResult Set MappingRelationships@OneToOne@OneToMany@ManyToOne@ManyToManyCascade TypesCascade OptionsExampleFetching StrategiesEager vs Lazy@Fetch(FetchMode)Entity GraphInheritance Mapping@InheritanceSingle Table (Default)Joined TableTransactionsContainer-Managed (CDI)Application-ManagedREQUIRED vs REQUIRES_NEWLockingOptimistic LockingPessimistic LockingLock ModesEntity LifecycleStatesLifecycle CallbacksCallbacks and ListenersEntity ListenersInterface-based CallbacksPerformanceBatch Insert/Update@BatchSizeStatisticsCachingFirst-Level CacheSecond-Level CacheQuery CacheSpring Data JPARepositoryCustom RepositoryJPA SpecificationsBest PracticesUse Derived Queries WiselyUse ProjectionsAvoid N+1 QueriesClose EntityManagerNext StepsReferences