How to implement entity class with foreign key. transaction annotation
This guide demonstrates how to implement JPA entity classes with foreign key relationships and manage database transactions using the `@Transactional` annotation in a Spring Boot application.
Implementing Foreign Key Relationships in JPA
In JPA, foreign key relationships are typically mapped using object-oriented associations. For a many-to-one relationship, where multiple entities of one type (e.g., Order) can be associated with a single entity of another type (e.g., Customer), the @ManyToOne annotation is used. This annotation is placed on the field representing the "many" side of the relationship. The @JoinColumn annotation is then used to specify the actual foreign key column in the database table.
package com.example.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
// Customer Entity (Parent side of the relationship)
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// One customer can have many orders. CascadeType.ALL ensures operations like
// persist/remove on Customer also affect associated Orders. orphanRemoval=true
// deletes an Order if it's disassociated from a Customer.
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders;
// Constructors, Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<Order> getOrders() { return orders; }
public void setOrders(List<Order> orders) { this.orders = orders; }
}
// Order Entity (Child side with the foreign key)
@Entity
@Table(name = "customer_order") // Renamed table to avoid SQL keyword 'ORDER'
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
private LocalDateTime orderDate;
// Many orders can belong to one customer. FetchType.LAZY is generally recommended
// to avoid loading the Customer object unless explicitly accessed.
// @JoinColumn specifies the foreign key column (customer_id) in the 'customer_order' table.
// nullable = false means an order must always be associated with a customer.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
// Constructors, Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getOrderNumber() { return orderNumber; }
public void setOrderNumber(String orderNumber) { this.orderNumber = orderNumber; }
public LocalDateTime getOrderDate() { return orderDate; }
public void setOrderDate(LocalDateTime orderDate) { this.orderDate = orderDate; }
public Customer getCustomer() { return customer; }
public void setCustomer(Customer customer) { this.customer = customer; }
}
@ManyToOne: This annotation marks the relationship, indicating that many instances of the current entity (Order) can be associated with one instance of the target entity (Customer).@JoinColumn(name = "column_name", nullable = false): This specifies the foreign key column (customer_id) in the current entity's table (customer_order).nullable = falseensures that anOrdermust always have an associatedCustomer, enforcing referential integrity at the database level.
Transaction Management with `@Transactional`
In a Spring Boot application using JPA, database operations should typically be performed within a transaction. The @Transactional annotation, provided by Spring, simplifies transaction management by automatically handling transaction boundaries (begin, commit, rollback). It can be applied at the class level (all public methods become transactional) or method level.
package com.example.repository;
import com.example.model.Customer;
import com.example.model.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
// Repository interfaces for basic CRUD operations
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
}
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomer(Customer customer);
}
package com.example.service;
import com.example.model.Customer;
import com.example.model.Order;
import com.example.repository.CustomerRepository;
import com.example.repository.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class OrderService {
private final CustomerRepository customerRepository;
private final OrderRepository orderRepository;
public OrderService(CustomerRepository customerRepository, OrderRepository orderRepository) {
this.customerRepository = customerRepository;
this.orderRepository = orderRepository;
}
@Transactional // All operations within this method will run in a single transaction
public Order createOrder(Long customerId, String orderNumber) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new IllegalArgumentException("Customer not found with ID: " + customerId));
Order order = new Order();
order.setOrderNumber(orderNumber);
order.setOrderDate(LocalDateTime.now());
order.setCustomer(customer); // Link order to customer
return orderRepository.save(order);
}
@Transactional(readOnly = true) // Optimize for read-only operations
public List<Order> getOrdersByCustomer(Long customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new IllegalArgumentException("Customer not found with ID: " + customerId));
return orderRepository.findByCustomer(customer);
}
@Transactional
public void deleteOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("Order not found with ID: " + orderId));
orderRepository.delete(order);
}
}
- Atomicity: Ensures that all operations within a transactional method complete successfully or none of them do. If an unhandled exception occurs, the transaction is rolled back.
- Propagation: Defines how transactions are managed when methods call each other. For example,
Propagation.REQUIRED(default) means a new transaction is created if none exists, or the current one is joined. - Isolation: Controls the degree to which changes made by one transaction are visible to others (e.g.,
Isolation.READ_COMMITTED,Isolation.REPEATABLE_READ). - Read-Only:
@Transactional(readOnly = true)can be used for methods that only read data, potentially optimizing performance by allowing the persistence provider to apply read-only optimizations.
Understanding Cascade Types
CascadeType defines how persistence operations (like persist, merge, remove) are cascaded from a parent entity to its associated child entities. For foreign key relationships, this can be crucial for managing the lifecycle of related objects.
// Example of CascadeType.ALL and orphanRemoval=true on a @OneToMany relationship
// from Customer to Order:
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders;
CascadeType.PERSIST: Cascades the persist operation. If you persist the parent, all its new children will also be persisted.CascadeType.MERGE: Cascades the merge operation. If you merge the parent, its detached children will also be merged.CascadeType.REMOVE: Cascades the remove operation. If you remove the parent, its associated children will also be removed.CascadeType.REFRESH: Cascades the refresh operation.CascadeType.DETACH: Cascades the detach operation.CascadeType.ALL: Cascades all persistence operations (equivalent to combining all specific cascade types). This is often used for parent-child relationships where the child's lifecycle is entirely dependent on the parent.orphanRemoval = true: Used with@OneToManyor@OneToOnerelationships. If a child entity is disassociated from its parent (e.g., removed from the collection), it is automatically deleted from the database. It is often used in conjunction withCascadeType.ALLorCascadeType.REMOVE.
Understanding Fetch Types
FetchType defines when and how associated entities are loaded from the database. This directly impacts application performance and memory usage, especially for relationships that could involve large amounts of data.
// Example of FetchType.LAZY on a @ManyToOne relationship (default for ManyToOne)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
// Example of FetchType.EAGER (default for OneToOne and ManyToOne, but often overridden for performance)
// @OneToOne(fetch = FetchType.EAGER)
// @JoinColumn(name = "address_id")
// private Address address;
FetchType.LAZY: The associated entity (or collection) is fetched from the database only when it is actually accessed. This is the default for@OneToManyand@ManyToManyrelationships and is generally recommended to avoid loading unnecessary data, improving application startup and query performance.FetchType.EAGER: The associated entity (or collection) is fetched immediately along with the parent entity. This is the default for@OneToOneand@ManyToOnerelationships. While convenient for immediate data access, it can lead to performance issues and N+1 query problems if too much data is eagerly loaded unnecessarily.