Khi sử dụng MyBatis để truy vấn cơ sở dữ liệu, nếu mức độ ghi log được cấu hình ở chế độ debug, hệ thống sẽ tự động in ra câu truy vấn SQL, các tham số truyền vào và số lượng bản ghi kết quả, ví dụ như:
==> Preparing: SELECT id, description FROM demo WHERE id = ?
==> Parameters: 1(Integer)
<== Total: 1
Trong khi đó, với JDBC thông thường, để in ra các thông tin tương tự, chúng ta phải thêm logic ghi log thủ công vào từng bước:
String sql = "SELECT id, description FROM demo WHERE id = ?";
System.out.println("==> Preparing: " + sql);
PreparedStatement psmt = conn.prepareStatement(sql);
System.out.println("==> Parameters: 1(Integer)");
psmt.setInt(1, 1);
ResultSet rs = psmt.executeQuery();
int count = 0;
while (rs.next()) {
count++;
}
System.out.println("<== Total: " + count);
Lý do là chúng ta không thể thay đổi mã nguồn của JDBC — không thể yêu cầu đối tượng PreparedStatement tự động in ra câu truy vấn khi được tạo. Điều này dẫn đến việc cần phải bổ sung logic ghi log tại nhiều điểm: khi lấy PreparedStatement, khi thiết lập tham số, và khi xử lý ResultSet. Đây là một giải pháp gây rối và khó duy trì.
Tuy nhiên, đây chính là trường hợp điển hình mà mô hình proxy (giao diện giả) có thể áp dụng: tạo ra một lớp proxy để bao bọc và mở rộng hành vi của đối tượng gốc mà không thay đổi mã nguồn gốc.
Quay lại với MyBatis — mặc dù nó vẫn dựa trên JDBC để giao tiếp với cơ sở dữ liệu, nhưng như một framework tầng dữ liệu, MyBatis đã tận dụng dynamic proxy để mở rộng chức năng gốc của JDBC. Các thành phần liên quan nằm trong gói org.apache.ibatis.logging.jdbc. Dưới đây là phân tích chi tiết từ phương thức getMapper() để hiểu cách MyBatis in ra SQL.
MyBatis hỗ trợ hai cách gọi API: một là dùng chuỗi ID statement, hai là dùng interface Mapper. Tuy nhiên, cả hai đều dẫn đến cùng một luồng xử lý. Ta bắt đầu từ phương thức getMapper() trong SqlSession:
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
}
Đối tượng Configuration lưu trữ cấu hình đã được phân tích. Tiếp tục đi sâu vào:
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
}
MapperRegistry giữ vai trò quản lý mối quan hệ giữa các interface Mapper và factory tạo proxy tương ứng:
public class MapperRegistry {
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> factory = (MapperProxyFactory<T>) knownMappers.get(type);
if (factory == null) {
throw new BindingException("Type " + type + " is not registered.");
}
try {
return factory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error creating mapper instance.", e);
}
}
}
MapperProxyFactory là nhà máy tạo ra các đối tượng proxy cho interface Mapper, sử dụng dynamic proxy để sinh ra lớp proxy thực tế:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
mapperProxy
);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
Khi gọi phương thức trong interface Mapper, tất cả đều được điều hướng qua MapperProxy — nơi thực hiện việc ánh xạ phương thức đến câu lệnh SQL trong file XML tương ứng. Cụ thể:
public class MapperProxy<T> implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
}
Tiếp theo, phương thức execute() trong MapperMethod sẽ xác định loại truy vấn (SELECT, INSERT, UPDATE, DELETE), rồi chuyển hướng đến thực thi phù hợp. Với truy vấn SELECT, dòng quan trọng nhất là:
result = sqlSession.selectOne(command.getName(), param);
Phương thức này được triển khai trong DefaultSqlSession, và cuối cùng sẽ gọi đến Executor để thực thi câu truy vấn:
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database.", e);
} finally {
ErrorContext.instance().reset();
}
}
Executor mặc định là SimpleExecutor. Trong quá trình thực thi, phương thức doQuery() sẽ khởi tạo StatementHandler và chuẩn bị câu truy vấn:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
Điểm then chốt nằm ở đây: khi lấy Connection, MyBatis kiểm tra xem có bật chế độ debug hay không. Nếu có, nó sẽ tạo một proxy cho Connection:
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
Việc tạo proxy được thực hiện bằng Proxy.newProxyInstance(), với InvocationHandler là ConnectionLogger:
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
Khi gọi phương thức prepareStatement() trên proxy, phương thức invoke() của ConnectionLogger sẽ được kích hoạt:
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug("Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
}
// ...
}
Ở đây, câu truy vấn được in ra ngay trước khi tạo PreparedStatement. Ngoài ra, MyBatis cũng áp dụng kỹ thuật tương tự với PreparedStatement — tạo proxy để in ra tham số và kết quả.
Cần lưu ý rằng, mức độ ghi log được kiểm soát bởi cấp độ ghi log của package chứa interface Mapper. Chỉ cần cấu hình level log là DEBUG cho package đó, bạn sẽ thấy được câu SQL được in ra.
Cuối cùng, một câu hỏi còn tồn tại: log object từ đâu ra? MyBatis không tích hợp sẵn bộ ghi log nào — thay vào đó, nó hỗ trợ nhiều hệ thống logging phổ biến như SLF4J, Commons Logging, JDK Logging...
Điều này được thực hiện nhờ vào Adapter Pattern (mô hình thích hợp): MyBatis định nghĩa một interface ghi log riêng (org.apache.ibatis.logging.Log), và cung cấp các adapter để chuyển đổi giữa các thư viện logging khác nhau. Hệ thống LogFactory sẽ tự động chọn và tải đúng implementation dựa trên thư viện đang có trong classpath.
public final class LogFactory {
private static Constructor<? extends Log> logConstructor;
static {
tryImplementation(() -> useSlf4jLogging());
tryImplementation(() -> useCommonsLogging());
tryImplementation(() -> useJdkLogging());
// ...
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
throw new LogException("Cannot create logger for " + logger, t);
}
}
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// bỏ qua lỗi nếu không tìm thấy
}
}
}
private static void setImplementation(Class<? extends Log> implClass) {
try {
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Using logging adapter: " + implClass);
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Failed to set logging implementation.", t);
}
}
}
Hệ thống sẽ lần lượt thử tải các implementation. Khi tìm thấy một thư viện logging nào đó có mặt trong classpath, nó sẽ chọn và thiết lập làm default. Từ đó, mọi log đều được gửi tới đúng hệ thống.
Tóm lại, quy trình in ra SQL trong MyBatis gồm hai yếu tố chính:
- Sử dụng dynamic proxy để bao bọc các đối tượng JDBC như
ConnectionvàPreparedStatement, thêm logic in log. - Áp dụng adapter pattern để tích hợp với các hệ thống logging phổ biến, đảm bảo tính linh hoạt và khả năng tùy chỉnh.