本文共 8643 字,大约阅读时间需要 28 分钟。
本文带大家实现一个支持分表的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 ListcolumnMetaList; 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 ("); ListcolumnMetaList = 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")@SpringBootTestpublic 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框架就完成了!
转载地址:http://rhsni.baihongyu.com/