MyBatis 分表實踐

1. 前言

項目開發接到需求,要求將業務數據按月歸檔,也就是每個月的數據單獨保留在一張表中,每個月都要生成新表。以前從沒有遇到過這樣的場景,快速思考實現方案,主要的難點如下:

  1. 項目使用 MyBatis 框架,ORM 的思想是一個 bean 映射一張表,如何實現一個 bean 對象映射多張結構相同而名稱不同的表
  2. 每月生成新表,如何知道數據庫是否已經存在當月表,不存在時如何創建新表

幸虧 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]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章