1 起因
在这篇文章中:
我们介绍了light-dao框架的基本实现。在使用了一段时间后我发现,这个框架在某些场景下,还是过重了。
比如:
select * from info where id = 10;
如果使用light-dao中原本的做法,需要这样:
@Select("select * from info where id = {0}")
List<UserInfo> selectUserInfo(int id);
当然,在sql比较简单时,这样写也很方便。但是,想要插入一条数据的时候,就很麻烦了。
需要这样:
@Update("insert into " + TABLE_NAME + "(id, name) values ({user.id}, {user.name});")
int insert(@SqlParam("user") User user);
再比如,如果user本身是分表的,目前的light-dao需要做较大的修改,需要把tableName作为参数定制,还是相当复杂的。
因此,目前的light-dao有几个比较严重的问题:
a 当表中字段比较多的时候,sql会比较复杂,很容易出问题
b 当表中需要新增字段的时候,不仅需要改model层,还需要更改dao层的sql,很容易遗漏
c 对于分表的情况支持的不好
鉴于上面的问题,我一直在思考,能不能设计一套更简单的框架,能满足下面几个条件
a 90%的条件下,不需要写sql
b 和model能更好的映射,在插入,查找,删除的时候,代码能尽量的简单,在增删字段的时候,对dao层能透明
c 比较好的支持分表需求
2 示例
在反复尝试了几次后,终于完成了上述的几个目标,我们先贴一段示例代码:
info表按id分表100个
a model层
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Info {
@PrimaryKey
int id;
String information;
int userId;
}
b dao层
@Repository
public class InfoDAO implements ShardDAOBaseGet<Info>, ShardDAOBaseInsert<Info> {
@Autowired
@Qualifier("myDbDataSource")
private DataSource myDataSource;
@Override
public Class<Info> getClazz() {
return Info.class;
}
@Override
public NamedParameterJdbcTemplate getReader() {
return new NamedParameterJdbcTemplate(myDataSource);
}
@Override
public NamedParameterJdbcTemplate getWriter() {
return new NamedParameterJdbcTemplate(myDataSource);
}
public Info getInfoById(long shardId, long id) {
return getByKey(shardId, "id", id);
}
public int insertInfo(long shardId, Info info) {
return insert(shardId, info);
}
}
这样一来,就清爽了很多。
3 源码解析
(1) DAOBase
这个接口是整个框架的核心模块,它的代码如下:
public interface DAOBase<T> {
Map<Class, RowMapper> ROW_MAPPER_MAP = new ConcurrentHashMap<>();
Map<Class, String> TABLE_NAME_MAP = new ConcurrentHashMap<>();
Map<Class, String> PRIMARY_KEY_NAME_MAP = new ConcurrentHashMap<>();
Class<T> getClazz();
@SuppressWarnings("unchecked")
default RowMapper<T> getRowMapper() {
return ROW_MAPPER_MAP.computeIfAbsent(getClazz(), BeanPropertyRowMapper::new);
}
default String getTableName() {
return TABLE_NAME_MAP.computeIfAbsent(getClazz(), (clazz) -> {
String tableName = clazz.getSimpleName();
tableName = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, tableName);
return tableName;
});
}
default String getPrimaryKeyName() {
return PRIMARY_KEY_NAME_MAP.computeIfAbsent(getClazz(), (clazz) ->
Arrays.stream(clazz.getDeclaredFields())
.filter(field ->
field.getDeclaredAnnotation(PrimaryKey.class) != null)
.findAny()
.get()
.getName());
}
default long getPrimaryKey(T model) {
return (long) ReflectUtils.getField(model, getPrimaryKeyName());
}
}
这里面一共有5个接口:
a Class<T> getClazz() 这个接口描述了model的class
b RowMapper<T> getRowMapper() 这个接口用于将数据库的一行数据映射为一个model
c String getTableName() 这个接口描述需要访问的表名
d String getPrimaryKeyName() 这个接口描述这个表的主键名(我们只支持单主键,并且只支持主键类型为long)
e long getPrimaryKey(Object model) 这个接口描述的是通过model获取主键(主要在stream访问时使用)
(2) DAOBaseGet
这个接口继承自DAOBase,主要实现了select相关接口,挑几个比较典型的接口介绍下
a <Key> T getByKey(String keyName, Key key)
这个接口相当于
select * from TABLE where key_name = key
b <Key> Stream<T> listByKeyDesc(String keyName, Key key)
这个接口相当于
select * from TABLE where key_name = key ordery by primary_key desc
c <Key extends Comparable> Stream<T> listByRangeDesc(String keyName, Range<Key> range)
这个接口相当于
select * from TABLE where key_name >(=) range.left and key_name <(=) range.right order by primary_key desc
它的实现代码如下:
default <Key extends Comparable> Stream<T> listByRangeDesc(String keyName, Range<Key> range) {
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource();
final String dbPrimaryKeyName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,
getPrimaryKeyName());
List<String> conditions = DAOUtils.buildRangeConditions(mapSqlParameterSource, keyName, range);
return CursorIterator.<Long, T>newGenericBuilder().bufferSize(BUFFER_SIZE)
.start(Long.MAX_VALUE).cursorExtractor(this::getPrimaryKey)
.build((cursor, limit) -> getReader().query(
new SqlQueryBuilder()
.select("*")
.from(getTableName())
.where(conditions)
.and(format("%s<=:cursor", dbPrimaryKeyName))
.orderBy(dbPrimaryKeyName)
.desc()
.limit(":limit")
.build(),
mapSqlParameterSource.addValue("cursor", cursor)
.addValue("limit", limit),
getRowMapper()))
.stream();
}
其中CursorIterator使用了迭代模式返回结果。
(3) DAOBaseInsert
这个接口主要用于insert or replace or update语句,还是挑几个接口介绍下
a int insert(T model)
这个语句等价于
insert into TABLE(column1, column2...) values (value1, value2...);
b long insertReturnKey(T model)
这个语句会返回自增的主键指
c int insertOnDuplicate(T model, String... conditions)
这个语句等价于
insert into TABLE(column1, column2...) values(value1, value2...)
on duplicate key update conditions
d <Key> int update(long primaryKey, String keyName, Key key)
这个语句等价于
update TABLE set key_name = key where primary_key = primaryKey
贴一下update的代码实现:
default int update(long primaryKey, List<Map.Entry<String, Object>> entryList, NamedParameterJdbcTemplate template) {
if (template == null) {
template = getWriter();
}
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource();
List<String> conditions = new ArrayList<>();
entryList.forEach(entry -> {
conditions.add(format(" %s=:%s ", CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, entry.getKey()),
entry.getKey()));
mapSqlParameterSource.addValue(entry.getKey(), entry.getValue());
});
final String dbPrimaryKeyName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,
getPrimaryKeyName());
return template.update(
new SqlUpdateBuilder()
.update(getTableName())
.set(conditions)
.where(format("%s=:%s", dbPrimaryKeyName, getPrimaryKeyName()))
.build(),
mapSqlParameterSource.addValue(getPrimaryKeyName(), primaryKey));
}
(4) 分表逻辑
分表逻辑的实现基于对表的数量取模,比如user表有100个分表,分别为user_0, user_1 ... user_99,在实现时需要指定分表的id,在我们代码中叫shardId。比如user表按userId分为100张表,则我们可以使用ShardDAOBaseGet接口,并在每个get和list相关的接口中传入userId,框架会自动完成分表逻辑。
4 使用方式
(1) 首先定义一个数据库的数据源
<!-- for example -->
<bean id="myDbDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close" lazy-init="false">
<property name="driverClassName" value="org.h2.Driver"></property>
<property name="url" value="jdbc:h2:mem:my_db"></property>
<property name="username" value="test"></property>
<property name="password" value=""></property>
</bean>
注:比较推荐的做法其实是把数据库的相关数据配置在zoomkeeper中,方便动态切换。
(2) 根据表结构构建model层,比如,表的sql语句为
create table info (id int, information varchar, user_id int);
则对应的model代码为:
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Info {
@PrimaryKey //用于指定主键
int id;
String information;
int userId;
}
注: @PrimaryKey用于指定表的主键,必须有,否则在listByXXDesc方法中会有问题。
同时注意表名和model的类名要一致,比如表名为user_info,则model类名应为UserInfo。字段名和数据库的字段也需要一致。(model使用驼峰,数据库使用下划线命名方式)
(3) 构建DAO代码
@Repository
public class InfoDAO implements ShardDAOBaseGet<Info>, ShardDAOBaseInsert<Info> {
@Autowired
@Qualifier("myDbDataSource")
private DataSource myDataSource;
@Override
public Class<Info> getClazz() {
return Info.class;
}
@Override
public NamedParameterJdbcTemplate getReader() {
return new NamedParameterJdbcTemplate(myDataSource);
}
@Override
public NamedParameterJdbcTemplate getWriter() {
return new NamedParameterJdbcTemplate(myDataSource);
}
public Info getInfoById(long shardId, long id) {
return getByKey(shardId, "id", id);
}
public int insertInfo(long shardId, Info info) {
return insert(shardId, info);
}
}
这样就可以方便的使用DAOBase里各种默认方法了。
5 源码地址
注:本次升级属于兼容性升级,原来的light-dao相关的自动注入,注解模式仍可使用。
当对数据库的使用比较轻量(不存在join表,group by等复杂操作)时,推荐使用新的DAOBase构建DAO层代码。如果确实有很多复杂的重sql逻辑,比如数据分析,数据导出等,还是建议使用原来的@Select sql注入的模式。
新的代码逻辑主要集中在
package com.littlersmall.lightdao.base
github地址
have fun