Data & JPA — Cheat Sheet
Spring Boot · 4 topics. Download the PDF or the Instagram carousel and share it.
Entity Relationships
@OneToMany, @ManyToOne, and @ManyToMany map object relationships to foreign keys and join tables. Choosing the right fetch type and understanding the owning side prevents the N+1 problem and LazyInitializationException.
- ✓The owning side (has the FK column) controls what gets written to the database — always set it.
- ✓Use bidirectional helper methods (addItem/removeItem) to keep both sides in sync in memory.
- ✓LAZY is the default for collections and the safe choice — EAGER on collections causes full loads on every query.
- ✓N+1 problem: use JOIN FETCH, @EntityGraph, or hibernate.default_batch_fetch_size to batch-load associations.
- ✓orphanRemoval = true deletes child entities when removed from the parent collection.
- ✓Never use CascadeType.REMOVE on @ManyToMany — deleting one side would cascade-delete shared entities.
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.UUID)
private String id;
@ManyToOne(fetch = FetchType.LAZY) // owning side — holds customer_id FK
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
// mappedBy = field name in OrderItem that owns the relationship
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL,
orphanRemoval = true, fetch = FetchType.LAZY)
private List<OrderItem> items = new ArrayList<>();
// ✅ Helper method — keeps both sides in sync
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this); // set the owning side
}
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
}
}
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order; // owning side — holds order_id FK
@Column(nullable = false)
private String productId;
@Column(nullable = false)
private int quantity;
}Transactions
@Transactional wraps a method in a database transaction — all operations commit together or roll back together. Understanding propagation and isolation levels prevents subtle data corruption bugs in production.
- ✓@Transactional on a class applies to all public methods — methods can override with their own annotation.
- ✓Spring rolls back on RuntimeException/Error by default — use rollbackFor for checked exceptions.
- ✓Self-invocation bypasses the proxy — internal @Transactional calls from the same class have no effect.
- ✓readOnly = true is an optimization hint to the database — use it for all read-only service methods.
- ✓REQUIRES_NEW suspends the outer transaction and commits independently — essential for audit logs.
- ✓Prefer READ_COMMITTED isolation + optimistic locking (@Version) over high isolation levels in most web apps.
@Service
@Transactional // class-level default: all public methods are transactional
public class OrderService {
// Inherits class-level @Transactional
public Order createOrder(CreateOrderRequest req) {
Order order = orderRepository.save(new Order(req));
inventoryService.reserve(req.items()); // same transaction
paymentService.charge(req.paymentMethod(), order.total()); // same transaction
// If paymentService.charge() throws → entire transaction rolls back
return order;
}
// Override: read-only hint (enables flush-mode optimization)
@Transactional(readOnly = true)
public Order findById(String id) {
return orderRepository.findById(id).orElseThrow();
}
// Roll back on checked exception too
@Transactional(rollbackFor = PaymentException.class)
public void processRefund(String orderId) throws PaymentException {
// ...
}
// ❌ Self-invocation — proxy is bypassed, NO transaction started
public void methodA() {
this.methodB(); // calls target, not proxy — @Transactional ignored!
}
@Transactional
public void methodB() { /* ... */ }
}Database Migrations
Flyway manages database schema changes through versioned SQL scripts. Every schema change is a numbered migration file — repeatable, auditable, and automatically applied on startup.
- ✓Migration files are named V{version}__{description}.sql — double underscore between version and name.
- ✓Never modify an applied migration — Flyway checksums files and will fail on any change.
- ✓baseline-on-migrate = true allows Flyway to take over an existing schema without migrating from scratch.
- ✓For large production tables: add nullable column → backfill in batches → add NOT NULL — never in one migration.
- ✓Repeatable migrations (R__name.sql) re-run when their content changes — useful for stored procedures and views.
- ✓Run Flyway validate in CI to catch any tampering with already-applied migration files.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
# application.yml
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true # safe for existing DBs with no flyway history
out-of-order: false # strictly ordered (recommended)
# File naming convention:
# V{version}__{description}.sql — versioned (applied once, never re-run)
# R__{description}.sql — repeatable (re-runs when checksum changes)
# U{version}__{description}.sql — undo (Enterprise edition)
# Migration files:
src/main/resources/db/migration/
├── V1__create_users_table.sql
├── V2__create_orders_table.sql
├── V3__add_status_to_orders.sql
└── V4__create_order_items_table.sqlCaching
Spring's caching abstraction (@Cacheable, @CacheEvict, @CachePut) decouples cache logic from business logic. Back it with Redis for a distributed cache shared across all app instances.
- ✓@EnableCaching activates Spring's caching proxy — without it, @Cacheable annotations are ignored.
- ✓@Cacheable returns the cached value on cache hit; executes the method and caches the result on miss.
- ✓@CachePut always executes the method and always updates the cache — use after write operations.
- ✓@CacheEvict removes entries — use allEntries = true sparingly (removes all keys in the cache region).
- ✓Use GenericJackson2JsonRedisSerializer for JSON-serialized Redis values — readable and debuggable.
- ✓Always set a TTL on every cache to prevent stale data accumulating in Redis forever.
@Configuration
@EnableCaching
public class CacheConfig { }
@Service
public class CourseService {
// Cache result — key = courseId
// courses::123 stored in Redis
@Cacheable(value = "courses", key = "#courseId")
public CourseDto getCourse(String courseId) {
return courseRepository.findById(courseId)
.map(courseMapper::toDto)
.orElseThrow(() -> new CourseNotFoundException(courseId));
// Only called on first request — cached result returned on subsequent calls
}
// Condition: only cache if course is published
@Cacheable(value = "courses", key = "#courseId",
condition = "#result.status == 'PUBLISHED'")
public CourseDto getCoursePublished(String courseId) { ... }
// Always execute method AND update cache — for write operations
@CachePut(value = "courses", key = "#result.id")
public CourseDto updateCourse(String courseId, UpdateCourseRequest req) {
Course updated = courseRepository.save(/* ... */);
return courseMapper.toDto(updated);
}
// Remove specific entry on update/delete
@CacheEvict(value = "courses", key = "#courseId")
public void deleteCourse(String courseId) {
courseRepository.deleteById(courseId);
}
// Evict ALL entries in the cache
@CacheEvict(value = "courses", allEntries = true)
@Scheduled(cron = "0 0 3 * * *") // 3 AM daily cache refresh
public void clearCourseCache() { }
}