Spring JPA Entities
Introduction
Spring JPA Entities are Java objects that represent data stored in a database. They are a core component of the Java Persistence API (JPA), which is the standard ORM (Object-Relational Mapping) specification for Java applications. Spring Data JPA provides an additional layer of abstraction on top of JPA, making it easier to implement data access layers for your Spring applications.
In this guide, we'll explore how to create and work with JPA entities in Spring applications, covering everything from basic entity mappings to more complex relationships and advanced features.
What are JPA Entities?
JPA entities are Plain Old Java Objects (POJOs) that are mapped to database tables. Each entity instance corresponds to a row in the table, and each entity property corresponds to a column.
Key characteristics of JPA entities:
- They are annotated with
@Entity
- They have a primary key, annotated with
@Id
- They must have a no-argument constructor
- They cannot be
final
(nor can their methods or persistent instance variables)
Creating Your First JPA Entity
Let's start by creating a simple Customer
entity:
package com.example.demo.entity;
import javax.persistence.*;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
// Default constructor - required by JPA
public Customer() {
}
// Constructor with fields
public Customer(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
'}';
}
}
In this example:
@Entity
marks the class as a JPA entity@Id
marks theid
field as the primary key@GeneratedValue
indicates that the primary key should be automatically generated (auto-increment in the database)
Entity Mapping Annotations
JPA provides various annotations to customize how entities are mapped to database tables:
Basic Table Mapping
@Entity
@Table(name = "customers",
schema = "sales",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"email"})
})
public class Customer {
// Entity implementation
}
Column Mapping
@Column(name = "first_name", length = 50, nullable = false)
private String firstName;
@Column(name = "email", unique = true)
private String email;
@Column(name = "date_of_birth")
@Temporal(TemporalType.DATE)
private Date dateOfBirth;
@Column(name = "profile", columnDefinition = "TEXT")
private String profile;
@Transient
private Integer age; // Not persisted to the database
ID Generation Strategies
JPA offers several strategies for generating primary keys:
// Auto-increment (identity column)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Sequence generator
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customer_seq")
@SequenceGenerator(name = "customer_seq", sequenceName = "customer_sequence", allocationSize = 1)
private Long id;
// Table generator
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "customer_gen")
@TableGenerator(name = "customer_gen", table = "id_generator",
pkColumnName = "gen_name", valueColumnName = "gen_value",
pkColumnValue = "customer_id", initialValue = 1000, allocationSize = 10)
private Long id;
Entity Relationships
JPA supports different types of relationships between entities:
One-to-One Relationship
// In Customer class
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
// In Address class
@Entity
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String street;
private String city;
private String state;
private String zipCode;
@OneToOne(mappedBy = "address")
private Customer customer;
// Constructors, getters and setters
}
One-to-Many Relationship
// In Customer class
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
// In Order class
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private LocalDate orderDate;
private BigDecimal totalAmount;
@ManyToOne
@JoinColumn(name = "customer_id")
private Customer customer;
// Constructors, getters and setters
}
Many-to-Many Relationship
// In Product class
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
@ManyToMany(mappedBy = "products")
private Set<Category> categories = new HashSet<>();
// Constructors, getters and setters
}
// In Category class
@Entity
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "product_category",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "product_id")
)
private Set<Product> products = new HashSet<>();
// Constructors, getters and setters
}
Advanced Entity Features
Embedded Objects
You can include complex types as part of your entity using the @Embedded
annotation:
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private ContactInfo contactInfo;
// Constructors, getters and setters
}
@Embeddable
public class ContactInfo {
@Column(name = "email_address")
private String emailAddress;
@Column(name = "phone_number")
private String phoneNumber;
// Constructors, getters and setters
}
Inheritance Strategies
JPA supports different strategies for mapping inheritance relationships:
Single Table Strategy (default)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type")
public abstract class Payment {
@Id
@GeneratedValue
private Long id;
private BigDecimal amount;
// Common methods
}
@Entity
@DiscriminatorValue("CC")
public class CreditCardPayment extends Payment {
private String cardNumber;
private String expirationDate;
// Credit card specific methods
}
@Entity
@DiscriminatorValue("BT")
public class BankTransferPayment extends Payment {
private String accountNumber;
private String bankName;
// Bank transfer specific methods
}
Joined Table Strategy
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Vehicle {
@Id
@GeneratedValue
private Long id;
private String manufacturer;
private String model;
}
@Entity
public class Car extends Vehicle {
private int numberOfDoors;
private String fuelType;
}
@Entity
public class Motorcycle extends Vehicle {
private boolean hasSidecar;
}
Table Per Class Strategy
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Person {
@Id
@GeneratedValue
private Long id;
private String name;
private LocalDate dateOfBirth;
}
@Entity
public class Student extends Person {
private String studentId;
private double gpa;
}
@Entity
public class Professor extends Person {
private String employeeId;
private String department;
}
Entity Lifecycle Events
JPA provides lifecycle event callbacks to execute code at specific points in an entity's lifecycle:
@Entity
public class AuditedEntity {
@Id
@GeneratedValue
private Long id;
private String name;
private LocalDateTime createdDate;
private LocalDateTime lastModifiedDate;
@PrePersist
protected void onCreate() {
createdDate = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
lastModifiedDate = LocalDateTime.now();
}
// Other entity code
}
Available lifecycle annotations:
@PrePersist
: Before an entity is persisted@PostPersist
: After an entity is persisted@PreUpdate
: Before an entity is updated@PostUpdate
: After an entity is updated@PreRemove
: Before an entity is deleted@PostRemove
: After an entity is deleted@PostLoad
: After an entity is loaded from the database
Entity Validation with Bean Validation
You can add validation constraints to your entity fields using Bean Validation annotations:
import javax.validation.constraints.*;
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
@Min(value = 18, message = "Age should be at least 18")
private int age;
// Constructors, getters and setters
}
Real-World Example: E-Commerce Application
Here's a more comprehensive example of entities for an e-commerce application:
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String firstName;
@NotBlank
private String lastName;
@Email
@Column(unique = true)
private String email;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL)
private ShippingAddress defaultShippingAddress;
// Methods to manage relationships
public void addOrder(Order order) {
orders.add(order);
order.setCustomer(this);
}
public void removeOrder(Order order) {
orders.remove(order);
order.setCustomer(null);
}
// Constructors, getters and setters
}
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_number", unique = true)
private String orderNumber;
@Column(name = "order_date")
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL)
private ShippingAddress shippingAddress;
@PrePersist
public void prePersist() {
orderDate = LocalDateTime.now();
orderNumber = "ORD-" + System.currentTimeMillis();
}
// Methods to calculate total, etc.
public BigDecimal getTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Constructors, getters and setters
}
@Entity
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
private Integer quantity;
@Column(name = "unit_price")
private BigDecimal unitPrice;
// Method to calculate subtotal
public BigDecimal getSubtotal() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
// Constructors, getters and setters
}
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@NotNull
private BigDecimal price;
private String sku;
@Column(name = "stock_quantity")
private Integer stockQuantity;
@ManyToMany
@JoinTable(
name = "product_category",
joinColumns = @JoinColumn(name = "product_id"),
inverseJoinColumns = @JoinColumn(name = "category_id")
)
private Set<Category> categories = new HashSet<>();
// Constructors, getters and setters
}
@Entity
public class ShippingAddress {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String street;
@NotBlank
private String city;
@NotBlank
private String state;
@NotBlank
@Column(name = "zip_code")
private String zipCode;
@NotBlank
private String country;
// Constructors, getters and setters
}
@Entity
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
@ManyToMany(mappedBy = "categories")
private Set<Product> products = new HashSet<>();
// Constructors, getters and setters
}
public enum OrderStatus {
CREATED,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED
}
Best Practices for JPA Entities
-
Use appropriate fetch strategies: Use
FetchType.LAZY
for most associations to improve performance. -
Use bidirectional relationships wisely: Establish bidirectional relationships only when necessary; they're more complex to maintain.
-
Implement equals() and hashCode() properly: Base these on business keys or the ID field, but be careful with ID-based implementations for new entities.
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
private String isbn; // business key
private String title;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(isbn, book.isbn);
}
@Override
public int hashCode() {
return Objects.hash(isbn);
}
}
-
Use DTOs for data transfer: Don't expose entities directly in your API; use DTOs to transfer data between layers.
-
Consider using @NamedQueries: For frequently used queries, consider using
@NamedQuery
annotations.
@Entity
@NamedQueries({
@NamedQuery(
name = "Customer.findByLastName",
query = "SELECT c FROM Customer c WHERE c.lastName = :lastName"
),
@NamedQuery(
name = "Customer.findByEmail",
query = "SELECT c FROM Customer c WHERE c.email = :email"
)
})
public class Customer {
// Entity implementation
}
- Use database indexes: Add
@Index
annotations to improve query performance.
@Entity
@Table(name = "customers", indexes = {
@Index(name = "idx_customer_email", columnList = "email"),
@Index(name = "idx_customer_last_name", columnList = "last_name")
})
public class Customer {
// Entity implementation
}
- Prefer UUID over database-generated IDs for distributed systems:
@Entity
public class User {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
@Column(name = "id", updatable = false, nullable = false)
private UUID id;
// Other fields
}
Summary
JPA entities are the foundation of your data layer in Spring applications. They provide a type-safe, object-oriented way of working with your database. In this guide, we've covered:
- Basic entity definition and mapping
- Different types of relationships between entities
- Advanced features like inheritance and embeddables
- Entity lifecycle events
- Validation and best practices
Understanding how to properly design and implement JPA entities is crucial for building efficient, maintainable Spring applications. By following the best practices and patterns outlined in this guide, you'll be well on your way to creating a robust data access layer.
Additional Resources
Exercises
-
Create a
Library
application with entities forBook
,Author
, andPublisher
. Implement appropriate relationships between them. -
Build a
Blog
application with entities forPost
,Comment
, andUser
. Implement validation for each entity. -
Implement an inheritance hierarchy for a
BankAccount
entity with specialized account types likeSavingsAccount
andCheckingAccount
. -
Create an e-commerce data model with entities for
Product
,Order
,Customer
, andShoppingCart
. Add appropriate relationships and business logic. -
Build a school management system with entities for
Student
,Course
,Teacher
, andEnrollment
. Implement a many-to-many relationship between students and courses.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)