원클릭으로
jpa-patterns
JPA/Hibernate optimization patterns, entity design, and query performance for Spring Data
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
JPA/Hibernate optimization patterns, entity design, and query performance for Spring Data
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when implementing Spring Boot 3.x applications. Provides hands-on implementation patterns for web, data, security, testing, and cloud-native development.
Review REST API contracts for HTTP semantics, versioning, backward compatibility, and response consistency. Use when user asks "review API", "check endpoints", "REST review", or before releasing API changes.
Use when designing Spring Boot 3.x application architecture, making technology decisions, or planning project structure. Provides architectural patterns and references for project setup, JPA, security, testing, and reactive programming.
Use when designing, reviewing, or modifying REST API endpoints to enforce proper HTTP verbs, status codes, versioning, and API contract best practices.
Use when reviewing or writing code to enforce DRY, KISS, and YAGNI principles with Java-specific guidance.
Use when implementing or reviewing code that benefits from established design patterns like Builder, Factory, Strategy, Observer, Decorator, or Adapter.
| name | jpa-patterns |
| description | JPA/Hibernate optimization patterns, entity design, and query performance for Spring Data |
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@CreatedDate
@Column(updatable = false)
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
@Version
private Long version;
// getters only -- immutable fields
}
@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
@Column(nullable = false)
private String customerId;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status = OrderStatus.DRAFT;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalAmount = BigDecimal.ZERO;
// Proper bidirectional relationship management
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
recalculateTotal();
}
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
recalculateTotal();
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// equals/hashCode using business key or ID
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order other)) return false;
return getId() != null && getId().equals(other.getId());
}
@Override
public int hashCode() {
return getClass().hashCode(); // constant for JPA entities
}
}
@Entity
@Table(name = "order_items")
public class OrderItem extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "order_id")
private Order order;
@Column(nullable = false)
private String productId;
@Column(nullable = false)
private int quantity;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice;
public BigDecimal getSubtotal() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}
// BAD: N+1 -- loads orders, then items for each order
List<Order> orders = orderRepository.findAll();
orders.forEach(o -> o.getItems().size()); // triggers N queries
// GOOD: Fetch join in repository
public interface OrderRepository extends JpaRepository<Order, String> {
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") OrderStatus status);
// EntityGraph alternative
@EntityGraph(attributePaths = {"items"})
List<Order> findByStatus(OrderStatus status);
}
// Interface projection -- fetches only needed columns
public interface OrderSummary {
String getId();
String getCustomerId();
OrderStatus getStatus();
BigDecimal getTotalAmount();
Instant getCreatedAt();
}
public interface OrderRepository extends JpaRepository<Order, String> {
List<OrderSummary> findSummaryByCustomerId(String customerId);
// DTO projection with constructor expression
@Query("""
SELECT new com.app.dto.OrderStatsDto(
o.status,
COUNT(o),
SUM(o.totalAmount)
)
FROM Order o
WHERE o.createdAt >= :since
GROUP BY o.status
""")
List<OrderStatsDto> getOrderStats(@Param("since") Instant since);
}
// Always paginate list endpoints
@GetMapping
public ResponseEntity<Page<OrderDto>> listOrders(
@RequestParam(required = false) OrderStatus status,
@ParameterObject Pageable pageable) {
var orders = orderRepository.findByStatus(status, pageable);
return ResponseEntity.ok(orders.map(orderMapper::toDto));
}
// Custom pageable defaults
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
var resolver = new PageableHandlerMethodArgumentResolver();
resolver.setMaxPageSize(100);
resolver.setFallbackPageable(PageRequest.of(0, 20, Sort.by("createdAt").descending()));
resolvers.add(resolver);
}
}
// Batch inserts with proper configuration
// application.yml:
// spring.jpa.properties.hibernate.jdbc.batch_size: 50
// spring.jpa.properties.hibernate.order_inserts: true
// spring.jpa.properties.hibernate.order_updates: true
@Transactional
public void importProducts(List<CreateProductRequest> requests) {
var batchSize = 50;
var products = new ArrayList<Product>(batchSize);
for (int i = 0; i < requests.size(); i++) {
products.add(productMapper.toEntity(requests.get(i)));
if (products.size() >= batchSize) {
productRepository.saveAll(products);
entityManager.flush();
entityManager.clear();
products.clear();
}
}
if (!products.isEmpty()) {
productRepository.saveAll(products);
}
}
public class OrderSpecifications {
public static Specification<Order> byCustomer(String customerId) {
return (root, query, cb) ->
customerId == null ? null : cb.equal(root.get("customerId"), customerId);
}
public static Specification<Order> byStatus(OrderStatus status) {
return (root, query, cb) ->
status == null ? null : cb.equal(root.get("status"), status);
}
public static Specification<Order> createdAfter(Instant after) {
return (root, query, cb) ->
after == null ? null : cb.greaterThanOrEqualTo(root.get("createdAt"), after);
}
public static Specification<Order> amountBetween(BigDecimal min, BigDecimal max) {
return (root, query, cb) -> {
if (min == null && max == null) return null;
if (min != null && max != null) return cb.between(root.get("totalAmount"), min, max);
if (min != null) return cb.greaterThanOrEqualTo(root.get("totalAmount"), min);
return cb.lessThanOrEqualTo(root.get("totalAmount"), max);
};
}
}
// Usage in service
public Page<OrderDto> searchOrders(OrderSearchRequest request, Pageable pageable) {
var spec = Specification.where(byCustomer(request.customerId()))
.and(byStatus(request.status()))
.and(createdAfter(request.fromDate()))
.and(amountBetween(request.minAmount(), request.maxAmount()));
return orderRepository.findAll(spec, pageable).map(orderMapper::toDto);
}
FetchType.LAZY for all @ManyToOne and @OneToManyJOIN FETCH or @EntityGraph to load related data in one query@Index annotations for frequently queried columns@Transactional(readOnly = true) for queries