本文带大家实现一个支持分表的ORM框架,通过简单代码实现来理解核心原理
惯例贴出GitHub地址:https://github.com/whiteBX/worm
首先明确一个我们要实现的效果,然后再考虑实现,下面以user表作为例子:
@Table("user_")
@SplitKey(column = "id", tableNum = 8)
public class UserDO {
@Column(value = "id", columnType = JDBCType.BIGINT)
private Long id;
@Column(value = "user_name", columnType = JDBCType.VARCHAR)
private String userName;
@Column(value = "real_name", columnType = JDBCType.VARCHAR)
private String realName;
@Column(value = "phone", columnType = JDBCType.VARCHAR)
private String phone;
@Column(value = "sex", columnType = JDBCType.TINYINT)
private short sex;
// 省略getter/setter
}
我们想要实现的就是在UserDO上加上自定义的Table、SplitKey、Column注解,之后就可以通过操作对象的方式来实现对于用户表的增删改查,并且能支持根据SplitKey中配置的列和表数量来自动实现分表操作。
首先来定义之前用到的三个注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String value();
JDBCType columnType();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SplitKey {
/**
* 分库分表的列,其类型必须为Long
*/
String column();
/**
* 分表数量
*/
int tableNum();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
接下来是如何使用这些注解,这里我们用一个ReflectUtil来封装出从类中解析表名、分表规则、列名等操作:
public class ReflectUtil {
/**
* 解析表名
*/
public static String parseTable(Class clazz) {
AssertUtil.isTrue(clazz.isAnnotationPresent(Table.class), ErrorCode.ACCESS_ERROR, "class need annotation Table");
Table table = (Table) clazz.getDeclaredAnnotation(Table.class);
return table.value();
}
/**
* 解析分表规则
*/
public static SplitKey parseSplitKey(Class clazz) {
AssertUtil.isTrue(clazz.isAnnotationPresent(SplitKey.class), ErrorCode.ACCESS_ERROR, "class need annotation SplitKey");
return (SplitKey) clazz.getDeclaredAnnotation(SplitKey.class);
}
/**
* 解析列
*/
public static Column parseColumn(Class clazz, String fieldName) {
try {
return clazz.getDeclaredField(fieldName).getDeclaredAnnotation(Column.class);
} catch (NoSuchFieldException e) {
throw new BizException(ErrorCode.ACCESS_ERROR, "parse Column error");
}
}
/**
* 反射获取对象的属性
*/
public static Object getFieldValue(Object obj, String fieldName) {
Field field = null;
boolean access = false;
try {
field = obj.getClass().getDeclaredField(fieldName);
access = field.isAccessible();
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
throw new BizException(ErrorCode.ACCESS_ERROR, "get Field value error: " + e.getMessage());
} finally {
field.setAccessible(access);
}
}
/**
* 解析获取tableMeta
*/
public static TableMeta parseMeta(Class clazz) {
TableMeta meta = new TableMeta();
meta.setTableName(parseTable(clazz));
meta.setTableClass(clazz);
meta.setSplitKey(parseSplitKey(clazz));
for (Field field : clazz.getDeclaredFields()) {
Column column = parseColumn(clazz, field.getName());
meta.addColumnMeta(field.getName(), column.value(), column.columnType());
}
return meta;
}
}
上面工具类中引入了一个TableMeta类,这个类是我们用来缓存所有标注了Table注解的类的信息的,方便后面直接使用,也比每次去对表增删改查时新反射解析一次效率更高,其代码如下:
public class TableMeta {
/**
* 表名
*/
private String tableName;
/**
* 对应实体类型
*/
private Class tableClass;
/**
* 分库分表
*/
private SplitKey splitKey;
/**
* 列属性
*/
private List<ColumnMeta> columnMetaList;
public class ColumnMeta {
/**
* 字段名
*/
private String field;
/**
* 列名
*/
private String column;
/**
* 列类型
*/
private JDBCType jdbcType;
public ColumnMeta(String field, String column, JDBCType jdbcType) {
this.field = field;
this.column = column;
this.jdbcType = jdbcType;
}
}
public ColumnMeta getColumnMetaByColumn(String columnName) {
for (ColumnMeta columnMeta : columnMetaList) {
if (columnMeta.equals(columnMeta.column)) {
return columnMeta;
}
}
return null;
}
public ColumnMeta getColumnMetaByField(String field) {
for (ColumnMeta columnMeta : columnMetaList) {
if (field.equals(columnMeta.field)) {
return columnMeta;
}
}
return null;
}
public void addColumnMeta(String field, String column, JDBCType jdbcType) {
if (this.columnMetaList == null) {
columnMetaList = new ArrayList<>();
}
columnMetaList.add(new ColumnMeta(field, column, jdbcType));
}
//省略getter/setter
}
通过上面我们已经可以获取到表名、列这些需要的属性了,接下来来获取分表情况下实际的表名:
public class DefaultSplitStrategy {
public static String getRealTableName(Object o) {
TableMeta tableMeta = OrmHolder.get(o.getClass());
AssertUtil.notNull(tableMeta, ErrorCode.ACCESS_ERROR, "");
// not split
if (tableMeta.getSplitKey() == null) {
return tableMeta.getTableName();
}
SplitKey splitKey = tableMeta.getSplitKey();
Object splitValue = ReflectUtil.getFieldValue(o, splitKey.column());
AssertUtil.isTrue(splitValue instanceof Long, ErrorCode.ACCESS_ERROR, "split key must be long: " + splitKey.column());
return tableMeta.getTableName() + (Long) splitValue % splitKey.tableNum();
}
}
这里默认实现采用了取余的方式,如果需要更复杂的分表方式可以自行扩充。
到这一步已经拿到了数据库的所有信息了,包括:实际的表名、列名、列属性、列的值。接下来要做的就是根据增删该查来把SQL拼出来即可:
public class SqlUtil {
public static String buildInsertSQL(String tableName, Object obj) {
StringBuilder sql = new StringBuilder();
StringBuilder valueStr = new StringBuilder();
sql.append("Insert into ").append(tableName).append(" (");
valueStr.append("VALUES (");
List<TableMeta.ColumnMeta> columnMetaList = OrmHolder.get(obj.getClass()).getColumnMetaList();
for (TableMeta.ColumnMeta columnMeta : columnMetaList) {
sql.append(columnMeta.getColumn()).append(",");
valueStr.append("'").append(ReflectUtil.getFieldValue(obj, columnMeta.getField())).append("',");
}
sql.deleteCharAt(sql.lastIndexOf(","));
valueStr.deleteCharAt(valueStr.lastIndexOf(","));
valueStr.append(")");
sql.append(") ").append(valueStr);
return sql.toString();
}
}
这里只实现了拼装insert的sql,删改查原理差不多,大家自行拼一下即可。最后通过继承JdbcTemplate来实现一个我们自己的JdbcTemplate把所需要的东西组装起来就可以了:
public class WJdbcTemplate extends JdbcTemplate implements JdbcOperation {
public WJdbcTemplate(DataSource dataSource) {
super(dataSource);
}
public int insert(Object obj) {
return this.update(SqlUtil.buildInsertSQL(DefaultSplitStrategy.getRealTableName(obj), obj));
}
}
下面是测试代码:
@Configuration
@PropertySource("classpath:applications.properties")
public class BeanConfig {
@Bean
@ConfigurationProperties()
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean(name = "dataSource")
public DataSource dataSource(DataSourceProperties dataSourceProperties) {
return DataSourceBuilder.create(dataSourceProperties.getClass().getClassLoader())
.type(HikariDataSource.class)
.driverClassName(dataSourceProperties.getDriverClassName())
.url(dataSourceProperties.getJdbcUrl())
.username(dataSourceProperties.getUser())
.password(dataSourceProperties.getPassword())
.build();
}
@Bean(name = "wJdbcTemplate")
public JdbcTemplate jdbcTemplate(
@Qualifier("dataSource") DataSource dataSource) {
return new WJdbcTemplate(dataSource);
}
}
@RunWith(SpringRunner.class)
@SpringBootApplication
@ComponentScan("org.white.worm")
@SpringBootTest
public class TestOrm {
@Resource
private WJdbcTemplate wJdbcTemplate;
@Test
public void testInsert() {
UserDO userDO = new UserDO();
int count = 0;
for (long i = 0; i < 1000; i++) {
userDO.setId(i);
userDO.setUserName("white" + i);
userDO.setRealName("白" + i);
userDO.setPhone("13100000000");
userDO.setSex((short) (i % 2));
count += wJdbcTemplate.insert(userDO);
}
System.out.println(count);
}
}
启动mysql并创建好user_0 ~ user_7之后,运行TestOrm中的testInsert方法,我们发现数据库都按主键取余的方式插入了对应的表中。
至此我们一个自定义的支持分表的ORM框架就完成了!