JDBC, JPA, Hibernate và Spring Data JPA khác nhau thế nào?
Mình muốn học Spring Data JPA.
OK, mình học Spring Data JPA theo format quen thuộc: Bài toán → Vấn đề → Giải pháp → Code → Giải thích → Phỏng vấn. Trọng tâm là những gì một Senior Backend cần hiểu: transaction, performance và concurrency.
Cả chương dùng chung một project shop-app (E-commerce) trên PostgreSQL:
users
| 1 - N
v
orders
| 1 - N
v
order_items
| N - 1
v
products
Spring Boot 3.x, Jakarta Persistence (jakarta.persistence.*), Java 17+. Nếu dự án cũ dùng Spring Boot 2.x thì import là javax.persistence, ý tưởng giữ nguyên.
1. Bài toán: lấy User theo ID
Bảng PostgreSQL:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255)
);
Yêu cầu: viết Java API trả về User theo ID. Cách cổ điển là JDBC:
public User findById(Long id) throws SQLException {
String sql = "SELECT * FROM users WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setLong(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
}
}
}
return null;
}
2. Vấn đề
- Tự viết SQL, tự tạo
PreparedStatement, tự execute. - Tự đọc
ResultSetvà mapping từng cột thành field Java. - Với 100 bảng và hàng trăm API, lượng code lặp lại rất lớn.
- Quan hệ (Order có nhiều OrderItem) phải tự join và tự ghép object.
3. Giải pháp: ORM
ORM là Object Relational Mapping: mapping Java object với bảng database.
Java Object DB Table
+---------------+ ORM +----------------+
| User | <---------> | users |
| id: Long | | id BIGINT |
| name: String | | name VARCHAR |
| email: String| | email VARCHAR |
+---------------+ +----------------+
Bốn khái niệm hay bị nhầm khi phỏng vấn:
+--------------------------------------------+
| Spring Data JPA |
| Repository abstraction, giảm boilerplate |
+--------------------------------------------+
|
v
+--------------------------------------------+
| JPA Specification (jakarta.persistence) |
| Định nghĩa chuẩn API và hành vi ORM |
+--------------------------------------------+
|
v
+--------------------------------------------+
| Hibernate (JPA Provider) |
| Triển khai ORM, sinh SQL |
+--------------------------------------------+
|
v
+--------------------------------------------+
| JDBC Driver --> PostgreSQL |
| Gửi SQL đến database |
+--------------------------------------------+
| Công nghệ | Vai trò |
|---|---|
| JDBC | Java API cấp thấp để giao tiếp với relational database |
| JPA | Specification: quy định chuẩn persistence/ORM (annotation, EntityManager, JPQL) |
| Hibernate | Framework triển khai JPA (JPA provider), sinh SQL |
| Spring Data JPA | Thư viện của Spring giúp dùng JPA dễ hơn qua Repository |
JPA là chuẩn, không tự chạy được. Hibernate là provider phổ biến nhất. Spring Data JPA làm việc thông qua JPA và mặc định dùng Hibernate trong Spring Boot.
4. Code từng bước
Bước 1: Dependency
<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>
Starter này kéo theo Hibernate, HikariCP (connection pool) và Spring Data JPA.
Bước 2: application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/shop
username: ${DB_USER}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate # production: dùng Flyway/Liquibase, không để update
open-in-view: false
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: debug # in ra SQL Hibernate sinh
Username/password lấy từ biến môi trường, không hardcode.
Bước 3: Code Java. Entity:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
protected User() {} // JPA cần no-arg constructor
// getters, setters
}
Repository, chỉ một interface:
public interface UserRepository extends JpaRepository<User, Long> {
}
Không cần viết implementation, đã có sẵn:
userRepository.findById(1L);
userRepository.findAll();
userRepository.save(user);
userRepository.deleteById(1L);
userRepository.count();
Service và Controller:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional(readOnly = true)
public UserDto getUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
return new UserDto(user.getId(), user.getName(), user.getEmail());
}
}
public record UserDto(Long id, String name, String email) {}
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.getUser(id);
}
}
Production nên trả DTO, không trả thẳng Entity ra JSON (tránh lazy loading lỗi, lộ field, vòng lặp serialize).
Bước 4: Test bằng curl
curl http://localhost:8080/users/10
# {"id":10,"name":"John","email":"john@example.com"}
# Log:
# select u1_0.id,u1_0.email,u1_0.name from users u1_0 where u1_0.id=?
5. Cách hoạt động: chuyện gì xảy ra khi gọi findById()?
UserService.getUser(10L)
|
v
UserRepository.findById(10L)
|
v
Spring Data JPA Repository Proxy (SimpleJpaRepository)
|
v
EntityManager.find(User.class, 10L)
|
v
Hibernate (JPA Provider) --> sinh SQL, mapping ResultSet -> User
|
v
JDBC Driver
|
v
PostgreSQL
Hibernate sinh SQL tương đương:
SELECT u.id, u.name, u.email
FROM users u
WHERE u.id = 10;
Kết quả được mapping thành object User{id=10, name="John", ...}.
Điểm cần nhớ: findById() không phải lúc nào cũng gửi SQL. Nếu Entity đã nằm trong Persistence Context, Hibernate trả luôn từ first-level cache. Bài 4.3.2 sẽ học kỹ.
6. So sánh
| JDBC | JPA + Hibernate (EntityManager) | Spring Data JPA | |
|---|---|---|---|
| Viết SQL | Tự viết hết | JPQL hoặc sinh tự động | Derived query, @Query |
| Mapping | Tự đọc ResultSet | Tự động qua annotation | Tự động |
| CRUD | Tự viết | persist/find/remove | Có sẵn trong JpaRepository |
| Kiểm soát SQL | Cao nhất | Trung bình | Trung bình, cần hiểu Hibernate bên dưới |
7. Khi nào nên dùng / không nên dùng
- Nên dùng Spring Data JPA cho ứng dụng nghiệp vụ CRUD, domain model rõ ràng (order, user, product).
- Cân nhắc JDBC/jOOQ/native SQL cho report phức tạp, bulk xử lý hàng triệu dòng, SQL đặc thù database.
- Không phải chọn một: cùng project có thể dùng JPA cho nghiệp vụ và
JdbcTemplatecho report.
setPrice() mà không save() database vẫn update? Câu trả lời nằm ở Persistence Context.Vậy ai thực sự tạo SQL để truy vấn PostgreSQL: Spring Data JPA, JPA hay JDBC Driver?
Đáp án là Hibernate.
- Spring Data JPA chỉ tạo proxy cho repository và gọi xuống
EntityManager. Với derived query nhưfindByEmail, nó dựng JPQL/Criteria, nhưng chưa phải SQL. - JPA là specification, chỉ là bộ interface và quy tắc.
- Hibernate là provider: dịch JPQL/thao tác Entity thành SQL theo dialect PostgreSQL.
- JDBC Driver chỉ gửi SQL đó tới PostgreSQL và trả kết quả về.
findByEmail("a@x.com")
Spring Data -> JPQL: select u from User u where u.email = ?1
Hibernate -> SQL : select ... from users u1_0 where u1_0.email=?
JDBC Driver -> gửi qua network tới PostgreSQL
Nếu JPA chỉ là chuẩn thì đổi Hibernate sang EclipseLink có dễ không?
Bạn hiểu đúng về lý thuyết: code chỉ dùng jakarta.persistence.* thì đổi provider được. Chỉ cần chỉnh một điểm: thực tế hiếm khi "dễ".
- Dự án thường dùng thêm tính năng riêng của Hibernate:
@BatchSize,@Formula,@Cache, các propertyhibernate.*. - Hành vi lazy loading, flush, SQL sinh ra có khác biệt nhỏ giữa provider.
- Spring Boot auto-config mặc định Hibernate.
Giá trị thật của JPA là: kiến thức và annotation dùng chung, không phải "đổi provider mỗi tuần".
Phỏng vấn hỏi JPA, Hibernate, Spring Data JPA thì trả lời sao?
Q1. Spring Data JPA là gì?
🇻🇳 Tiếng Việt:
Spring Data JPA là một phần của Spring Data, giúp truy cập database qua JPA đơn giản hơn. Nó cung cấp repository abstraction: CRUD có sẵn, query method, phân trang, sắp xếp, giảm boilerplate. Bên dưới thường dùng Hibernate làm JPA provider.
🇬🇧 English:
Spring Data JPA is part of the Spring Data project that simplifies database access using JPA. It provides repository abstractions, supports CRUD operations, query methods, pagination and sorting, and reduces boilerplate code. Hibernate is commonly used as the underlying JPA implementation.
Q2. JPA khác Hibernate thế nào?
🇻🇳 Tiếng Việt:
JPA là specification, định nghĩa API chuẩn cho object-relational mapping. Hibernate là ORM framework triển khai specification đó, và là thành phần thực sự sinh SQL.
🇬🇧 English:
JPA is a specification that defines standard APIs for object-relational mapping, while Hibernate is an ORM framework that implements the JPA specification and actually generates the SQL.
Q3. Khi gọi repository.findById() thì chuyện gì xảy ra?
🇻🇳 Tiếng Việt:
Repository là proxy do Spring Data tạo, gọi
EntityManager.find(). Hibernate kiểm tra Persistence Context trước; nếu chưa có Entity thì sinh SELECT, chạy qua JDBC, mapping kết quả thành Entity và đưa vào trạng thái managed.
🇬🇧 English:
The repository is a Spring Data proxy that delegates to
EntityManager.find(). Hibernate first checks the Persistence Context; if the entity is not there, it generates a SELECT, executes it through JDBC, maps the row into an entity and keeps it as a managed instance.
Cách nhớ ngắn gọn
- JDBC = gửi SQL. JPA = chuẩn. Hibernate = người làm (sinh SQL). Spring Data JPA = repository cho tiện.
- Repository → EntityManager → Hibernate → JDBC → PostgreSQL.
findById()có thể không chạm DB nhờ first-level cache.- Trả DTO, không trả Entity ra API.
Tự kiểm tra
Thành phần nào sinh SQL khi dùng Spring Data JPA với Hibernate?
Hibernate (JPA provider). JDBC Driver chỉ gửi SQL tới database.
JPA có phải là một thư viện chạy được không?
Không. JPA là specification (interface + quy tắc). Cần provider như Hibernate để chạy.
UserRepository extends JpaRepository<User, Long> lấy implementation từ đâu?
Spring Data tạo proxy lúc runtime, mặc định ủy quyền cho SimpleJpaRepository, bên trong dùng EntityManager.
Vì sao nên trả DTO thay vì Entity ở Controller?
Tránh LazyInitializationException, tránh lộ field nội bộ, tránh vòng lặp khi serialize quan hệ hai chiều, tách API khỏi schema.
Khi nào nên dùng JDBC/native SQL thay vì JPA?
Report phức tạp, bulk xử lý lớn, SQL đặc thù database, khi cần kiểm soát SQL tuyệt đối.