22.sharding-jdbc源码之INSERT解析

阿飞Javaer,转载请注明原创出处,谢谢!

INSERT语法

分析insert解析之前,首先看一下mysql官方对insert语法的定义,因为SQL解析跟语法息息相关:

INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name
    [PARTITION (partition_name [, partition_name] ...)]
    [(col_name [, col_name] ...)]
    {VALUES | VALUE} (value_list) [, (value_list)] ...
    [ON DUPLICATE KEY UPDATE assignment_list]

INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name
    [PARTITION (partition_name [, partition_name] ...)]
    SET assignment_list
    [ON DUPLICATE KEY UPDATE assignment_list]

INSERT [LOW_PRIORITY | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name
    [PARTITION (partition_name [, partition_name] ...)]
    [(col_name [, col_name] ...)]
    SELECT ...
    [ON DUPLICATE KEY UPDATE assignment_list]

摘自https://dev.mysql.com/doc/refman/8.0/en/insert.html

INSERT解析

接下来分析sharding-jdbc是如何解析insert类型的SQL语句的,通过SQLStatement result = sqlParser.parse();得到SQL解析器后,执行AbstractInsertParserparse()方法解析insert sql,核心源码如下:

@Override
public final DMLStatement parse() {
    lexerEngine.nextToken();
    InsertStatement result = new InsertStatement();
    insertClauseParserFacade.getInsertIntoClauseParser().parse(result);
    insertClauseParserFacade.getInsertColumnsClauseParser().parse(result);
    if (lexerEngine.equalAny(DefaultKeyword.SELECT, Symbol.LEFT_PAREN)) {
        throw new UnsupportedOperationException("Cannot INSERT SELECT");
    }
    insertClauseParserFacade.getInsertValuesClauseParser().parse(result);
    insertClauseParserFacade.getInsertSetClauseParser().parse(result);
    appendGenerateKey(result);
    return result;
}

对应的泳道图如下所示:


INSERT解析泳道图

第1步-lexerEngine.nextToken()

由parse()源码可知,insert解析第1步就是调用 lexerEngine.nextToken(),nextToken()在之前的文章已经分析过(戳链接),即跳到下一个token,由于任意SQL解析都会在SQLParsingEngine中调用lexerEngine.nextToken(),这里再调用lexerEngine.nextToken(),所以总计已经跳过两个token。

为什么要一开始就调用nextToken()呢?回到insert的语法:INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE] [INTO] tbl_name~~~INSERT INTO一定会有, LOW_PRIORITY,DELAYED ,HIGH_PRIORITY和IGNORE几个关键词可选,即在表名之前,至少有两个token。所以跳过两个token后再进行下一步操作(SQLParsingEngine.parse()中调用了一次nextToken(),这里再调用一次nextToken());

第2步-InsertIntoClauseParser.parse(result)

由parse()源码可知,insert解析第2步就是调用 insertClauseParserFacade.getInsertIntoClauseParser().parse(result);,即解析insert into后面的表名,封装到InsertStatementtable属性中,核心源码如下所示:

  1. 检查是否有不支持的关键词(跳过两个token后,只有Oracle有两个不支持的关键词:ALL和FIRST);
  2. 跳到INTO(因为INSERT和INTO之间还有其他关键词,在此之前即使调用了两次nextToken(),当前token也不一定就是INTO);
  3. 获取下一个token即表名(根据insert语法可知,INTO后肯定是表名。表名token值:Token(type=IDENTIFIER, literals=`t_user`, endPosition=27));
  4. 解析表名赋值给InsertStatement
  5. 如果表名和VALUES关键词之间有其他关键词则跳过(例如MySQL的分区insert语法:INSERT [LOW_PRIORITY | HIGH_PRIORITY] [IGNORE] [INTO] tbl_name [PARTITION (partition_name [, partition_name] ... ...
public void parse(final InsertStatement insertStatement) {
    // step1
    lexerEngine.unsupportedIfEqual(getUnsupportedKeywordsBeforeInto());
    // step2
    lexerEngine.skipUntil(DefaultKeyword.INTO);
    // step3
    lexerEngine.nextToken();
    // step4
    tableReferencesClauseParser.parse(insertStatement, true);
    // step5
    skipBetweenTableAndValues(insertStatement);
}

第3步-InsertColumnsClauseParser.parse()

由parse()源码可知,insert解析第3步就是调用 insertClauseParserFacade.getInsertColumnsClauseParser().parse(result);,即解析insert into t_user后面的列,封装到InsertStatementcolumns属性中,核心源码如下所示:

public void parse(final InsertStatement insertStatement) {
    Collection<Column> result = new LinkedList<>();
    // 如果当前token是(,即左括号,那么尝试解析括号里的insert的列名
    if (lexerEngine.equalAny(Symbol.LEFT_PAREN)) {
        // 得到insert的目标表名
        String tableName = insertStatement.getTables().getSingleTableName();
        Optional<String> generateKeyColumn = shardingRule.getGenerateKeyColumn(tableName);
        int count = 0;
        do {
            // 调用nextToken()解析到insert的某一列
            lexerEngine.nextToken();
            // 得到列名
            String columnName = SQLUtil.getExactlyValue(lexerEngine.getCurrentToken().getLiterals());
            // 封装到Column中添加到最后的结果中
            result.add(new Column(columnName, tableName));
            // 调用nextToken()进行解析(如果还没到最后就是都好,否则就是右括号)
            lexerEngine.nextToken();
            // 如果存在主键(例如id),并且当前解析的列刚好是主键列,那么记录下主键列的位置(generateKeyColumnIndex)
            if (generateKeyColumn.isPresent() && generateKeyColumn.get().equalsIgnoreCase(columnName)) {
                insertStatement.setGenerateKeyColumnIndex(count);
            }
            count++;
            // 如果遍历到右括号,或者SQL最后,就停止解析
        } while (!lexerEngine.equalAny(Symbol.RIGHT_PAREN) && !lexerEngine.equalAny(Assist.END));
        // 记录下insert最后一列的位置,即右括号前的位置,例如"insert ignore into `t_user`(user_id, status) values(? , ?)"这个SQL的status所在位置
        insertStatement.setColumnsListLastPosition(lexerEngine.getCurrentToken().getEndPosition() - lexerEngine.getCurrentToken().getLiterals().length());
        lexerEngine.nextToken();
    }
    // 将遍历得到的所有insert的列赋值给InsertStatement中的columns属性
    insertStatement.getColumns().addAll(result);
}

第4步-不支持的语法检查

由parse()源码可知,insert解析第4步就是调用如下代码,即检查是否有sharding-jdbc不支持的insert ... select ...语法(例如insert ignore into t_user(user_id, status) select 18, 'VALID' from dual),如果有就抛出UnsupportedOperationException异常:

if (lexerEngine.equalAny(DefaultKeyword.SELECT, Symbol.LEFT_PAREN)) {
    throw new UnsupportedOperationException("Cannot INSERT SELECT");
}

第5步-InsertValuesClauseParser.parse()

由parse()源码可知,insert解析第5步就是调用 insertClauseParserFacade.getInsertValuesClauseParser().parse(result);,即解析insert into sql中的value集合,封装到InsertStatementconditions属性中,通过Conditions.add()源码可知,只会添加sharding列的值,例如insert ignore into t_user(id, user_id, status)values(?, ?, ?)只有user_id是sharding列,所以只会添加它:

public void add(final Condition condition, final ShardingRule shardingRule) {
    if (shardingRule.isShardingColumn(condition.getColumn())) {
        conditions.put(condition.getColumn(), condition);
    }
}

第6步-InsertSetClauseParser.parse()

由parse()源码可知,insert解析第6步就是调用 insertClauseParserFacade.getInsertSetClauseParser().parse(result);,即解析insert into ... set ...这种语法的SQL,例如:insert into t_user set id=24, user_id=24, status='NEW';核心源码如下所示:

public void parse(final InsertStatement insertStatement) {
    // 即如果不是**insert into ... set ...**这种语法,那么return,不需要继续往下解析
    if (!lexerEngine.skipIfEqual(new Keyword[] {DefaultKeyword.SET})) {
        return;
    }
    do {
        Column column = new Column(SQLUtil.getExactlyValue(lexerEngine.getCurrentToken().getLiterals()), insertStatement.getTables().getSingleTableName());
        lexerEngine.nextToken();
        lexerEngine.accept(Symbol.EQ);
        SQLExpression sqlExpression;
        // 分析token, 根据不同类型得到不用的SQLExpression
        if (lexerEngine.equalAny(Literals.INT)) {
            sqlExpression = new SQLNumberExpression(Integer.parseInt(lexerEngine.getCurrentToken().getLiterals()));
        } else if (lexerEngine.equalAny(Literals.FLOAT)) {
            sqlExpression = new SQLNumberExpression(Double.parseDouble(lexerEngine.getCurrentToken().getLiterals()));
        } else if (lexerEngine.equalAny(Literals.CHARS)) {
            sqlExpression = new SQLTextExpression(lexerEngine.getCurrentToken().getLiterals());
        } else if (lexerEngine.equalAny(DefaultKeyword.NULL)) {
            sqlExpression = new SQLIgnoreExpression(DefaultKeyword.NULL.name());
        } else if (lexerEngine.equalAny(Symbol.QUESTION)) {
            sqlExpression = new SQLPlaceholderExpression(insertStatement.getParametersIndex());
            insertStatement.increaseParametersIndex();
        } else {
            throw new UnsupportedOperationException("");
        }
        lexerEngine.nextToken();
        if (lexerEngine.equalAny(Symbol.COMMA, DefaultKeyword.ON, Assist.END)) {
            // 说明,这里只会添加数据库或者表的sharding列
            insertStatement.getConditions().add(new Condition(column, sqlExpression), shardingRule);
        } else {
            lexerEngine.skipUntil(Symbol.COMMA, DefaultKeyword.ON);
        }
    } while (lexerEngine.skipIfEqual(Symbol.COMMA));
}

第7步-appendGenerateKey(result)

由parse()源码可知,insert解析第7步就是调用 appendGenerateKey(result),即如果TableRule申明了.generateKeyColumn("id", MyKeyGenerator.class),并且SQL中没有主键列,那么InsertStatementsqlTokens还需要增加两个token:ItemsTokenGeneratedKeyToken,核心源码如下:

private void appendGenerateKey(final InsertStatement insertStatement) {
    String tableName = insertStatement.getTables().getSingleTableName();
    Optional<String> generateKeyColumn = shardingRule.getGenerateKeyColumn(tableName);
    // generateKeyColumn存在,即generateKeyColumn.isPresent(),即TableRule定义时指定了.generateKeyColumn();例如TableRule.generateKeyColumn("id", MyKeyGenerator.class)
    if (!generateKeyColumn.isPresent()) {
        return;
    }
    // Insert SQL语句中没有TableRule定义时TableRule.generateKeyColumn("id")指定的列,例如id;
    if (null != insertStatement.getGeneratedKey()) {
        return;
    }
    ItemsToken columnsToken = new ItemsToken(insertStatement.getColumnsListLastPosition());
    columnsToken.getItems().add(generateKeyColumn.get());
    insertStatement.getSqlTokens().add(columnsToken);
    insertStatement.getSqlTokens().add(new GeneratedKeyToken(insertStatement.getValuesListLastPosition()));
}

ItemsTokenGeneratedKeyToken这两个token用于后面的SQL重写,例如将insert ignore into t_user(user_id, status) values(? , ?)这样的SQL重写为insert ignore into t_user(user_id, status, id) values(? , ?, ?)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容