1. 前言
項目開發接到需求,要求將業務數據按月歸檔,也就是每個月的數據單獨保留在一張表中,每個月都要生成新表。以前從沒有遇到過這樣的場景,快速思考實現方案,主要的難點如下:
- 項目使用
MyBatis
框架,ORM
的思想是一個 bean 映射一張表,如何實現一個 bean 對象映射多張結構相同而名稱不同的表?- 每月生成新表,如何知道數據庫是否已經存在當月表,不存在時如何創建新表?
幸虧 MyBatis
這個輕量級的 ORM
框架爲手寫 SQL 語句留下了餘地,否則只能把活交給 DBA
去琢磨了。我們知道 MyBatis
通過 Mapper
去操作數據庫,並且可以自行手寫靈活的 SQL
語句,這就給了我們極大的便利
2. 動態創建表
2.1 查詢數據庫是否存在目標表
基於 Mapper
定義接口方法,在方法上添加註解@Select
自行寫好 SQL 語句。這條語句將數據庫名稱
和表名稱
作爲入參,從數據庫本身保存的表信息中統計目標數據庫中目標表的數量,通過其返回值可判斷目標表是否存在
@Repository
public interface ActionEventMapper extends BaseMapper<ActionEvent> {
@Select("SELECT count(*) FROM information_schema.`tables` WHERE TABLE_SCHEMA = #{dbName} " +
"AND TABLE_NAME = #{tableName}")
int countTable(@Param("tableName") String tableName, @Param("dbName") String dbName);
}
2.2 動態創建表
同樣的,將手寫的 SQL 語句映射到接口方法上,將表名作爲參數傳入,完成目標表的動態創建。需注意 MySql 中對錶名的格式有要求,連接符必須使用下劃線_
,否則會有語法錯誤。另外SQL 語句中表名使用 ${} 直接拼接,而不是使用 #{} 佔位符
@Repository
public interface ActionEventMapper extends BaseMapper<ActionEvent> {
@Update("CREATE TABLE IF NOT EXISTS ${tableName}(" +
" `FuiId` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵'," +
" `FuiEventType` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '事件類型'," +
" `FuiMicroSeconds` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '微秒'," +
" `FuiCreateTime` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '創建時間'," +
" `FuiUpdateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間'," +
" `FuiCasVersion` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'cas'," +
" PRIMARY KEY (`FuiId`)," +
")ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET=utf8 COMMENT='按月歸檔表';")
@CacheEvict(value = "action_event_cache", key = "1")
void createNewTable(@Param("tableName") String tableName);
2.3 定時創建表
使用 Spring 框架自帶的定時任務註解@Scheduled
創建 cron
任務,定時創建表
@Slf4j
@Component
public class CreateTableJob {
@Autowired
private ActionEventRepository actionEventRepository;
/**
* 每月 28 號 00:00:00 創建下月的表
*/
@Scheduled(cron = "0 0 0 28 1-12 ?")
public void createTable() {
String tableName = actionEventTableUtil.getNextMonthTableName();
log.warn("It is time to create next month table:{}", tableName);
try {
actionEventRepository.createTable(tableName);
} catch (Exception e) {
log.warn("Cron job create next month table:" + tableName + "fail!", e);
}
}
}
3. 數據插入
3.1 單條數據插入
單條數據的插入非常簡單,只需要注意將表名入參,並使用 ${tableName} 拼接表名
@Repository
public interface ActionEventMapper extends BaseMapper<ActionEvent> {
@Insert("INSERT INTO ${tableName}(FuiEventType, FuiMicroSeconds, FuiCreateTime, FuiCasVersion) VALUES "
+ "(#{actionEvent.eventType}, #{actionEvent.microSeconds}, #{actionEvent.createTime}, #{actionEvent.casVersion})")
// @Options 註解將插入表時主鍵字段 FuiId 生成的值回填到 bean 對象 actionEvent 的 id 屬性
@Options(useGeneratedKeys = true, keyProperty = "actionEvent.id", keyColumn = "FuiId")
int save(@Param(value = "actionEvent") ActionEvent actionEvent, @Param("tableName") String tableName);
3.2 批量插入
多條數據的批量插入相對複雜,SQL 語句爲類似腳本的形式,註解@Insert
中不再是一個很長的字符串,而是一個字符串數組
@Repository
public interface ActionEventMapper extends BaseMapper<ActionEvent> {
@Insert({"<script>",
"INSERT INTO ${tableName}(FuiEventType, FuiMicroSeconds, FuiCreateTime, FuiCasVersion) VALUES ",
"<foreach collection='matchActionEvents' item='matchActionEvent' index='index' separator=','>",
"(#{actionEvent.eventType}, #{actionEvent.microSeconds}, #{actionEvent.createTime}, #{actionEvent.casVersion})",
"</foreach>",
"</script>"})
@Options(useGeneratedKeys = true, keyProperty = "param1.id", keyColumn = "FuiId")
int saveBatch(@Param(value = "actionEvent") List<ActionEvent> actionEvents,
@Param("tableName") String tableName);
3.3 注意
使用@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "FuiId")
將表生成的主鍵回填到 bean 對象對應屬性的時候需注意,keyProperty
需要指定爲對應入參的對應屬性,形式爲參數名.id
。一般報錯會有如下信息,表明了可用參數,對於批量插入一般是使用param1.id
Specified key properties are [id] and available parameters are [actionEvent, param1, tableName, param2]