JPA Basics — Practical Demo
Hands-on examples for JPA Basics. We use a small
Product/Order/Customerdomain throughout to keep context consistent.
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.
@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)
}
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)
)
@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).
@Entity
@Table(name = "customers")
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String email;
}
@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<>();
}
public interface OrderRepository extends JpaRepository<Order, Long> {}
// 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
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.
@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;
}
@Entity
@Table(name = "products")
public class Product extends AuditableEntity { // ← inherits audit fields
// ... id, name, price, description as before
}
@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
- Easy: Add a
@Column(unique = true)constraint to theProductnamefield and verify the generated DDL. - Medium: Add a
@PreUpdatelifecycle callback toProductthat logs the entity ID and the timestamp when it is about to be updated. - Hard: Implement a
@ManyToManybetweenProductandTag(using a join tableproduct_tags), save a product with two tags, and verify that deleting the product does not delete the tags (configurecascadeappropriately).
Back to Topic
Return to JPA Basics for theory, trade-offs, interview questions, and further reading.