Spring Boot Cheatsheet
Spring Data JPA
Use this Spring Boot reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Entity Basics
@Entity @Table(name = "products") @Getter @Setter // Lombok public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // auto-increment (DB-level) private Long id; @Column(name = "product_name", nullable = false, length = 255) private String name; @Column(unique = true) private String sku; @Column(precision = 10, scale = 2) private BigDecimal price; @Enumerated(EnumType.STRING) // store enum name, not ordinal private Status status; @Lob private String description; // large text @CreationTimestamp private Instant createdAt; @UpdateTimestamp private Instant updatedAt; @Version private Long version; // optimistic locking }
@GeneratedValue Strategies
| Strategy | When to Use |
|---|---|
IDENTITY | MySQL, PostgreSQL auto-increment |
SEQUENCE | PostgreSQL sequences (better batching) |
UUID | Globally unique IDs |
AUTO | JPA picks (avoid — behavior varies) |
// UUID primary key @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id;
Relationships
@ManyToOne / @OneToMany
@Entity public class Order { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "customer_id", nullable = false) private Customer customer; @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) private List<OrderItem> items = new ArrayList<>(); }
Always use
FetchType.LAZYfor@ManyToOne— EAGER is the JPA default and causes N+1 problems.
@ManyToMany
@Entity public class Post { @ManyToMany @JoinTable( name = "post_tags", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private Set<Tag> tags = new HashSet<>(); }
@OneToOne
@Entity public class UserProfile { @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; }
Cascade Types
| Type | Effect |
|---|---|
PERSIST | Save child with parent |
MERGE | Merge child with parent |
REMOVE | Delete child with parent |
REFRESH | Refresh child with parent |
ALL | All of the above |
DETACH | Detach child with parent |
Repository Interfaces
// Basic CRUD public interface ProductRepository extends JpaRepository<Product, Long> {} // With findBy / deleteBy / countBy / existsBy public interface ProductRepository extends JpaRepository<Product, Long> { List<Product> findByStatus(Status status); Optional<Product> findBySku(String sku); boolean existsBySku(String sku); long countByStatus(Status status); void deleteByStatus(Status status); // Derived query keywords: And, Or, Between, LessThan, GreaterThan, // Like, Containing, StartingWith, EndingWith, In, NotIn, // IsNull, IsNotNull, OrderBy, Asc, Desc List<Product> findByPriceBetweenAndStatusOrderByPriceAsc( BigDecimal min, BigDecimal max, Status status); // Limit results List<Product> findTop5ByStatusOrderByCreatedAtDesc(Status status); Optional<Product> findFirstBySkuStartingWith(String prefix); // Pagination Page<Product> findByStatus(Status status, Pageable pageable); Slice<Product> findByCategory(String category, Pageable pageable); // no total count // Projections (interface) List<ProductSummary> findByStatus(Status status, Class<ProductSummary> type); }
JPQL and Native Queries
// JPQL (entity-based, portable) @Query("SELECT p FROM Product p WHERE p.price < :maxPrice AND p.status = :status") List<Product> findCheapByStatus(@Param("maxPrice") BigDecimal maxPrice, @Param("status") Status status); // Native SQL @Query(value = "SELECT * FROM products WHERE LOWER(name) LIKE LOWER(:pattern)", nativeQuery = true) List<Product> searchByNameNative(@Param("pattern") String pattern); // Modifying query @Modifying @Transactional @Query("UPDATE Product p SET p.status = :status WHERE p.id IN :ids") int bulkUpdateStatus(@Param("status") Status status, @Param("ids") List<Long> ids); // Dynamic count query for pagination @Query(value = "SELECT p FROM Product p WHERE p.category = :cat", countQuery = "SELECT COUNT(p) FROM Product p WHERE p.category = :cat") Page<Product> findByCategory(@Param("cat") String cat, Pageable pageable);
Specifications (Dynamic Queries)
public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {}
public class ProductSpecs { public static Specification<Product> hasStatus(Status status) { return (root, query, cb) -> cb.equal(root.get("status"), status); } public static Specification<Product> nameLike(String pattern) { return (root, query, cb) -> cb.like(cb.lower(root.get("name")), "%" + pattern.toLowerCase() + "%"); } } // Usage List<Product> results = repository.findAll( Specification.where(ProductSpecs.hasStatus(Status.ACTIVE)) .and(ProductSpecs.nameLike("spring")));
Projections
// Interface projection — Spring generates proxy at runtime public interface ProductSummary { Long getId(); String getName(); BigDecimal getPrice(); } List<ProductSummary> summaries = repository.findByStatus(Status.ACTIVE, ProductSummary.class); // DTO projection — constructor expression @Query("SELECT new com.example.dto.ProductDto(p.id, p.name, p.price) FROM Product p") List<ProductDto> findAllDtos(); // Record DTO (Spring Data 3+) public record ProductDto(Long id, String name, BigDecimal price) {} @Query("SELECT new com.example.dto.ProductDto(p.id, p.name, p.price) FROM Product p") List<ProductDto> findAllDtos();
Transactions
// Service layer — place @Transactional here, never on repository @Service @Transactional(readOnly = true) // default for reads (performance hint) public class ProductService { public ProductDto findById(Long id) { ... } // uses readOnly tx @Transactional // overrides class-level for writes public ProductDto create(CreateProductRequest req) { ... } @Transactional( propagation = Propagation.REQUIRES_NEW, // new tx regardless isolation = Isolation.READ_COMMITTED, timeout = 5, // seconds rollbackFor = {IOException.class} ) public void importBatch(List<CreateProductRequest> items) { ... } }
Propagation Types
| Propagation | Behavior |
|---|---|
REQUIRED | Join existing or create new (default) |
REQUIRES_NEW | Always create a new, suspend current |
SUPPORTS | Join if exists, non-transactional otherwise |
NOT_SUPPORTED | Suspend current, run non-transactional |
MANDATORY | Must have existing tx or throws |
NEVER | Must NOT have tx or throws |
NESTED | Savepoint inside existing tx |
Auditing
// Enable in @Configuration @EnableJpaAuditing // Entity mixin or base class @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class Auditable { @CreatedDate private Instant createdAt; @LastModifiedDate private Instant updatedAt; @CreatedBy private String createdBy; @LastModifiedBy private String updatedBy; } // Provide the current user @Bean public AuditorAware<String> auditorProvider() { return () -> Optional.ofNullable(SecurityContextHolder.getContext()) .map(ctx -> ctx.getAuthentication()) .filter(a -> a != null && a.isAuthenticated()) .map(Authentication::getName); }
application.properties JPA Settings
# DDL (create, create-drop, validate, update, none) spring.jpa.hibernate.ddl-auto=validate # use Flyway/Liquibase in prod # Show SQL spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # Dialect (usually auto-detected) spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect # Batch inserts spring.jpa.properties.hibernate.jdbc.batch_size=50 spring.jpa.properties.hibernate.order_inserts=true spring.jpa.properties.hibernate.order_updates=true # Open Session in View (disable for REST APIs) spring.jpa.open-in-view=false
Common Pitfalls
- N+1 queries — use
@EntityGraph,JOIN FETCHin JPQL, or@BatchSizeto avoid per-entity lazy loads. LazyInitializationException— accessing a lazy relation outside a transaction; fix by fetching eagerly in the query or expanding the transaction boundary.detached entity passed to persist— passing an entity with a set ID tosave()when the entity is not managed; either merge it or don't set the ID.open-in-view=true(default) — keeps the Hibernate session open for the entire HTTP request (through the view rendering); disable it in REST APIs.@Transactionalon the same class — self-invocation bypasses the proxy; calling a@Transactionalmethod from within the same bean won't start a transaction. Inject the bean into itself or useAopContext.currentProxy().@GeneratedValue(AUTO)with sequences — Hibernate may allocate IDs in large batches (e.g., 50) causing apparent gaps; useSEQUENCEwith a named sequence to control allocation size.