Return

Giải Mã Ma Thuật Spring Data JPA & Tự Viết JPA Provider

Written by LocLT

Bạn khai báo interface UserRepository extends JpaRepository<User, Long>bùm! Mọi thứ chạy mượt mà dù bạn không viết một dòng code implementation nào. Nhiều developer gọi đó là “Spring Magic”.

Nhưng là những kỹ sư phần mềm, chúng ta không tin vào phép thuật. Hôm nay, chúng ta sẽ đi từ lớp bề mặt (Dynamic Proxy) xuống tận cùng “đáy đại dương” (Database Internals) bằng cách tự tay xây dựng một Custom JPA Provider (mang tên Mini-Hibernate).

Phần 1: Sự Biến Mất Của Implementation (Dynamic Proxy)

Bí mật đầu tiên nằm ở Java Dynamic Proxy. Spring Data JPA thực chất là một nhà máy sản xuất Proxy khổng lồ.

Simulation: Cách Spring tạo Implementation lúc Runtime

Hãy xem đoạn code giả lập (Simulation) dưới đây để hiểu cách Spring đánh tráo Interface thành Object thực thi:

MagicProxyDemo.java

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 1. Interface của người dùng (Chỉ có chữ ký hàm)
interface UserRepo {
    String findByName(String name);
}

public class MagicProxyDemo {
    public static void main(String[] args) {
        // 2. Tạo Proxy (Spring làm bước này tự động khi start app)
        UserRepo repo = (UserRepo) Proxy.newProxyInstance(
            UserRepo.class.getClassLoader(),
            new Class[]{UserRepo.class},
            new JpaProxyHandler() // Trình xử lý cuộc gọi (Interceptor)
        );

        // 3. Gọi hàm - Dù không có class UserRepoImpl, code vẫn chạy!
        System.out.println(repo.findByName("Ethan")); 
    }
}

// 4. "Thằng đứng giữa" (InvocationHandler)
class JpaProxyHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        // Đây là nơi Spring phân tích tên hàm "findByName"
        if (method.getName().equals("findByName")) {
            String arg = (String) args[0];
            System.out.println("--> [Magic] Chuyển thành SQL: SELECT * FROM users WHERE name = '" + arg + "'");
            return "User{name='" + arg + "'}";
        }
        return null;
    }
}

Kết luận Phần 1: Không có phép màu nào cả. Spring chỉ đơn giản là dùng Proxy để chặn cuộc gọi hàm, sau đó ủy quyền (delegate) xuống tầng dưới. Tầng dưới đó chính là JPA Provider.


Phần 2: Deep Dive Architecture - Xây Dựng JPA Provider Từ Số 0

Bây giờ, chúng ta sẽ bỏ qua lớp vỏ bọc Spring và đi sâu vào cách hoạt động của một ORM Framework thực thụ (như Hibernate hay EclipseLink). Chúng ta sẽ implement Mini-Hibernate.

1. Kiến Trúc Tổng Quan (Macro Architecture)

Một JPA Provider chuẩn xoay quanh 3 giai đoạn vòng đời (Lifecycle Phases):

2. Bootstrapping & Factory Pattern

Mọi thứ bắt đầu từ SPI (Service Provider Interface). Spring container sẽ quét file META-INF/services/jakarta.persistence.spi.PersistenceProvider để tìm class khởi động.

MiniPersistenceProvider.java

public class MiniPersistenceProvider implements PersistenceProvider {
    @Override
    public EntityManagerFactory createEntityManagerFactory(String emName, Map map) {
        // 1. Đọc cấu hình (JDBC URL, User, Pass)
        HikariConfig dbConfig = new HikariConfig();
        dbConfig.setJdbcUrl((String) map.get("jakarta.persistence.jdbc.url"));
        // ... set user/pass
        
        // 2. Khởi tạo Connection Pool & Metamodel (Nặng nhất)
        return new MiniEntityManagerFactoryImpl(new HikariDataSource(dbConfig));
    }
}

3. Metamodel: Cầu Nối Giữa Java & SQL

Làm sao Framework biết class User map vào bảng tbl_users? Chúng ta không thể scan Annotation liên tục lúc chạy (quá chậm). Giải pháp là parse một lần duy nhất lúc khởi động (Warmup).

EntityMetadata.java (The Parser)

public class EntityMetadata {
    private final String tableName;
    private final Field idField;
    private final List<Field> columns = new ArrayList<>();

    public EntityMetadata(Class<?> clazz) {
        // Logic: Ưu tiên @Table name, fallback về Class name
        Table tableAnn = clazz.getAnnotation(Table.class);
        this.tableName = (tableAnn != null && !tableAnn.name().isEmpty()) 
                         ? tableAnn.name() : clazz.getSimpleName();

        // Introspection: Quét field cache lại để dùng sau này
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Column.class)) {
                field.setAccessible(true); // Tối ưu Reflection
                this.columns.add(field);
            }
            // ... xử lý @Id
        }
    }
}

4. Runtime: Persistence Context (Bộ Não)

EntityManager chỉ là một người quản lý. “Bộ não” thực sự nằm ở Persistence Context (L1 Cache). Nó đảm bảo tính nhất quán dữ liệu (Identity Map) và tối ưu hiệu năng (Lazy Write).

MiniEntityManagerImpl.java

public class MiniEntityManagerImpl implements EntityManager {
    // Mỗi Session có 1 Context riêng
    private final PersistenceContext context = new PersistenceContext(); 
    
    @Override
    public void persist(Object entity) {
        // 1. Transactional Write-Behind
        // KHÔNG insert vào DB ngay! Chỉ xếp hàng (Queue) chờ.
        context.addEntity(entity); 
    }

    @Override
    public void flush() {
        // 2. Synchronization
        // Lúc này mới mở Transaction, lấy danh sách từ Queue và bắn SQL 1 lần.
        List<Object> dirtyEntities = context.getDirtyEntities();
        for (Object e : dirtyEntities) {
            EntityMetadata meta = metamodel.get(e.getClass());
            persister.insert(e, meta);
        }
    }
}

5. Execution Engine: EntityPersister

Tầng cuối cùng, nơi “chạm” vào JDBC. Persister sử dụng Metadata đã parse ở trên để sinh SQL động và map kết quả trả về (`ResultSet`) thành Java Object (`Hydration`).

EntityPersister.java

public class EntityPersister {
    public void insert(Object entity, EntityMetadata meta) {
        // 1. Sinh Dynamic SQL: "INSERT INTO table (col1, col2) VALUES (?, ?)"
        String sql = sqlGenerator.generateInsert(meta);
        
        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
            // 2. Bind Parameters từ Entity Object
            int idx = 1;
            for (Field field : meta.getColumns()) {
                stmt.setObject(idx++, field.get(entity));
            }
            stmt.executeUpdate();
        } catch (Exception e) { throw new PersistenceException(e); }
    }
}

Phần 3: Tích Hợp Vào Spring Boot - Zero-Config (Auto-Configuration)

Trong thực tế, người dùng Spring Boot cực kỳ lười (theo nghĩa tích cực). Họ không muốn viết code boilerplate. Để Mini-Hibernate thực sự “ma thuật”, chúng ta cần implement Spring Boot Auto-Configuration.

1. Cơ chế Tự Động Nhận Diện (Custom JpaVendorAdapter)

Chúng ta tạo một JpaVendorAdapter để báo cho Spring biết cách giao tiếp với Provider của mình:

public class MiniJpaVendorAdapter extends AbstractJpaVendorAdapter {
    @Override
    public PersistenceProvider getPersistenceProvider() {
        return new MiniPersistenceProvider();
    }
    // ... trả về class EntityManagerFactory và EntityManager chuẩn
}

2. Đăng ký Auto-Configuration

Sử dụng file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, Spring Boot sẽ tự động quét và khởi tạo Bean nếu thấy thư viện của chúng ta trong classpath:

@AutoConfiguration
@ConditionalOnClass(MiniPersistenceProvider.class)
public class MiniHibernateAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            DataSource ds, JpaVendorAdapter adapter, ConfigurableApplicationContext ctx) {
        
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(ds);
        em.setJpaVendorAdapter(adapter);
        
        // Tự động quét Entity từ package của Application
        List<String> pkgs = AutoConfigurationPackages.get(ctx);
        em.setPackagesToScan(pkgs.toArray(new String[0]));
        
        return em;
    }
}

3. Cách sử dụng (The Zero-Config Experience)

Bây giờ, developer chỉ cần thêm dependency và cấu hình file application.properties. Không cần bất kỳ class @Configuration nào nữa!

application.properties

# Spring Boot sẽ tự động nhận diện MiniPersistenceProvider qua AutoConfig
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=123456

# Các entity sẽ tự động được tìm thấy nhờ cơ chế scan package tự động!

Deep Insight: Đây là cách Spring Data JPA “biến mất”. Nó không chỉ dùng Proxy cho Repository, mà còn dùng Auto-Configuration để tự xây dựng toàn bộ cơ sở hạ tầng (Infrastructure) dựa trên các thư viện (Hibernate, EclipseLink, hay Mini-Hibernate) mà nó tìm thấy trong dự án.