Spring Data JPA

·Cheat Sheet
JPA / JakartaHibernate ORMSpring Data

Drop this file into your Next.js app/ directory as page.js — uses Tailwind + JetBrains Mono from Google Fonts.

History
Raw JDBCManual SQL, connection mgmt, field mapping
HibernateORM — auto-map tables; write Java not SQL
JPA SpecStandardised ORM API (Jakarta Persistence)
Spring Data JPAConvenience layer; Repository interfaces auto-generate CRUD
Architecture Stack
·Your CodeEntity classes, Repository calls
Spring Data JPARepository interfaces, auto-generated CRUD
JPA / JakartaEntityManager, Persistence Context, JPQL
HibernateJPA impl, Session, HQL, caching
JDBC / DBRaw SQL, connection pool
JPA vs Hibernate
JPA
Portability · Standard API · Vendor-independent · EntityManager · JPQL
Hibernate
Advanced caching · Multi-tenancy · Batch · Session · HQL · more features
Entity lifecycle: New → Managed → Detached → Removed
Entity Annotations
@Entity                          // class = DB table
@Table(name="employees",
       uniqueConstraints=@UniqueConstraint(
         columnNames={"email"}))
public class Employee {

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

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

  @Column(unique=true)
  private String email;

  @Enumerated(EnumType.STRING)   // "SENIOR" not 2
  private Level level;

  @Embedded                      // inline Address cols
  private Address address;

  @Column(updatable=false)       // written once only
  private LocalDateTime createdAt;

  @Transient                     // NOT stored in DB
  private String displayLabel;

  @Lob @Column(columnDefinition="TEXT")
  private String bio;            // CLOB/BLOB
}
@GeneratedValue Strategies
IDENTITYDB auto-increment (MySQL)
SEQUENCEUses a DB sequence object
TABLEDedicated table tracks next val
AUTOJPA picks per DB
@Embeddable / @Embedded
@Embeddable
public class Address {
  private String street, city, zip;
}  // cols inlined into parent table

// Embed twice → rename cols with:
@Embedded
@AttributeOverrides({
  @AttributeOverride(name="street",
    column=@Column(name="home_street")),
  @AttributeOverride(name="city",
    column=@Column(name="home_city"))
})
private Address homeAddress;
Validation Annotations
@NotNull@NotBlank@Email@Positive@Min(0)@Size(max=255)@Pattern
Relationships
@OneToOne
class User {
  @OneToOne(mappedBy="user", cascade=CascadeType.ALL)
  private Profile profile;  // no FK here
}
class Profile {
  @OneToOne
  @JoinColumn(name="user_id")  // FK lives here
  private User user;
}
@OneToMany / @ManyToOne
class User {
  @OneToMany(mappedBy="user")
  private List<Order> orders;
}
class Order {
  @ManyToOne               // owns FK
  @JoinColumn(name="user_id")
  private User user;
}
@ManyToMany — pivot table
class Student {
  @ManyToMany  // Owner — controls join table
  @JoinTable(name="student_course",
    joinColumns=@JoinColumn(name="student_id"),
    inverseJoinColumns=@JoinColumn(name="course_id"))
  private Set<Course> courses = new HashSet<>();
}
class Course {
  @ManyToMany(mappedBy="courses")  // Observer
  private Set<Student> students;
}
mappedBy"I don't own FK — look at the other side"
@JoinColumnThis side owns the FK column
No mappedByJPA creates two FK columns (bad!)
BidirectionalAlways update BOTH sides in Java code
Cascade & Fetch
PERSISTSave parent → auto-save children
MERGEUpdate parent → auto-update children
REMOVEDelete parent → auto-delete children
REFRESHResync parent → resync children from DB
DETACHDetach parent → detach children
ALLAll above — strong parent-child
orphanRemoval=true → removed from collection = DELETE
LAZYdefault collections. Load on access.
EAGERdefault single refs. Always loaded.
N+1 Problem
1 query fetches N parents → then N separate queries fire to load each parent's children (LAZY access in a loop). Total: N+1 queries instead of 1 or 2. Destroys performance at scale.
Fixes ↓
// Fix 1: JOIN FETCH in JPQL
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();

// Fix 2: EntityGraph (Spring Data)
@EntityGraph(attributePaths={"books"})
List<Author> findAll();

// Fix 3: Batch for large sets
@BatchSize(size=50) private List<Book> books;
Rule: default all LAZY, JOIN FETCH only when needed
Repository Hierarchy
·RepositoryMarker interface
CrudRepositoryBasic CRUD
PagingAndSortingRepository+ Pagination & Sorting
JpaRepository+ JPA-specific (most used)
JpaSpecificationExecutor+ Dynamic filtering
All JpaRepository Methods
// SAVE
save(entity)           // INSERT if id null, else UPDATE
saveAll(iterable)      // bulk
saveAndFlush(entity)   // save + immediate DB sync

// FIND
findById(id)           // Optional<T> — hits DB now
getReferenceById(id)   // lazy proxy, SELECT on access
findAll()              // ⚠️ no Pageable = full table scan
findAllById(ids)       // WHERE id IN (...)
findAll(pageable)      // paginated
findAll(sort)          // sorted

// CHECK
existsById(id)         // boolean
count()                // SELECT COUNT(*)

// DELETE
deleteById(id)
delete(entity)
deleteAll()            // ⚠️ clears whole table
deleteAllInBatch()     // single DELETE — faster
flush()                // sync context to DB
getReferenceById → throws EntityNotFoundException on access if missing
Derived Query Methods
// Numeric
List<P> findByPriceLessThan(double p);
List<P> findByPriceBetween(double min, double max);
List<P> findByStockGreaterThanAndCategoryIn(
              int s, Collection<String> cats);

// String
List<P> findByNameIgnoreCase(String name);
List<P> findByNameContainingIgnoreCase(String kw);
List<P> findByNameStartingWith(String prefix);

// Null / Boolean / Enum
List<P> findByDescriptionIsNull();
List<P> findByActiveTrue();
List<P> findByStatusIn(Collection<Status> s);

// Date
List<P> findByCreatedAtAfter(LocalDateTime d);
List<P> findByCreatedAtBetween(LocalDateTime f, LocalDateTime t);

// Limiting
Optional<P> findFirstByOrderByPriceAsc();
List<P>     findTop5ByOrderByRatingDesc();

// Count / Exists / Delete
long    countByCategory(String cat);
boolean existsByEmail(String email);
long    deleteByCreatedAtBefore(LocalDateTime d);
@Query — JPQL & Native
JPQL (entity class + field names, DB-agnostic)
@Query("SELECT u FROM User u WHERE u.email=:e")
Optional<User> byEmail(@Param("e") String e);

// JOIN FETCH — solves N+1
@Query("SELECT p FROM Post p JOIN FETCH p.comments WHERE p.author.id=:id")
List<Post> withComments(@Param("id") Long id);

// Aggregate
@Query("SELECT AVG(p.price) FROM Product p WHERE p.category=:c")
Double avgPrice(@Param("c") String c);

// UPDATE/DELETE — @Modifying required
@Modifying @Transactional
@Query("UPDATE User u SET u.active=false WHERE u.lastLogin<:d")
int deactivateOld(@Param("d") LocalDate d);
Native SQL (DB-specific features)
@Query(value="SELECT * FROM users WHERE email LIKE %:d",
       nativeQuery=true)
List<User> byDomain(@Param("d") String d);

// Native + Pagination needs countQuery
@Query(value="SELECT * FROM products ORDER BY price DESC",
       countQuery="SELECT COUNT(*) FROM products",
       nativeQuery=true)
Page<Product> allNative(Pageable p);
Projections
Interface Projection
public interface UserSummary {
  String getName();
  String getEmail();
  @Value("#{target.firstName+' '+target.lastName}")
  String getFullName();  // computed field
}
List<UserSummary> findByActive(boolean a);
DTO / Record Projection
public record UserDTO(String name, String email, long cnt) {}

@Query("""
  SELECT new com.example.UserDTO(u.name, u.email, COUNT(p))
  FROM User u LEFT JOIN u.posts p GROUP BY u.id""")
List<UserDTO> findUserSummaries();
Dynamic — caller picks shape
<T> List<T> findByActive(boolean a, Class<T> type);

repo.findByActive(true, User.class);        // full
repo.findByActive(true, UserSummary.class); // slim
repo.findByActive(true, UserDTO.class);     // dto
Native + Interface Projection
// alias must match getter name (camelCase)
@Query(value="SELECT p.name, p.category_id AS categoryId FROM products p",
       nativeQuery=true)
List<ProductSummary> findActiveSummaries();
Pagination & Sorting
// Repository — add Pageable param
Page<User>  findByActive(boolean a, Pageable p);
Slice<User> findByDept(String d, Pageable p);

// Build Pageable (0-based pages!)
PageRequest.of(0, 10);
PageRequest.of(2, 10, Sort.by("name"));

// Page<T> metadata
page.getContent()        // List<T>
page.getNumber()         // current page (0-based)
page.getTotalElements()  // total DB rows
page.getTotalPages()     // ceil(total/size)
page.isFirst() / isLast() / hasNext()
page.map(UserDTO::fromEntity) // transform in-place

// Sort patterns
Sort.by("name")                    // ASC default
Sort.by(Direction.DESC, "name")
Sort.by("dept").ascending()
    .and(Sort.by("salary").descending())  // chained
// Fine-grained
Sort.by(
  Sort.Order.asc("lastName").nullsLast(),
  Sort.Order.desc("salary").nullsFirst(),
  Sort.Order.asc("name").ignoreCase()
)

// REST controller pattern
@GetMapping("/users")
public Page<UserDTO> list(
  @RequestParam(defaultValue="0")  int page,
  @RequestParam(defaultValue="10") int size,
  @RequestParam(defaultValue="id") String sort) {
  return repo.findAll(PageRequest.of(page, size,
    Sort.by(sort))).map(UserDTO::fromEntity);
}
Page2 queries (data + COUNT)
Slice1 query, hasNext only
⚠️ Always whitelist sort fields from HTTP params
Specifications — Dynamic Filtering
Repo extends JpaSpecificationExecutor<Employee>
// 1. Specification class
public class EmployeeSpec {
  public static Specification<Employee>
  hasDepartment(String dept) {
    return (root, query, cb) ->
      dept == null ? null
      : cb.equal(root.get("department"), dept);
  }

  public static Specification<Employee>
  salaryBetween(Double min, Double max) {
    return (root, query, cb) ->
      cb.between(root.get("salary"), min, max);
  }
}

// 2. Service — compose specs
Specification<Employee> spec =
  Specification.where(EmployeeSpec.hasDepartment(dept))
               .and(EmployeeSpec.salaryBetween(min, max));
return repo.findAll(spec, pageable);
(root, query, cb)
rootEntry point. root.get("field"), root.join("rel")
queryStructure. query.distinct(true), orderBy(…)
cbPredicates. cb.equal, cb.like, cb.between, cb.and, cb.or
Enables: /employees?department=IT&minSalary=50000
@Transactional — How it Works
Spring AOP proxy intercepts the call:
try {
  txManager.begin();        // 1. START TX
  yourMethod();             // 2. run code
  persistenceCtx.flush();  // 3. sync dirty entities
  txManager.commit();       // 4. COMMIT
} catch (RuntimeException e) {
  txManager.rollback();     // 5. ROLLBACK
  throw e;
}

// ⚠️ Checked exceptions do NOT rollback by default!
@Transactional(rollbackFor=Exception.class)

// ⚠️ Self-invocation BREAKS @Transactional
public void outer() {
  inner();  // this.inner() — bypasses proxy!
}  // Fix: inject self, or split to two beans
@Transactional(
  propagation  = Propagation.REQUIRED,
  isolation    = Isolation.READ_COMMITTED,
  readOnly     = false,
  timeout      = 30,
  rollbackFor  = {CustomException.class},
  noRollbackFor= {BusinessWarning.class}
)
Propagation Types
REQUIREDJoin existing TX; create if none (default)
REQUIRES_NEWAlways new TX; commits independently — audit logs
MANDATORYMust have existing TX; throws if not
NEVERThrows if inside an existing TX
SUPPORTSUses TX if present; runs without if not
NOT_SUPPORTEDSuspends active TX; runs without one
readOnly = true Optimisations
Dirty CheckingDisabled — no snapshots or field comparisons
Flush ModeSet to NEVER — no flush overhead
JDBC DriverCan auto-route to read replicas (MySQL)
DB Undo LogSkipped in PostgreSQL/Oracle — lower lock contention
Best Practice — readOnly Pattern
@Service
@Transactional(readOnly=true)  // class default: reads
public class ProductService {

  // Inherits readOnly=true ✅
  public Page<Product> findAll(Pageable p) {
    return repo.findAll(p);
  }

  // Override for writes ✅
  @Transactional  // readOnly=false
  public Product save(Product p) {
    return repo.save(p);
  }

  @Transactional
  public Product update(Long id, ProductDTO dto) {
    Product p = repo.findById(id).orElseThrow();
    p.setName(dto.getName());
    return p; // no save()! dirty check auto-flushes
  }
}
Dirty Checking
Managed entities inside a TX are tracked. Field changes auto-flush on commit — no explicit save() needed for updates.
Spring Data JPA · Always LAZY by default, JOIN FETCH when needed · readOnly=true on class, override for writes · Never findAll() without Pageable in production