Cheat SheetsSpring BootData & JPA

Data & JPA — Cheat Sheet

Spring Boot · 4 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Data & JPA
Spring Boot4 topicsQuick revision reference
1

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.
Java — bidirectional @OneToMany / @ManyToOne
@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;
}
2

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.
Java — @Transactional basics, rollback, self-invocation pitfall
@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() { /* ... */ }
}
3

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.
XML + YAML — Flyway dependency and config
<!-- 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.sql
4

Caching

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.
Java — @Cacheable, @CachePut, @CacheEvict
@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() { }
}
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/spring-boot