All skills
Skillintermediate

JPA Optimization

```java package com.example.domain.model;

Claude Code Knowledge Pack7/10/2026

Overview

JPA Optimization

Optimized Entity Design

package com.example.domain.model;

@Entity
@Table(
    name = "users",
    indexes = {
        @Index(name = "idx_email", columnList = "email"),
        @Index(name = "idx_created_at", columnList = "created_at")
    }
)
@EntityListeners(AuditingEntityListener.class)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

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

    @Column(nullable = false, unique = true, length = 100)
    private String email;

    @Column(nullable = false, length = 50)
    private String username;

    @Column(nullable = false)
    private Boolean active = true;

    @OneToMany(
        mappedBy = "user",
        cascade = CascadeType.ALL,
        orphanRemoval = true,
        fetch = FetchType.LAZY
    )
    @BatchSize(size = 25)
    @Builder.Default
    private List orders = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    private Instant updatedAt;

    @Version
    private Long version;

    // Helper methods
    public void addOrder(Order order) {
        orders.add(order);
        order.setUser(this);
    }

    public void removeOrder(Order order) {
        orders.remove(order);
        order.setUser(null);
    }
}

Repository with Custom Queries

package com.example.domain.repository;

public interface UserRepository extends JpaRepository<User, Long> {

    // N+1 prevention with EntityGraph
    @EntityGraph(attributePaths = {"orders", "department"})
    @Query("SELECT u FROM User u WHERE u.id = :id")
    Optional findByIdWithOrders(@Param("id") Long id);

    // Projection for read-only queries
    @Query("""
        SELECT new com.example.application.dto.UserSummary(
            u.id, u.email, u.username, COUNT(o)
        )
        FROM User u
        LEFT JOIN u.orders o
        WHERE u.active = true
        GROUP BY u.id, u.email, u.username
        """)
    List findActiveUsersSummary();

    // Fetch join to avoid N+1
    @Query("""
        SELECT DISTINCT u FROM User u
        LEFT JOIN FETCH u.orders
        WHERE u.department.id = :deptId
        """)
    List findByDepartmentWithOrders(@Param("deptId") Long deptId);

    // Pagination with count query optimization
    @Query(
        value = "SELECT u FROM User u WHERE u.active = true",
        countQuery = "SELECT COUNT(u) FROM User u WHERE u.active = true"
    )
    Page findActiveUsers(Pageable pageable);

    // Batch update
    @Modifying
    @Query("UPDATE User u SET u.active = false WHERE u.createdAt < :date")
    int deactivateOldUsers(@Param("date") Instant date);

    // Native query for complex operations
    @Query(
        value = """
            SELECT u.* FROM users u
            WHERE u.id IN (
                SELECT DISTINCT o.user_id
                FROM orders o
                WHERE o.total > :amount
            )
            """,
        nativeQuery = true
    )
    List findUsersWithLargeOrders(@Param("amount") Double amount);
}

DTO Projections

package com.example.application.dto;

// Interface-based projection
public interface UserProjection {
    Long getId();
    String getEmail();
    String getUsername();
    DepartmentProjection getDepartment();

    interface DepartmentProjection {
        String getName();
    }
}

// Class-based projection (constructor expression)
public record UserSummary(
    Long id,
    String email,
    String username,
    Long orderCount
) {}

// Dynamic projection
public interface UserRepository extends JpaRepository<User, Long> {
     List findByActive(boolean active, Class type);
}

// Usage
List users = userRepository.findByActive(true, UserProjection.class);

Query Optimization Patterns

package com.example.application.service;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserQueryService {

    private final UserRepository userRepository;
    private final EntityManager entityManager;

    // Batch fetching
    public List findUsersWithOrders(List userIds) {
        return entityManager.createQuery("""
            SELECT DISTINCT u FROM User u
            LEFT JOIN FETCH u.orders
            WHERE u.id IN :ids
            """, User.class)
            .setParameter("ids", userIds)
            .setHint("hibernate.default_batch_fetch_size", 25)
            .getResultList();
    }

    // Pagination with total count
    public Page findUsersPaged(Pageable pageable) {
        List users = entityManager.createQuery("""
            SELECT new com.example.application.dto.UserSummary(
                u.id, u.email, u.username, COUNT(o)
            )
            FROM User u
            LEFT JOIN u.orders o
            GROUP BY u.id, u.email, u.username
            """, UserSummary.class)
            .setFirstResult((int) pageable.getOffset())
            .setMaxResults(pageable.getPageSize())
            .getResultList();

        Long total = entityManager.createQuery(
            "SELECT COUNT(DISTINCT u) FROM User u",
            Long.class
        ).getSingleResult();

        return new PageImpl<>(users, pageable, total);
    }

    // Stream large datasets
    @Transactional(readOnly = true)
    public void processLargeDataset() {
        try (Stream stream = userRepository.streamByActiveTrue()) {
            stream
                .filter(user -> user.getOrders().size() > 10)
                .forEach(this::processUser);
        }
    }

    // Criteria API for dynamic queries
    public List findUsersByCriteria(UserSearchCriteria criteria) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery query = cb.createQuery(User.class);
        Root user = query.from(User.class);

        List predicates = new ArrayList<>();

        if (criteria.email() != null) {
            predicates.add(cb.like(user.get("email"), "%" + criteria.email() + "%"));
        }
        if (criteria.active() != null) {
            predicates.add(cb.equal(user.get("active"), criteria.active()));
        }
        if (criteria.createdAfter() != null) {
            predicates.add(cb.greaterThan(user.get("createdAt"), criteria.createdAfter()));
        }

        query.where(predicates.toArray(new Predicate[0]));
        return entityManager.createQuery(query).getResultList();
    }
}

Batch Operations

@Service
@RequiredArgsConstructor
public class UserBatchService {

    private final UserRepository userRepository;
    private final EntityManager entityManager;

    @Transactional
    public void batchInsert(List users) {
        int batchSize = 50;
        for (int i = 0; i < users.size(); i++) {
            entityManager.persist(users.get(i));
            if (i % batchSize == 0 && i > 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
        entityManager.flush();
        entityManager.clear();
    }

    @Transactional
    public void batchUpdate(List users) {
        int batchSize = 50;
        for (int i = 0; i < users.size(); i++) {
            entityManager.merge(users.get(i));
            if (i % batchSize == 0 && i > 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
        entityManager.flush();
        entityManager.clear();
    }
}

Second-Level Cache Configuration

spring:
  jpa:
    properties:
      hibernate:
        cache:
          use_second_level_cache: true
          use_query_cache: true
          region:
            factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
        javax:
          cache:
            provider: org.ehcache.jsr107.EhcacheCachingProvider
            uri: classpath:ehcache.xml

<config xmlns="http://www.ehcache.org/v3">
    <cache alias="users">
        <key-type>java.lang.Long</key-type>
        <value-type>com.example.domain.model.User</value-type>
        <expiry>
            <ttl unit="minutes">10</ttl>
        </expiry>
        <resources>
            <heap unit="entries">1000</heap>
        </resources>
    </cache>
</config>

Performance Monitoring

@Component
@Aspect
@Slf4j
public class QueryPerformanceAspect {

    @Around("@annotation(org.springframework.data.jpa.repository.Query)")
    public Object logQueryPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long duration = System.currentTimeMillis() - start;
            if (duration > 1000) {
                log.warn("Slow query detected: {} took {}ms",
                    joinPoint.getSignature(), duration);
            }
        }
    }
}

Quick Reference

PatternUse Case
@EntityGraphPrevent N+1 queries
JOIN FETCHEager fetch associations
DTO ProjectionRead-only queries
@BatchSizeBatch fetch collections
@Query with paginationLarge datasets
@ModifyingBulk updates/deletes
Criteria APIDynamic queries
Second-level cacheFrequently accessed data
Stream APIProcess large results
readOnly = trueOptimization hint