Skip to main content

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:

java
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 the id 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

java
@Entity
@Table(name = "customers",
schema = "sales",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"email"})
})
public class Customer {
// Entity implementation
}

Column Mapping

java
@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:

java
// 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

java
// 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

java
// 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

java
// 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:

java
@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)

java
@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

java
@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

java
@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:

java
@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:

java
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:

java
@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

  1. Use appropriate fetch strategies: Use FetchType.LAZY for most associations to improve performance.

  2. Use bidirectional relationships wisely: Establish bidirectional relationships only when necessary; they're more complex to maintain.

  3. Implement equals() and hashCode() properly: Base these on business keys or the ID field, but be careful with ID-based implementations for new entities.

java
@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);
}
}
  1. Use DTOs for data transfer: Don't expose entities directly in your API; use DTOs to transfer data between layers.

  2. Consider using @NamedQueries: For frequently used queries, consider using @NamedQuery annotations.

java
@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
}
  1. Use database indexes: Add @Index annotations to improve query performance.
java
@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
}
  1. Prefer UUID over database-generated IDs for distributed systems:
java
@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

  1. Create a Library application with entities for Book, Author, and Publisher. Implement appropriate relationships between them.

  2. Build a Blog application with entities for Post, Comment, and User. Implement validation for each entity.

  3. Implement an inheritance hierarchy for a BankAccount entity with specialized account types like SavingsAccount and CheckingAccount.

  4. Create an e-commerce data model with entities for Product, Order, Customer, and ShoppingCart. Add appropriate relationships and business logic.

  5. Build a school management system with entities for Student, Course, Teacher, and Enrollment. 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! :)