Module 3
Master Object-Relational Mapping
This work is licensed under CC BY-NC-SA 4.0
© Way-Up 2025
Java Persistence API (JPA) is a specification for Object-Relational Mapping (ORM)
Several implementations exist:
| Implementation | Description |
|---|---|
| Hibernate | Most popular, feature-rich |
| EclipseLink | Reference implementation |
| OpenJPA | Apache project |
We'll use Hibernate - the most widely adopted ORM framework
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
# application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "full_name", length = 100)
private String fullName;
@Column(name = "created_at")
private LocalDateTime createdAt;
// Constructors, getters, setters
}
@Entity - Marks class as JPA entity@Table - Specifies table name (optional if class name matches)@Id - Marks primary key field@GeneratedValue - Auto-generate primary key@Column - Maps field to column (optional with customization)AUTO - Let JPA chooseIDENTITY - Database auto-incrementSEQUENCE - Database sequenceTABLE - Separate table for ID generation@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Book> books = new ArrayList<>();
}
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
}
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}
Control how operations propagate to related entities:
| Cascade Type | Description |
|---|---|
| PERSIST | Propagate persist operation |
| MERGE | Propagate merge operation |
| REMOVE | Propagate remove operation |
| REFRESH | Propagate refresh operation |
| DETACH | Propagate detach operation |
| ALL | All of the above |
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
Data loaded only when accessed
@ManyToOne(fetch = FetchType.EAGER)
private Author author;
Data loaded immediately with parent entity
Best Practice: Use LAZY by default, load eagerly only when needed
No need to write implementation code!
public interface UserRepository extends JpaRepository<User, Long> {
// Spring Data generates implementation automatically
// Query methods by convention
User findByEmail(String email);
List<User> findByFullNameContaining(String name);
List<User> findByCreatedAtAfter(LocalDateTime date);
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
// Pagination and sorting
Page<User> findAll(Pageable pageable);
}
Repository<T, ID> - Marker interfaceCrudRepository<T, ID> - Basic CRUD operationsPagingAndSortingRepository<T, ID> - Adds pagination and sortingJpaRepository<T, ID> - JPA-specific extensionssaveAll, deleteInBatch)| Keyword | Example |
|---|---|
| findBy... | findByEmail(String email) |
| And, Or | findByNameAndEmail(String name, String email) |
| Between | findByCreatedAtBetween(LocalDateTime start, LocalDateTime end) |
| LessThan, GreaterThan | findByAgeGreaterThan(int age) |
| Like, Containing | findByNameContaining(String name) |
| OrderBy | findByAgeOrderByNameAsc(int age) |
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByDomain(@Param("domain") String domain);
// Native SQL
@Query(value = "SELECT * FROM users WHERE created_at > ?1",
nativeQuery = true)
List<User> findRecentUsers(LocalDateTime date);
// Modifying queries
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);
// DTOs / Projections
@Query("SELECT new com.example.dto.UserDTO(u.id, u.email) FROM User u")
List<UserDTO> findAllUserDTOs();
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public Page<User> getUsers(int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("fullName").ascending());
return userRepository.findAll(pageable);
}
public List<User> getTopUsers(int count) {
Pageable pageable = PageRequest.of(0, count);
return userRepository.findAll(pageable).getContent();
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUser(User user) {
userRepository.save(user);
// Any exception rolls back the transaction
}
@Transactional(readOnly = true)
public User findUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createUserInNewTransaction(User user) {
userRepository.save(user);
// Always executes in new transaction
}
}
// 1 query to get all authors
List<Author> authors = authorRepository.findAll();
// N queries - one for each author's books!
for (Author author : authors) {
System.out.println(author.getBooks().size());
}
// Use JOIN FETCH to load everything in one query
@Query("SELECT a FROM Author a LEFT JOIN FETCH a.books")
List<Author> findAllWithBooks();
// Or use @EntityGraph
@EntityGraph(attributePaths = "books")
List<Author> findAll();
// Good - batch insert
userRepository.saveAll(users);
// Bad - multiple individual inserts
for (User user : users) {
userRepository.save(user);
}
// Load only needed fields
public interface UserProjection {
Long getId();
String getEmail();
}
List<UserProjection> findAllProjectedBy();
Add Hibernate second-level cache with Ehcache:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
</dependency>
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User { }
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
Create migration files in src/main/resources/db/migration/:
db/migration/
├── V1__create_users_table.sql
├── V2__add_email_index.sql
└── V3__create_orders_table.sql
-- V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
full_name VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Automatically track who created/modified entities and when:
@Configuration
@EnableJpaAuditing
public class JpaConfig { }
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
@Entity
public class User extends Auditable {
// User fields
}
Task: Design a blog application data model
Task: Implement complex queries
Task: Identify and fix N+1 queries
Disabled by default in Spring Boot 2+
spring.jpa.open-in-view=false
Always use @Transactional for write operations
Fetch data within transaction or use DTOs
Monitor SQL with show-sql=true
In this module, you learned:
Next Module: RESTful Web Services & APIs