Hibernate Interceptor & StatementInspector

Bài viết được sự cho phép của tác giả Giang Phan

1. Hibernate Interceptor

1.1. Hibernate Interceptor là gì?

Interceptor là một tính năng rất hữu ích cho ứng dụng để kiểm soát đối tượng với các sự kiện nhất định xảy ra bên trong Hibernate.

Một đối tượng đi qua các giai đoạn khác nhau trong lifecycle của nó. Interface Interceptor trong Hibernate cung cấp các phương thức có thể được gọi ở các giai đoạn khác nhau, để thực hiện một số nhiệm vụ được yêu cầu.

Các Interceptor được đăng ký dưới dạng callback và cung cấp các liên kết liên lạc giữa session và application của Hibernate. Với một callback như vậy, một ứng dụng có thể kiểm tra và thay đổi các thuộc tính của một đối tượng persistent trước khi nó được save, update, delete or load.

  Hibernate là gì? Sao phải dùng nó thay JDBC?
  Kiểm tra tính hợp lệ của dữ liệu đầu vào form Spring Web MVC bởi Hibernate Validator

Có 2 cách để tạo một Interceptor:

  • Implement org.hibernate.Interceptor interface.
  • extend org.hibernate.EmptyInterceptor class.

Nếu không có yêu cầu đặc biệt, chúng ta nên extends class EmptyInterceptor và chỉ override các phương thức được yêu cầu.

Interface org.hibernate.Interceptor bao gồm các phương thức sau:

  • instantiate() : Phương thức này được gọi khi một lớp persistent được khởi tạo.
  • onLoad() : Phương thức này được gọ khi một đối tượng được load từ database và khởi tạo.
  • onSave() : Phương thức này được gọ khi một đối tượng được lưu.
  • onDelete() : Phương thức này được gọi khi một đối tượng bị xóa.
  • preFlush() : Phương thức này được gọi trước khi flush.
  • postFlush() : Phương thức này được gọi sau khi flush và một đối tượng đã được update trong bộ nhớ.
  • isUnsaved() : Phương thức này được gọi khi một đối tượng được truyền vào phương thức saveOrUpdate() .
  • findDirty() : Phương thức này được gọi khi phương thức flush() được gọi trên một đối tượng Session.
  • onFlushDirty() : Phương thức này được gọi khi Hibernate phát hiện ra rằng một đối tượng là dirty (tức là đã được thay đổi) trong quá trình flush.
  • Ngoài ra còn nhiều phương thức khác, các bạn tham khảo thêm Hibernate Interceptor javadoc.

Một Interceptor có thể được sử dụng ở: Session hoặc SessionFactory scope.

1.2. Ví dụ sử dụng Hibernate Interceptor

1.2.1. Create Interceptor

package com.gpcoder;

import com.gpcoder.entities.Tag;
import lombok.extern.log4j.Log4j2;
import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Iterator;

@Log4j2
public class LoggingInterceptor extends EmptyInterceptor {

@Override
public String onPrepareStatement(String sql) {
log.debug("onPrepareStatement: {}", sql);
return super.onPrepareStatement(sql);
}

@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
log.debug("onSave: {}", entity);
return super.onSave(entity, id, state, propertyNames, types);
}

@Override
public void preFlush(Iterator entities) {
log.debug("preFlush: {}", entities.next());
super.preFlush(entities);
}

@Override
public void postFlush(Iterator entities) {
log.debug("postFlush: {}", entities.next());
super.postFlush(entities);
}
}

1.2.2. Register Interceptor

Cấu hình Interceptor ở mức Session:

Session session = sessionFactory
.withOptions()
.interceptor(new LoggingInterceptor() )
.openSession();

Trường hợp muốn cấu hình Interceptor ở mức SessionFactory, nghĩa là tất cả Session đều áp dụng Interceptor này:

package com.gpcoder.utils;

import com.gpcoder.LoggingInterceptor;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.service.ServiceRegistry;

public class HibernateUtils {

private static final SessionFactory sessionFactory = buildSessionFactory();

private HibernateUtils() {
super();
}

private static SessionFactory buildSessionFactory() {
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder() //
.configure() // Load hibernate.cfg.xml from resource folder by default
.build();
Metadata metadata = new MetadataSources(serviceRegistry).getMetadataBuilder().build();
return metadata.getSessionFactoryBuilder()
.applyInterceptor( new LoggingInterceptor() ) // Register interceptor
.build();
}

public static SessionFactory getSessionFactory() {
return sessionFactory;
}

public static void close() {
getSessionFactory().close();
}
}

Trong trường hợp muốn đảm bảo thread-safe, chúng ta cần xác định session context trong file hibernate.cfg.xml:

<property name="current_session_context_class">org.hibernate.context.internal.ThreadLocalSessionContext</property>

1.2.3. Tạo lớp ứng dụng

package com.gpcoder;

import com.gpcoder.entities.Tag;
import com.gpcoder.utils.HibernateUtils;
import lombok.extern.log4j.Log4j2;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

@Log4j2
public class HibernateInterceptorExample {

public static void main(String[] args) {
try (SessionFactory sessionFactory = HibernateUtils.getSessionFactory();
Session session = sessionFactory
.withOptions()
.interceptor(new LoggingInterceptor()) // Register interceptor
.openSession();
) {

session.getTransaction().begin();

Tag tag = new Tag();
tag.setName("Hibernate Interceptor");
session.persist(tag);

session.getTransaction().commit();
}
}
}

Output:

2020-Apr-02 22:41:38 PM [main] DEBUG com.gpcoder.LoggingInterceptor - onPrepareStatement: select nextval ('tag_id_seq')
2020-Apr-02 22:41:38 PM [main] DEBUG org.hibernate.SQL - select nextval ('tag_id_seq')
2020-Apr-02 22:41:38 PM [main] DEBUG com.gpcoder.LoggingInterceptor - onSave: Tag(id=43, name=Hibernate Interceptor)
2020-Apr-02 22:41:38 PM [main] DEBUG com.gpcoder.LoggingInterceptor - preFlush: Tag(id=43, name=Hibernate Interceptor)
2020-Apr-02 22:41:38 PM [main] DEBUG com.gpcoder.LoggingInterceptor - onPrepareStatement: insert into Tag (name, id) values (?, ?)
2020-Apr-02 22:41:38 PM [main] DEBUG org.hibernate.SQL - insert into Tag (name, id) values (?, ?)
2020-Apr-02 22:41:38 PM [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [Hibernate Interceptor]
2020-Apr-02 22:41:38 PM [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - [43]
2020-Apr-02 22:41:38 PM [main] DEBUG com.gpcoder.LoggingInterceptor - postFlush: Tag(id=43, name=Hibernate Interceptor)

2. Hibernate StatementInspector

Một tính năng Hibernate rất hữu ích nhưng ít được biết đến là khả năng chặn (Interceptor) và sửa đổi bất kỳ câu lệnh SQL nào được tạo tự động bằng tiện ích Hibernate StatementInspector. Trong phần tiếp theo của bài viết này, chúng ta sẽ xem cơ chế Hibernate StatementInspector hoạt động như thế nào.

2.1. Create StatementInspector

Interface StatementInspector chỉ gồm 1 phương thức duy nhất inspect(String sql). Phương thức  nhận một câu lệnh SQL mà Hibernate sắp thực hiện và cho phép bạn sửa đổi câu lệnh SQL và trả nó về Hibernate tiếp tục thực thi.

Ví dụ: tạo SqlCommentStatementInspector để remove các auto comment của Hiberate.

package com.gpcoder;

import lombok.extern.log4j.Log4j2;
import org.hibernate.resource.jdbc.spi.StatementInspector;

import java.util.regex.Pattern;

@Log4j2
public class SqlCommentStatementInspector implements StatementInspector {

private static final Pattern SQL_COMMENT_PATTERN = Pattern.compile("\\/\\*.*?\\*\\/\\s*");

@Override
public String inspect(String sql) {
log.debug("Executing SQL query: {}", sql);
String sqlAfterRemovedComment = SQL_COMMENT_PATTERN.matcher(sql).replaceAll("");
log.debug("After removed: {}", sqlAfterRemovedComment);
return sqlAfterRemovedComment;
}
}

2.2. Register StatementInspector

Để đăng ký một implment của StatementInspector, mở file hibernate.cfg.xml và thêm cấu hình sau:

<!-- Enable SQL comment --> <property name="use_sql_comments">true</property> <!-- egister an implementation of the StatementInspector interface --> <property name="session_factory.statement_inspector">com.gpcoder.SqlCommentStatementInspector</property>

Lưu ý:

  • use_sql_comments : enable option này lên để cho phép Hibernate auto generate comment để minh hoạ cho ví dụ của chúng ta. Trong ứng dụng thực tế không cần enable option này cho StatementInspector cụ thể.

Một cách khác để đăng ký là thông qua SessionFactoryBuilder:

package com.gpcoder.utils;

import com.gpcoder.LoggingInterceptor;
import com.gpcoder.SqlCommentStatementInspector;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.service.ServiceRegistry;

public class HibernateUtils {

private static final SessionFactory sessionFactory = buildSessionFactory();

private HibernateUtils() {
super();
}

private static SessionFactory buildSessionFactory() {
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder() //
.configure() // Load hibernate.cfg.xml from resource folder by default
.build();
Metadata metadata = new MetadataSources(serviceRegistry).getMetadataBuilder().build();
return metadata.getSessionFactoryBuilder()
.applyStatementInspector( new SqlCommentStatementInspector() ) // Register StatementInspector
.applyInterceptor( new LoggingInterceptor() ) // Register interceptor
.build();
}

public static SessionFactory getSessionFactory() {
return sessionFactory;
}

public static void close() {
getSessionFactory().close();
}
}

2.3. Tạo lớp ứng dụng

package com.gpcoder;

import com.gpcoder.entities.Tag;
import com.gpcoder.utils.HibernateUtils;
import lombok.extern.log4j.Log4j2;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

@Log4j2
public class HibernateInterceptorExample {

public static void main(String[] args) {
try (SessionFactory sessionFactory = HibernateUtils.getSessionFactory();
Session session = sessionFactory.openSession();
) {

session.getTransaction().begin();

Tag tag = new Tag();
tag.setName("Hibernate Interceptor");
session.persist(tag);

session.getTransaction().commit();
}
}
}

Output:

2020-Apr-02 23:33:44 PM [main] DEBUG com.gpcoder.SqlCommentStatementInspector - Executing SQL query: select nextval ('tag_id_seq')
2020-Apr-02 23:33:44 PM [main] DEBUG com.gpcoder.SqlCommentStatementInspector - After removed: select nextval ('tag_id_seq')
2020-Apr-02 23:33:44 PM [main] DEBUG org.hibernate.SQL - select nextval ('tag_id_seq')
2020-Apr-02 23:33:44 PM [main] DEBUG com.gpcoder.LoggingInterceptor - onSave: Tag(id=50, name=Hibernate Interceptor)
2020-Apr-02 23:33:44 PM [main] DEBUG com.gpcoder.LoggingInterceptor - preFlush: Tag(id=50, name=Hibernate Interceptor)
2020-Apr-02 23:33:44 PM [main] DEBUG com.gpcoder.SqlCommentStatementInspector - Executing SQL query: /* insert com.gpcoder.entities.Tag */ insert into Tag (name, id) values (?, ?)
2020-Apr-02 23:33:44 PM [main] DEBUG com.gpcoder.SqlCommentStatementInspector - After removed: insert into Tag (name, id) values (?, ?)
2020-Apr-02 23:33:44 PM [main] DEBUG org.hibernate.SQL - insert into Tag (name, id) values (?, ?)
2020-Apr-02 23:33:44 PM [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [Hibernate Interceptor]
2020-Apr-02 23:33:44 PM [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - [50]
2020-Apr-02 23:33:44 PM [main] DEBUG com.gpcoder.LoggingInterceptor - postFlush: Tag(id=50, name=Hibernate Interceptor)

Như bạn thấy, comment của hibernate /* insert com.gpcoder.entities.Tag */ đã được loại bỏ một cách tự động nhờ vào StatementInspector.

Một số trường hợp khác có thể sử dụng StatementInspector: capture tất cả câu lệnh SQL được call bởi Hibernate, quản lý số lần SQL được gọi để phục vụ cho testing, reporting, …

Tài liệu tham khảo:

Bài viết gốc được đăng tải tại gpcoder.com

Có thể bạn quan tâm:

Xem thêm Việc làm Developer hấp dẫn trên TopDev