JDBC, JPA, Hibernate và Spring Data JPA khác nhau thế nào?

4 câu hỏi~20 phútTừ khóa: JDBC, ORM, JPA specification, Hibernate, JPA provider, JpaRepository, EntityManager, repository proxy

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 đề

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ò
JDBCJava API cấp thấp để giao tiếp với relational database
JPASpecification: quy định chuẩn persistence/ORM (annotation, EntityManager, JPQL)
HibernateFramework triển khai JPA (JPA provider), sinh SQL
Spring Data JPAThư 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

JDBCJPA + Hibernate (EntityManager)Spring Data JPA
Viết SQLTự viết hếtJPQL hoặc sinh tự độngDerived query, @Query
MappingTự đọc ResultSetTự động qua annotationTự động
CRUDTự viếtpersist/find/removeCó sẵn trong JpaRepository
Kiểm soát SQLCao nhấtTrung bìnhTrung 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

Vậy ai thực sự tạo SQL để truy vấn PostgreSQL: Spring Data JPA, JPA hay JDBC Driver?

Đáp án là Hibernate.

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ễ".

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


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.