JDBC 学习笔记(十)—— 使用 JDBC 搭建一个简易的 ORM 框架
1. 数据映射
当我们获取到 ResultSet 之后,显然这个不是我们想要的数据结构。
数据库中的每一个表,在 Java 代码中,一定会有一个类与之对应,例如:
package com.gerrard.entity; import com.gerrard.annotation.ColumnAnnotation; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public final class Student { private int id; private String name; private String password; }
实现数据库表和 JavaBean 之间的转换,就是 ORM(Object Relational Mapping)框架设计的目的。
为此,我定义了一个转换的接口:
package com.gerrard.orm; import java.sql.ResultSet; import java.sql.ResultSetMetaData; public interface ResultSetAdapter<T> { T transferEntity(ResultSet rs, ResultSetMetaData meta); }
2. 死办法(这小章节不知道起什么名字好)
最先想到的,无疑就是特事特办,为每一个 JavaBean 都写一个转换类:
package com.gerrard.orm; import com.gerrard.constants.ErrorCode; import com.gerrard.entity.Student; import com.gerrard.exception.JdbcSampleException; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; public final class StudentResultSetAdapter implements ResultSetAdapter<Student> { @Override public Student transferEntity(ResultSet rs, ResultSetMetaData meta) { try { int id = rs.getInt("STUDENT_ID"); String name = rs.getString("STUDENT_NAME"); String password = rs.getString("STUDENT_PASSWORD"); return new Student(id, name, password); } catch (SQLException e) { throw new JdbcSampleException(ErrorCode.MISSING_COLUMN_ERROR, "Fail to find column."); } } }
显然,这种做法对单一类很方便,但是 JavaBean 一旦增多,就会显得很冗余。
3. 反射 + 注解
观察例如 Hibernate 之类的实现,不难发现,JavaBean 的每一个与数据库列相对应的属性,都有一个 @Column 注解。
那么,我们也可以使用类似的办法。
第一步,定义一个注解。
package com.gerrard.annotation; import java.lang.annotation.*; @Documented @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface ColumnAnnotation { String column() default ""; }
第二步,将注解加到 JavaBean 中。
package com.gerrard.entity; import com.gerrard.annotation.ColumnAnnotation; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public final class Student { @ColumnAnnotation(column = "STUDENT_ID") private int id; @ColumnAnnotation(column = "STUDENT_NAME") private String name; @ColumnAnnotation(column = "STUDENT_PASSWORD") private String password; }
第三步,在创建转换类的时候,完成数据库列名-JavaBean 属性的映射关系的初始化。
第四步,对 ResultSetMetaData 分析时,使用反射,将值注入到对应的 Field 中。
package com.gerrard.orm; import com.gerrard.annotation.ColumnAnnotation; import com.gerrard.constants.ErrorCode; import com.gerrard.exception.JdbcSampleException; import java.lang.reflect.Field; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.util.HashMap; import java.util.Map; public final class FlexibleResultSetAdapter<T> implements ResultSetAdapter<T> { private Map<String, Field> columnMap = new HashMap<>(); private Class<T> clazz; public FlexibleResultSetAdapter(Class<T> clazz) { this.clazz = clazz; initColumnMap(clazz); } private void initColumnMap(Class<T> clazz) { for (Field field : clazz.getDeclaredFields()) { ColumnAnnotation annotation = field.getAnnotation(ColumnAnnotation.class); columnMap.put(annotation.column(), field); } } @Override public T transferEntity(ResultSet rs, ResultSetMetaData meta) { try { T t = clazz.newInstance(); for (int i = 1; i <= meta.getColumnCount(); ++i) { String dbColumn = meta.getColumnName(i); Field field = columnMap.get(dbColumn); if (field == null) { throw new JdbcSampleException(ErrorCode.MISSING_COLUMN_ERROR, "Fail to find column " + dbColumn + "."); } field.setAccessible(true); field.set(t, rs.getObject(i)); } return t; } catch (Exception e) { String msg = "Fail to get ORM relation for class: " + clazz.getName(); throw new JdbcSampleException(ErrorCode.MISSING_COLUMN_ERROR, msg); } } }
最后,对 ORM 进行封装。
package com.gerrard.executor; import com.gerrard.constants.ErrorCode; import com.gerrard.exception.JdbcSampleException; import com.gerrard.orm.ResultSetAdapter; import com.gerrard.util.Connector; import com.gerrard.util.DriverLoader; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.LinkedList; import java.util.List; @NoArgsConstructor @AllArgsConstructor public final class SqlExecutorStatement<T> implements SqlExecutor<T> { private ResultSetAdapter<T> adapter; @Override public int executeUpdate(String sql) { DriverLoader.loadSqliteDriver(); try (Connection conn = Connector.getSqlConnection(); Statement stmt = conn.createStatement()) { return stmt.executeUpdate(sql); } catch (SQLException e) { String msg = "Fail to execute query using statement."; throw new JdbcSampleException(ErrorCode.EXECUTE_UPDATE_FAILURE, msg); } } @Override public List<T> executeQuery(String sql) { DriverLoader.loadSqliteDriver(); try (Connection conn = Connector.getSqlConnection(); Statement stmt = conn.createStatement()) { List<T> list = new LinkedList<>(); try (ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { list.add(adapter.transferEntity(rs, rs.getMetaData())); } } return list; } catch (SQLException e) { String msg = "Fail to execute query using statement."; throw new JdbcSampleException(ErrorCode.EXECUTE_QUERY_FAILURE, msg); } } }
这样一来,对于 JDBC 学习笔记(六)—— PreparedStatement 中 SQL 注入的例子,应该有更好的理解。