предисловие
Недавно я использовал ShardingSphere для реализации подтаблиц данных mysql на работе.Вот некоторые ямки, с которыми я столкнулся.
Введение в ShardingSphere
Apache ShardingSphere — это набор распределенных данных с открытым исходным кодом. Экосистема библиотечных решений, обеспечивающих стандартизированное горизонтальное масштабирование данных, распределенные транзакции и распределенное управление.
Проблема с обновлением поля шарда
На этот раз используется Sharding-JDBC, и разделяется только таблица, а не база данных (конечно, чтобы избежать проблемы с распределенными транзакциями, вызванной кросс-базой, ShardingSphere также поддерживает распределенные транзакции, такие как XA и Seata).
При выполнении оператора обновления, который обновляет поле сегмента, сообщается о следующей ошибке:
org.apache.shardingsphere.underlying.common.exception.ShardingSphereException: Can not update sharding key, logic table: [order], column: [org.apache.shardingsphere.sql.parser.sql.segment.dml.assignment.AssignmentSegment@2b877a54].
at org.apache.shardingsphere.sharding.route.engine.validator.impl.ShardingUpdateStatementValidator.validate(ShardingUpdateStatementValidator.java:59)
at org.apache.shardingsphere.sharding.route.engine.validator.impl.ShardingUpdateStatementValidator.validate(ShardingUpdateStatementValidator.java:42)
at org.apache.shardingsphere.sharding.route.engine.ShardingRouteDecorator.lambda$decorate$0(ShardingRouteDecorator.java:61)
at java.util.Optional.ifPresent(Optional.java:159)
...
Найдите соответствующий исходный код в соответствии со стеком вызовов
public final class ShardingUpdateStatementValidator implements ShardingStatementValidator<UpdateStatement> {
@Override
public void validate(final ShardingRule shardingRule, final UpdateStatement sqlStatement, final List<Object> parameters) {
String tableName = sqlStatement.getTables().iterator().next().getTableName().getIdentifier().getValue();
//遍历所有的set声明
for (AssignmentSegment each : sqlStatement.getSetAssignment().getAssignments()) {
String shardingColumn = each.getColumn().getIdentifier().getValue();
if (shardingRule.isShardingColumn(shardingColumn, tableName)) {
//当set语句段为分片键时进入
Optional<Object> shardingColumnSetAssignmentValue = getShardingColumnSetAssignmentValue(each, parameters);
Optional<Object> shardingValue = Optional.empty();
Optional<WhereSegment> whereSegmentOptional = sqlStatement.getWhere();
if (whereSegmentOptional.isPresent()) {
shardingValue = getShardingValue(whereSegmentOptional.get(), parameters, shardingColumn);
}
if (shardingColumnSetAssignmentValue.isPresent() && shardingValue.isPresent() && shardingColumnSetAssignmentValue.get().equals(shardingValue.get())) {
//当set子句中设置的的新值和where子句中的旧值相同可跳出
continue;
}
//抛出异常
throw new ShardingSphereException("Can not update sharding key, logic table: [%s], column: [%s].", tableName, each);
}
}
}
Анализ исходного кода показал, что при обновлении поля сегментирования только тогда, когда новое значение, установленное в предложении set, равно старому значению, указанному в предложении where, исключение не будет выдано, а другие случаи обновления полей сегментирования исключены. не разрешено!
между и разбор проблем
Поскольку в то время он преобразовывал старый проект, при преобразовании разделения таблицы исходный бизнес-код, оператор sql и т. д. не были изменены, а исходный источник данных был напрямую заменен вновь настроенным ShardingDataSource.
Но на этапе тестирования была обнаружена странная ошибка:
ava.lang.ClassCastException: org.apache.shardingsphere.sql.parser.sql.segment.dml.item.ExpressionProjectionSegment cannot be cast to org.apache.shardingsphere.sql.parser.sql.segment.dml.column.ColumnSegment
at org.apache.shardingsphere.sql.parser.mysql.visitor.MySQLVisitor.createBetweenSegment(MySQLVisitor.java:351)
at org.apache.shardingsphere.sql.parser.mysql.visitor.MySQLVisitor.visitPredicate(MySQLVisitor.java:313)
at org.apache.shardingsphere.sql.parser.mysql.visitor.MySQLVisitor.visitPredicate(MySQLVisitor.java:121)
at org.apache.shardingsphere.sql.parser.autogen.MySQLStatementParser$PredicateContext.accept(MySQLStatementParser.java:11690)
at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18)
at org.apache.shardingsphere.sql.parser.mysql.visitor.MySQLVisitor.visitBooleanPrimary(MySQLVisitor.java:273)
at org.apache.shardingsphere.sql.parser.mysql.visitor.MySQLVisitor.visitBooleanPrimary(MySQLVisitor.java:121)
at org.apache.shardingsphere.sql.parser.autogen.MySQLStatementParser$BooleanPrimaryContext.accept(MySQLStatementParser.java:11463)
at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18)
at org.apache.shardingsphere.sql.parser.mysql.visitor.MySQLVisitor.visitExpr(MySQLVisitor.java:258)
at org.apache.shardingsphere.sql.parser.mysql.visitor.MySQLVisitor.visitExpr(MySQLVisitor.java:121)
at org.apache.shardingsphere.sql.parser.autogen.MySQLStatementParser$ExprContext.accept(MySQLStatementParser.java:11241)
at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18)
at org.apache.shardingsphere.sql.parser.mysql.visitor.MySQLVisitor.visitExpr(MySQLVisitor.java:261)
at org.apache.shardingsphere.sql.parser.mysql.visitor.MySQLVisitor.visitExpr(MySQLVisitor.java:121)
at org.apache.shardingsphere.sql.parser.autogen.MySQLStatementParser$ExprContext.accept(MySQLStatementParser.java:11241)
at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractParseTreeVisitor.java:18)
Бизнес sql похож на это:
select * from order where compId = 2 and DATE_FORMAT(updateTime,'%Y-%m-%d') between DATE_FORMAT('20210616','%Y-%m-%d') and DATE_FORMAT('20210617','%Y-%m-%d')
Обратите внимание на следующее предложение между и , функция используется
Волна прямого позиционирования исходного кода (текущая версия: 4.1.1):
private PredicateSegment createBetweenSegment(final PredicateContext ctx) {
ColumnSegment column = (ColumnSegment) visit(ctx.bitExpr(0));//这一行代码类型转换异常
ExpressionSegment between = (ExpressionSegment) visit(ctx.bitExpr(1));
ExpressionSegment and = (ExpressionSegment) visit(ctx.predicate());
return new PredicateSegment(ctx.getStart().getStartIndex(), ctx.getStop().getStopIndex(), column, new PredicateBetweenRightValue(between, and));
}
Отладка точки останова:
Непосредственно вызовите visit(ctx.bitExpr(0)) и обнаружите, что возвращаемый тип — ExpressionProjectionSegment, а принудительное преобразование типа в ColumnSegment приводит к ошибке.
Согласно официальной документации и контексту кода, эта часть кода представляет собой процесс, с помощью которого механизм синтаксического анализа SQL анализирует оператор SQL в абстрактном синтаксическом дереве. Однако в методе createBetweenSegment сегмент оператора перед ключевым словом между в операторе between и по умолчанию считается сегментом столбца (ColumnSegment).Когда выражение используется перед between, полученный узел синтаксического дерева является сегментом выражения ( ВыражениеПроекцияСегмент).
В то время решением было переписать sql без использования выражений, чтобы обойти эту проблему, но позже выяснилось, что эта проблема была исправлена в последней версии (5.0.0-альфа).
private BetweenExpression createBetweenSegment(final PredicateContext ctx) {
ExpressionSegment left = (ExpressionSegment) visit(ctx.bitExpr(0));
ExpressionSegment between = (ExpressionSegment) visit(ctx.bitExpr(1));
ExpressionSegment and = (ExpressionSegment) visit(ctx.predicate());
boolean not = null != ctx.NOT();
return new BetweenExpression(ctx.start.getStartIndex(), ctx.stop.getStopIndex(), left, between, and, not);
}
При анализе сегмента между предполагается, что сегмент между является сегментом выражения по умолчанию.
Суммировать
1. При использовании оператора обновления для обновления поля сегментации разрешается устанавливать только поле сегментации так, чтобы оно совпадало со старым значением (старое значение должно быть в предложении where).
2. Выражения не поддерживаются перед ключевым словом между в версии 4.1.1 и поддерживаются в 5.0.0-альфа.