支持分表的ORM框架实现

本文带大家实现一个支持分表的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框架就完成了!

你可能感兴趣的:(JAVA,数据库)