Skip to main content

JPA Basics — Practical Demo

Hands-on examples for JPA Basics. We use a small Product/Order/Customer domain throughout to keep context consistent.

Prerequisites

You should be comfortable with basic Spring Boot setup and understand what @Entity and @Id mean before running these examples. See JPA Basics for the theory.


Example 1: Define a Basic Entity

The simplest entity — a Product with an auto-generated ID and a few columns.

Product.java
@Entity
@Table(name = "products")
public class Product {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // ← DB auto-increment
private Long id;

@Column(nullable = false, length = 200)
private String name;

@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;

@Column(columnDefinition = "TEXT") // ← maps to TEXT column type
private String description;

// Getters and setters (or use Lombok @Getter @Setter)
}
application.yml
spring:
jpa:
hibernate:
ddl-auto: create-drop # ← auto-creates schema on startup, drops on stop (dev only)
show-sql: true
properties:
hibernate:
format_sql: true

Expected SQL logged on startup:

create table products (
id bigint generated by default as identity,
name varchar(200) not null,
price numeric(10,2) not null,
description text,
primary key (id)
)
Key takeaway

@Column(nullable = false) maps to NOT NULL in DDL. The @GeneratedValue(strategy = IDENTITY) delegates ID generation to the database — the most common choice for modern databases.


Example 2: Add a ManyToOne Relationship

An Order belongs to one Customer. Keep LAZY loading on the @ManyToOne (it's EAGER by default — override it).

Customer.java
@Entity
@Table(name = "customers")
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String name;

private String email;
}
Order.java
@Entity
@Table(name = "orders")
public class Order {

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

@ManyToOne(fetch = FetchType.LAZY) // ← override EAGER default; load on demand
@JoinColumn(name = "customer_id") // ← foreign key column name in orders table
private Customer customer;

@Enumerated(EnumType.STRING)
private OrderStatus status;

@OneToMany(mappedBy = "order", // ← "mappedBy" = inverse side (no FK here)
cascade = CascadeType.ALL,
fetch = FetchType.LAZY)
private List<OrderItem> items = new ArrayList<>();
}
OrderRepository.java
public interface OrderRepository extends JpaRepository<Order, Long> {}
Demo
// Saving an order
Customer customer = customerRepo.save(new Customer("Alice", "alice@example.com"));
Order order = new Order();
order.setCustomer(customer);
order.setStatus(OrderStatus.PENDING);
orderRepo.save(order);
// ↑ two INSERTs: one for customer (if not saved), one for order
Key takeaway

cascade = CascadeType.ALL on @OneToMany means saving or deleting an Order will cascade to its OrderItem children automatically. Without this, you'd need to save items separately.


Example 3: Lifecycle Callbacks and Auditing

Track when entities are created and updated automatically using Spring Data auditing.

AuditableEntity.java
@MappedSuperclass          // ← not an entity itself; fields inherited by subclasses
@EntityListeners(AuditingEntityListener.class) // ← Spring Data auditing listener
public abstract class AuditableEntity {

@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

@CreatedBy
@Column(updatable = false)
private String createdBy;
}
Product.java (updated)
@Entity
@Table(name = "products")
public class Product extends AuditableEntity { // ← inherits audit fields
// ... id, name, price, description as before
}
Application.java
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider") // ← activates audit infrastructure
public class Application { ... }

@Bean
public AuditorAware<String> auditorProvider() {
// In a real app, return the current user from SecurityContextHolder
return () -> Optional.of("system");
}

Expected behaviour: After productRepo.save(product):

product.getCreatedAt() → 2026-03-08T10:00:00
product.getCreatedBy() → "system"

Exercises

  1. Easy: Add a @Column(unique = true) constraint to the Product name field and verify the generated DDL.
  2. Medium: Add a @PreUpdate lifecycle callback to Product that logs the entity ID and the timestamp when it is about to be updated.
  3. Hard: Implement a @ManyToMany between Product and Tag (using a join table product_tags), save a product with two tags, and verify that deleting the product does not delete the tags (configure cascade appropriately).

Back to Topic

Return to JPA Basics for theory, trade-offs, interview questions, and further reading.