sql-jpa
Write optimized JPA queries, JPQL, Spring Data specifications, and raw SQL for Spring Boot applications. Use this skill whenever the user needs to write database queries, optimize slow queries, design
Files1
View on GitHubSKILL.mdskills/sql-jpa/SKILL.md
GitHubSQL & JPA Queries Skill
Write correct, efficient JPA mappings, JPQL queries, and Spring Data patterns for Spring Boot applications.
Entity Relationship Mapping
One-to-Many (most common)
// Parent (One side)
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
// Helper methods to maintain bidirectional consistency
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
}
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
}
}
// Child (Many side)
@Entity
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // Always LAZY on @ManyToOne
@JoinColumn(name = "order_id", nullable = false)
private Order order;
}
Many-to-Many
@Entity
public class Student {
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
Fetch Strategy Rules
@ManyToOne→ alwaysFetchType.LAZY(EAGER causes performance issues)@OneToMany→ default is LAZY, keep it@ManyToMany→ always LAZY- Load eagerly only via
JOIN FETCHin specific queries where needed
Spring Data Repository Patterns
Derived Query Methods
// Simple conditions
Optional<User> findByEmail(String email);
List<User> findByStatus(UserStatus status);
boolean existsByEmail(String email);
long countByStatus(UserStatus status);
// Combined conditions
List<User> findByStatusAndCreatedAtAfter(UserStatus status, LocalDateTime date);
List<User> findByNameContainingIgnoreCase(String name);
// Ordering
List<Product> findByCategory_IdOrderByPriceAsc(Long categoryId);
// Pagination
Page<Product> findByStatus(ProductStatus status, Pageable pageable);
JPQL Queries
// Fetch join to avoid N+1
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.customer.id = :customerId")
List<Order> findByCustomerWithItems(@Param("customerId") Long customerId);
// Projection (DTO constructor)
@Query("SELECT new com.example.dto.OrderSummary(o.id, o.total, o.status, c.name) " +
"FROM Order o JOIN o.customer c WHERE o.status = :status")
List<OrderSummary> findOrderSummaries(@Param("status") OrderStatus status);
// Count with condition
@Query("SELECT COUNT(o) FROM Order o WHERE o.customer.id = :customerId AND o.status = 'PENDING'")
long countPendingOrdersByCustomer(@Param("customerId") Long customerId);
// Exists check (more efficient than findBy)
@Query("SELECT CASE WHEN COUNT(u) > 0 THEN true ELSE false END FROM User u WHERE u.email = :email")
boolean existsByEmailCustom(@Param("email") String email);
Native SQL (when JPQL isn't enough)
@Query(value = """
SELECT p.*, c.name as category_name
FROM products p
INNER JOIN categories c ON p.category_id = c.id
WHERE p.price BETWEEN :minPrice AND :maxPrice
ORDER BY p.price ASC
LIMIT :limit
""", nativeQuery = true)
List<Object[]> findProductsInPriceRange(
@Param("minPrice") BigDecimal min,
@Param("maxPrice") BigDecimal max,
@Param("limit") int limit);
Spring Data Specifications (Dynamic Filtering)
Best pattern for complex, dynamic queries:
// Specification class
public class ProductSpecification {
public static Specification<Product> hasCategory(Long categoryId) {
return (root, query, cb) ->
categoryId == null ? null : cb.equal(root.get("category").get("id"), categoryId);
}
public static Specification<Product> priceBetween(BigDecimal min, BigDecimal max) {
return (root, query, cb) -> {
if (min == null && max == null) return null;
if (min == null) return cb.lessThanOrEqualTo(root.get("price"), max);
if (max == null) return cb.greaterThanOrEqualTo(root.get("price"), min);
return cb.between(root.get("price"), min, max);
};
}
public static Specification<Product> nameContains(String name) {
return (root, query, cb) ->
name == null ? null : cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
}
// Repository — extend JpaSpecificationExecutor
public interface ProductRepository extends JpaRepository<Product, Long>,
JpaSpecificationExecutor<Product> { }
// Usage in service
public Page<ProductResponse> search(ProductFilterRequest filter, Pageable pageable) {
Specification<Product> spec = Specification
.where(ProductSpecification.hasCategory(filter.getCategoryId()))
.and(ProductSpecification.priceBetween(filter.getMinPrice(), filter.getMaxPrice()))
.and(ProductSpecification.nameContains(filter.getName()));
return productRepository.findAll(spec, pageable).map(mapper::toResponse);
}
Solving N+1 Problems
Detect: Multiple identical queries in Hibernate logs (spring.jpa.show-sql=true)
Fix Strategy:
// ❌ N+1: loads order items in a loop
List<Order> orders = orderRepository.findAll();
orders.forEach(o -> o.getItems().size()); // N queries
// ✅ Option 1: JOIN FETCH for small datasets
@Query("SELECT DISTINCT o FROM Order o LEFT JOIN FETCH o.items")
List<Order> findAllWithItems();
// ✅ Option 2: @EntityGraph for repository methods
@EntityGraph(attributePaths = {"items", "customer"})
List<Order> findByStatus(OrderStatus status);
// ✅ Option 3: Batch loading (for large datasets)
@BatchSize(size = 30)
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
Pagination with Fetch Join (Common Pitfall)
// ❌ Hibernate warning: HHH90003004 - paginates in memory
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.items")
Page<Order> findAllWithItems(Pageable pageable);
// ✅ Correct approach: two queries
@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.status = :status",
countQuery = "SELECT COUNT(o) FROM Order o WHERE o.status = :status")
Page<Order> findByStatusWithItems(@Param("status") OrderStatus status, Pageable pageable);
Useful Hibernate/JPA Configuration
spring:
jpa:
show-sql: true # Dev only
properties:
hibernate:
format_sql: true # Pretty print SQL
generate_statistics: true # Query performance stats
default_batch_fetch_size: 30 # Global batch loading
open-in-view: false # Disable OSIV (anti-pattern in APIs)