diff --git a/CHANGELOG.md b/CHANGELOG.md index b4c7526c2..2d46635fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,9 @@ Other important changes: With this change, the exception thrown is more predictable and the error is caught before sending the SQL to the database. - All the paging methods (limit, offset, fetchFirst) now have "WhenPresent" variations that will drop the phrase from - rendering if a null value is passed in + rendering if a null value is passed in +- The JOIN syntax is updated and now allows full boolean expressions like a WHERE clause. The prior JOIN syntax + is deprecated and will be removed in a future release. ## Release 1.5.2 - June 3, 2024 diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index df4292653..2cf6ac277 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -56,10 +56,6 @@ import org.mybatis.dynamic.sql.select.function.Substring; import org.mybatis.dynamic.sql.select.function.Subtract; import org.mybatis.dynamic.sql.select.function.Upper; -import org.mybatis.dynamic.sql.select.join.EqualTo; -import org.mybatis.dynamic.sql.select.join.EqualToValue; -import org.mybatis.dynamic.sql.select.join.JoinCondition; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.update.UpdateDSL; import org.mybatis.dynamic.sql.update.UpdateModel; import org.mybatis.dynamic.sql.util.Buildable; @@ -433,20 +429,36 @@ static AndOrCriteriaGroup and(List subCriteria) { } // join support - static JoinCriterion and(BindableColumn joinColumn, JoinCondition joinCondition) { - return new JoinCriterion.Builder() - .withConnector("and") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) + static ColumnAndConditionCriterion on(BindableColumn joinColumn, VisitableCondition joinCondition) { + return ColumnAndConditionCriterion.withColumn(joinColumn) + .withCondition(joinCondition) .build(); } - static JoinCriterion on(BindableColumn joinColumn, JoinCondition joinCondition) { - return new JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) - .build(); + /** + * Starting in version 2.0.0, this function is a synonym for {@link SqlBuilder#isEqualTo(BasicColumn)}. + * + * @param column the column + * @param the column type + * @return an IsEqualToColumn condition + * @deprecated since 2.0.0. Please replace with isEqualTo(column) + */ + @Deprecated(since = "2.0.0", forRemoval = true) + static IsEqualToColumn equalTo(BindableColumn column) { + return isEqualTo(column); + } + + /** + * Starting in version 2.0.0, this function is a synonym for {@link SqlBuilder#isEqualTo(Object)}. + * + * @param value the value + * @param the column type + * @return an IsEqualTo condition + * @deprecated since 2.0.0. Please replace with isEqualTo(value) + */ + @Deprecated(since = "2.0.0", forRemoval = true) + static IsEqualTo equalTo(T value) { + return isEqualTo(value); } // case expressions @@ -460,14 +472,6 @@ static SearchedCaseDSL case_() { return SearchedCaseDSL.searchedCase(); } - static EqualTo equalTo(BindableColumn column) { - return new EqualTo<>(column); - } - - static EqualToValue equalTo(T value) { - return new EqualToValue<>(value); - } - // aggregate support static CountAll count() { return new CountAll(); diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java index 825eaa699..a0423de08 100644 --- a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java @@ -32,8 +32,8 @@ import org.mybatis.dynamic.sql.where.AbstractWhereStarter; import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; -public class DeleteDSL extends AbstractWhereStarter.DeleteWhereBuilder, DeleteDSL> - implements Buildable { +public class DeleteDSL implements AbstractWhereStarter.DeleteWhereBuilder, DeleteDSL>, + Buildable { private final Function adapterFunction; private final SqlTable table; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java index d82076a49..a1a44d2ab 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java @@ -23,11 +23,13 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.SqlCriterion; import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.TableExpression; import org.mybatis.dynamic.sql.exception.DuplicateTableAliasException; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.select.join.JoinModel; import org.mybatis.dynamic.sql.select.join.JoinSpecification; import org.mybatis.dynamic.sql.select.join.JoinType; @@ -37,9 +39,9 @@ public abstract class AbstractQueryExpressionDSL, T extends AbstractQueryExpressionDSL> - extends AbstractWhereStarter { + implements AbstractWhereStarter { - private final List joinSpecificationBuilders = new ArrayList<>(); + private final List> joinSpecificationSuppliers = new ArrayList<>(); private final Map tableAliases = new HashMap<>(); private final TableExpression table; @@ -51,151 +53,151 @@ public TableExpression table() { return table; } - public T join(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.INNER, Arrays.asList(andJoinCriteria)); + public T join(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.INNER, Arrays.asList(andJoinCriteria)); return getThis(); } - public T join(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { + public T join(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { addTableAlias(joinTable, tableAlias); return join(joinTable, onJoinCriterion, andJoinCriteria); } - public T join(SqlTable joinTable, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.INNER, andJoinCriteria); + public T join(SqlTable joinTable, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.INNER, andJoinCriteria); return getThis(); } - public T join(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { + public T join(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { addTableAlias(joinTable, tableAlias); return join(joinTable, onJoinCriterion, andJoinCriteria); } - public T join(Buildable subQuery, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.INNER, + public T join(Buildable subQuery, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.INNER, andJoinCriteria); return getThis(); } - public T leftJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.LEFT, Arrays.asList(andJoinCriteria)); + public T leftJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.LEFT, Arrays.asList(andJoinCriteria)); return getThis(); } - public T leftJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { + public T leftJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { addTableAlias(joinTable, tableAlias); return leftJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T leftJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.LEFT, andJoinCriteria); + public T leftJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.LEFT, andJoinCriteria); return getThis(); } - public T leftJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { + public T leftJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { addTableAlias(joinTable, tableAlias); return leftJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T leftJoin(Buildable subQuery, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.LEFT, + public T leftJoin(Buildable subQuery, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.LEFT, andJoinCriteria); return getThis(); } - public T rightJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.RIGHT, Arrays.asList(andJoinCriteria)); + public T rightJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.RIGHT, Arrays.asList(andJoinCriteria)); return getThis(); } - public T rightJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { + public T rightJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { addTableAlias(joinTable, tableAlias); return rightJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T rightJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.RIGHT, andJoinCriteria); + public T rightJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.RIGHT, andJoinCriteria); return getThis(); } - public T rightJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { + public T rightJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { addTableAlias(joinTable, tableAlias); return rightJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T rightJoin(Buildable subQuery, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.RIGHT, + public T rightJoin(Buildable subQuery, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.RIGHT, andJoinCriteria); return getThis(); } - public T fullJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.FULL, Arrays.asList(andJoinCriteria)); + public T fullJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.FULL, Arrays.asList(andJoinCriteria)); return getThis(); } - public T fullJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { + public T fullJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { addTableAlias(joinTable, tableAlias); return fullJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T fullJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.FULL, andJoinCriteria); + public T fullJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.FULL, andJoinCriteria); return getThis(); } - public T fullJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { + public T fullJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { addTableAlias(joinTable, tableAlias); return fullJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T fullJoin(Buildable subQuery, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.FULL, + public T fullJoin(Buildable subQuery, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.FULL, andJoinCriteria); return getThis(); } - private void addJoinSpecificationBuilder(TableExpression joinTable, JoinCriterion onJoinCriterion, - JoinType joinType, List> andJoinCriteria) { - joinSpecificationBuilders.add(new JoinSpecification.Builder() + private void addJoinSpecificationSupplier(TableExpression joinTable, SqlCriterion onJoinCriterion, + JoinType joinType, List andJoinCriteria) { + joinSpecificationSuppliers.add(() -> new JoinSpecification.Builder() .withJoinTable(joinTable) .withJoinType(joinType) - .withJoinCriterion(onJoinCriterion) - .withJoinCriteria(andJoinCriteria)); + .withInitialCriterion(onJoinCriterion) + .withSubCriteria(andJoinCriteria).build()); } - protected void addJoinSpecificationBuilder(JoinSpecification.Builder builder) { - joinSpecificationBuilders.add(builder); + protected void addJoinSpecificationSupplier(Supplier joinSpecificationSupplier) { + joinSpecificationSuppliers.add(joinSpecificationSupplier); } protected Optional buildJoinModel() { - if (joinSpecificationBuilders.isEmpty()) { + if (joinSpecificationSuppliers.isEmpty()) { return Optional.empty(); } - return Optional.of(JoinModel.of(joinSpecificationBuilders.stream() - .map(JoinSpecification.Builder::build) - .toList())); + return Optional.of(JoinModel.of(joinSpecificationSuppliers.stream() + .map(Supplier::get) + .toList())); } protected void addTableAlias(SqlTable table, String tableAlias) { diff --git a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java index 42a20e551..0c244481c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java @@ -77,7 +77,7 @@ public OffsetFirstFinisher offsetWhenPresent(Long offset) { @Override public FetchFirstFinisher fetchFirstWhenPresent(Long fetchFirstRows) { this.fetchFirstRows = fetchFirstRows; - return () -> MultiSelectDSL.this; + return () -> this; } @NotNull diff --git a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java index 4e8bb4d76..c4e2b5d9a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java @@ -23,15 +23,17 @@ import java.util.function.Consumer; import org.jetbrains.annotations.NotNull; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; import org.mybatis.dynamic.sql.CriteriaGroup; import org.mybatis.dynamic.sql.SortSpecification; import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; import org.mybatis.dynamic.sql.configuration.StatementConfiguration; -import org.mybatis.dynamic.sql.select.join.JoinCondition; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.select.join.JoinSpecification; import org.mybatis.dynamic.sql.select.join.JoinType; import org.mybatis.dynamic.sql.util.Buildable; @@ -345,50 +347,57 @@ public JoinSpecificationStarter(TableExpression joinTable, JoinType joinType) { this.joinType = joinType; } - public JoinSpecificationFinisher on(BindableColumn joinColumn, JoinCondition joinCondition) { + public JoinSpecificationFinisher on(BindableColumn joinColumn, VisitableCondition joinCondition) { return new JoinSpecificationFinisher(joinTable, joinColumn, joinCondition, joinType); } - public JoinSpecificationFinisher on(BindableColumn joinColumn, JoinCondition onJoinCondition, - JoinCriterion... andJoinCriteria) { - return new JoinSpecificationFinisher(joinTable, joinColumn, onJoinCondition, joinType, andJoinCriteria); + public JoinSpecificationFinisher on(BindableColumn joinColumn, VisitableCondition onJoinCondition, + AndOrCriteriaGroup... subCriteria) { + return new JoinSpecificationFinisher(joinTable, joinColumn, onJoinCondition, joinType, subCriteria); } } public class JoinSpecificationFinisher - extends AbstractWhereStarter - implements Buildable, PagingDSL { - private final JoinSpecification.Builder joinSpecificationBuilder; + extends AbstractBooleanExpressionDSL + implements AbstractWhereStarter, Buildable, + PagingDSL { + + private final TableExpression table; + private final JoinType joinType; public JoinSpecificationFinisher(TableExpression table, BindableColumn joinColumn, - JoinCondition joinCondition, JoinType joinType) { - JoinCriterion joinCriterion = new JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) - .build(); + VisitableCondition joinCondition, JoinType joinType) { + this.table = table; + this.joinType = joinType; + addJoinSpecificationSupplier(this::buildJoinSpecification); - joinSpecificationBuilder = JoinSpecification.withJoinTable(table) - .withJoinType(joinType) - .withJoinCriterion(joinCriterion); + ColumnAndConditionCriterion criterion = ColumnAndConditionCriterion.withColumn(joinColumn) + .withCondition(joinCondition) + .build(); - addJoinSpecificationBuilder(joinSpecificationBuilder); + setInitialCriterion(criterion); } public JoinSpecificationFinisher(TableExpression table, BindableColumn joinColumn, - JoinCondition joinCondition, JoinType joinType, JoinCriterion... andJoinCriteria) { - JoinCriterion onJoinCriterion = new JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) + VisitableCondition joinCondition, JoinType joinType, AndOrCriteriaGroup... subCriteria) { + this.table = table; + this.joinType = joinType; + addJoinSpecificationSupplier(this::buildJoinSpecification); + + ColumnAndConditionCriterion criterion = ColumnAndConditionCriterion.withColumn(joinColumn) + .withCondition(joinCondition) + .withSubCriteria(Arrays.asList(subCriteria)) .build(); - joinSpecificationBuilder = JoinSpecification.withJoinTable(table) - .withJoinType(joinType) - .withJoinCriterion(onJoinCriterion) - .withJoinCriteria(Arrays.asList(andJoinCriteria)); + setInitialCriterion(criterion); + } - addJoinSpecificationBuilder(joinSpecificationBuilder); + private JoinSpecification buildJoinSpecification() { + return JoinSpecification.withJoinTable(table) + .withJoinType(joinType) + .withInitialCriterion(getInitialCriterion()) + .withSubCriteria(subCriteria) + .build(); } @NotNull @@ -408,16 +417,6 @@ public QueryExpressionWhereBuilder where() { return QueryExpressionDSL.this.where(); } - public JoinSpecificationFinisher and(BindableColumn joinColumn, JoinCondition joinCondition) { - JoinCriterion joinCriterion = new JoinCriterion.Builder() - .withConnector("and") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) - .build(); - joinSpecificationBuilder.withJoinCriterion(joinCriterion); - return this; - } - public JoinSpecificationStarter join(SqlTable joinTable) { return QueryExpressionDSL.this.join(joinTable); } @@ -504,6 +503,11 @@ public PagingDSL.OffsetFirstFinisher offsetWhenPresent(Long offset) { public PagingDSL.FetchFirstFinisher fetchFirstWhenPresent(Long fetchFirstRows) { return QueryExpressionDSL.this.fetchFirstWhenPresent(fetchFirstRows); } + + @Override + protected JoinSpecificationFinisher getThis() { + return this; + } } public class GroupByFinisher extends AbstractHavingStarter diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java index 4b5ef2077..525e3282d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java @@ -119,7 +119,7 @@ public OffsetFirstFinisher offsetWhenPresent(Long offset) { public FetchFirstFinisher fetchFirstWhenPresent(Long fetchFirstRows) { this.fetchFirstRows = fetchFirstRows; - return () -> SelectDSL.this; + return () -> this; } @Override diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/ColumnBasedJoinCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/join/ColumnBasedJoinCondition.java deleted file mode 100644 index 712010ca6..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/ColumnBasedJoinCondition.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2016-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.join; - -import java.util.Objects; - -import org.mybatis.dynamic.sql.BasicColumn; - -public abstract class ColumnBasedJoinCondition implements JoinCondition { - private final BasicColumn rightColumn; - - protected ColumnBasedJoinCondition(BasicColumn rightColumn) { - this.rightColumn = Objects.requireNonNull(rightColumn); - } - - public BasicColumn rightColumn() { - return rightColumn; - } - - @Override - public R accept(JoinConditionVisitor visitor) { - return visitor.visit(this); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java b/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java deleted file mode 100644 index 6f12eb052..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2016-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.join; - -import org.mybatis.dynamic.sql.BasicColumn; - -public class EqualTo extends ColumnBasedJoinCondition { - - public EqualTo(BasicColumn rightColumn) { - super(rightColumn); - } - - @Override - public String operator() { - return "="; //$NON-NLS-1$ - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java b/src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java deleted file mode 100644 index 2d038aa21..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2016-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.join; - -public class EqualToValue extends TypedJoinCondition { - public EqualToValue(T value) { - super(value); - } - - @Override - public String operator() { - return "="; //$NON-NLS-1$ - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java deleted file mode 100644 index 183bd9a02..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2016-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.join; - -public interface JoinCondition { - String operator(); - - R accept(JoinConditionVisitor visitor); -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java deleted file mode 100644 index 582b332f1..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2016-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.join; - -public interface JoinConditionVisitor { - R visit(TypedJoinCondition condition); - - R visit(ColumnBasedJoinCondition condition); -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java deleted file mode 100644 index 81925f0e4..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2016-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.join; - -import java.util.Objects; - -import org.mybatis.dynamic.sql.BindableColumn; - -public class JoinCriterion { - - private final String connector; - private final BindableColumn leftColumn; - private final JoinCondition joinCondition; - - private JoinCriterion(Builder builder) { - connector = Objects.requireNonNull(builder.connector); - leftColumn = Objects.requireNonNull(builder.joinColumn); - joinCondition = Objects.requireNonNull(builder.joinCondition); - } - - public String connector() { - return connector; - } - - public BindableColumn leftColumn() { - return leftColumn; - } - - public JoinCondition joinCondition() { - return joinCondition; - } - - public static class Builder { - private String connector; - private BindableColumn joinColumn; - private JoinCondition joinCondition; - - public Builder withConnector(String connector) { - this.connector = connector; - return this; - } - - public Builder withJoinColumn(BindableColumn joinColumn) { - this.joinColumn = joinColumn; - return this; - } - - public Builder withJoinCondition(JoinCondition joinCondition) { - this.joinCondition = joinCondition; - return this; - } - - public JoinCriterion build() { - return new JoinCriterion<>(this); - } - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java index ab95dcf5e..9db5243d2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java @@ -15,36 +15,29 @@ */ package org.mybatis.dynamic.sql.select.join; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; -import java.util.stream.Stream; import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel; import org.mybatis.dynamic.sql.util.Validator; -public class JoinSpecification { +public class JoinSpecification extends AbstractBooleanExpressionModel { private final TableExpression table; - private final List> joinCriteria; private final JoinType joinType; private JoinSpecification(Builder builder) { + super(builder); table = Objects.requireNonNull(builder.table); - joinCriteria = Objects.requireNonNull(builder.joinCriteria); joinType = Objects.requireNonNull(builder.joinType); - Validator.assertNotEmpty(joinCriteria, "ERROR.16"); //$NON-NLS-1$ + Validator.assertFalse(initialCriterion().isEmpty() && subCriteria().isEmpty(), + "ERROR.16"); //$NON-NLS-1$ } public TableExpression table() { return table; } - @SuppressWarnings("java:S1452") - public Stream> joinCriteria() { - return joinCriteria.stream(); - } - public JoinType joinType() { return joinType; } @@ -53,9 +46,8 @@ public static Builder withJoinTable(TableExpression table) { return new Builder().withJoinTable(table); } - public static class Builder { + public static class Builder extends AbstractBuilder { private TableExpression table; - private final List> joinCriteria = new ArrayList<>(); private JoinType joinType; public Builder withJoinTable(TableExpression table) { @@ -63,16 +55,6 @@ public Builder withJoinTable(TableExpression table) { return this; } - public Builder withJoinCriterion(JoinCriterion joinCriterion) { - this.joinCriteria.add(joinCriterion); - return this; - } - - public Builder withJoinCriteria(List> joinCriteria) { - this.joinCriteria.addAll(joinCriteria); - return this; - } - public Builder withJoinType(JoinType joinType) { this.joinType = joinType; return this; @@ -81,5 +63,10 @@ public Builder withJoinType(JoinType joinType) { public JoinSpecification build() { return new JoinSpecification(this); } + + @Override + protected Builder getThis() { + return this; + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java deleted file mode 100644 index 12b310d1f..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2016-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.join; - -import java.util.Objects; - -public abstract class TypedJoinCondition implements JoinCondition { - private final T value; - - protected TypedJoinCondition(T value) { - this.value = Objects.requireNonNull(value); - } - - public T value() { - return value; - } - - @Override - public R accept(JoinConditionVisitor visitor) { - return visitor.visit(this); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java deleted file mode 100644 index 2cba1a951..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2016-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select.render; - -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - -import java.util.Objects; - -import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.render.RenderedParameterInfo; -import org.mybatis.dynamic.sql.render.RenderingContext; -import org.mybatis.dynamic.sql.select.join.ColumnBasedJoinCondition; -import org.mybatis.dynamic.sql.select.join.JoinConditionVisitor; -import org.mybatis.dynamic.sql.select.join.TypedJoinCondition; -import org.mybatis.dynamic.sql.util.FragmentAndParameters; - -public class JoinConditionRenderer implements JoinConditionVisitor { - private final BindableColumn leftColumn; - private final RenderingContext renderingContext; - - private JoinConditionRenderer(Builder builder) { - leftColumn = Objects.requireNonNull(builder.leftColumn); - renderingContext = Objects.requireNonNull(builder.renderingContext); - } - - @Override - public FragmentAndParameters visit(TypedJoinCondition condition) { - RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(leftColumn); - - return FragmentAndParameters - .withFragment(condition.operator() + spaceBefore(parameterInfo.renderedPlaceHolder())) - .withParameter(parameterInfo.parameterMapKey(), condition.value()) - .build(); - } - - @Override - public FragmentAndParameters visit(ColumnBasedJoinCondition condition) { - return condition.rightColumn().render(renderingContext) - .mapFragment(s -> condition.operator() + spaceBefore(s)); - } - - public static class Builder { - private BindableColumn leftColumn; - private RenderingContext renderingContext; - - public Builder withLeftColumn(BindableColumn leftColumn) { - this.leftColumn = leftColumn; - return this; - } - - public Builder withRenderingContext(RenderingContext renderingContext) { - this.renderingContext = renderingContext; - return this; - } - - public JoinConditionRenderer build() { - return new JoinConditionRenderer<>(this); - } - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java index 667b4e299..6a437f010 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java @@ -15,17 +15,16 @@ */ package org.mybatis.dynamic.sql.select.render; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.Objects; import java.util.stream.Collectors; +import org.mybatis.dynamic.sql.exception.InvalidSqlException; import org.mybatis.dynamic.sql.render.RenderingContext; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.select.join.JoinModel; import org.mybatis.dynamic.sql.select.join.JoinSpecification; import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.util.Messages; public class JoinRenderer { private final JoinModel joinModel; @@ -46,43 +45,17 @@ public FragmentAndParameters render() { } private FragmentAndParameters renderJoinSpecification(JoinSpecification joinSpecification) { - FragmentAndParameters renderedTable = joinSpecification.table().accept(tableExpressionRenderer); - FragmentAndParameters renderedJoin = renderConditions(joinSpecification); - - String fragment = joinSpecification.joinType().type() - + spaceBefore(renderedTable.fragment()) - + spaceBefore(renderedJoin.fragment()); - - return FragmentAndParameters.withFragment(fragment) - .withParameters(renderedTable.parameters()) - .withParameters(renderedJoin.parameters()) - .build(); - } - - private FragmentAndParameters renderConditions(JoinSpecification joinSpecification) { - return joinSpecification.joinCriteria() - .map(this::renderCriterion) - .collect(FragmentCollector.collect()) - .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ - } - - private FragmentAndParameters renderCriterion(JoinCriterion joinCriterion) { - FragmentAndParameters renderedColumn = joinCriterion.leftColumn().render(renderingContext); - - String prefix = joinCriterion.connector() - + spaceBefore(renderedColumn.fragment()); - - JoinConditionRenderer joinConditionRenderer = new JoinConditionRenderer.Builder() + FragmentCollector fc = new FragmentCollector(); + fc.add(FragmentAndParameters.fromFragment(joinSpecification.joinType().type())); + fc.add(joinSpecification.table().accept(tableExpressionRenderer)); + fc.add(JoinSpecificationRenderer + .withJoinSpecification(joinSpecification) .withRenderingContext(renderingContext) - .withLeftColumn(joinCriterion.leftColumn()) - .build(); - - FragmentAndParameters suffix = joinCriterion.joinCondition().accept(joinConditionRenderer); + .build() + .render() + .orElseThrow(() -> new InvalidSqlException(Messages.getString("ERROR.46")))); //$NON-NLS-1$ - return FragmentAndParameters.withFragment(prefix + spaceBefore(suffix.fragment())) - .withParameters(suffix.parameters()) - .withParameters(renderedColumn.parameters()) - .build(); + return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } public static Builder withJoinModel(JoinModel joinModel) { diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java new file mode 100644 index 000000000..300e416d8 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.select.render; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionRenderer; +import org.mybatis.dynamic.sql.select.join.JoinSpecification; + +public class JoinSpecificationRenderer extends AbstractBooleanExpressionRenderer { + private JoinSpecificationRenderer(Builder builder) { + super("on", builder); //$NON-NLS-1$ + } + + public static JoinSpecificationRenderer.Builder withJoinSpecification(JoinSpecification joinSpecification) { + return new Builder(joinSpecification); + } + + public static class Builder extends AbstractBuilder { + public Builder(JoinSpecification joinSpecification) { + super(joinSpecification); + } + + public JoinSpecificationRenderer build() { + return new JoinSpecificationRenderer(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java index 15f021b16..c104e18e6 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java @@ -47,8 +47,8 @@ import org.mybatis.dynamic.sql.where.AbstractWhereStarter; import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; -public class UpdateDSL extends AbstractWhereStarter.UpdateWhereBuilder, UpdateDSL> - implements Buildable { +public class UpdateDSL implements AbstractWhereStarter.UpdateWhereBuilder, UpdateDSL>, + Buildable { private final Function adapterFunction; private final List columnMappings = new ArrayList<>(); diff --git a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java index 03da51bd8..e94f0a8a1 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java @@ -35,14 +35,14 @@ * * @param the implementation of the Where DSL customized for a particular SQL statement. */ -public abstract class AbstractWhereStarter, D extends AbstractWhereStarter> - implements ConfigurableStatement { +public interface AbstractWhereStarter, D extends AbstractWhereStarter> + extends ConfigurableStatement { - public F where(BindableColumn column, VisitableCondition condition, AndOrCriteriaGroup... subCriteria) { + default F where(BindableColumn column, VisitableCondition condition, AndOrCriteriaGroup... subCriteria) { return where(column, condition, Arrays.asList(subCriteria)); } - public F where(BindableColumn column, VisitableCondition condition, + default F where(BindableColumn column, VisitableCondition condition, List subCriteria) { SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) .withCondition(condition) @@ -52,11 +52,11 @@ public F where(BindableColumn column, VisitableCondition condition, return initialize(sqlCriterion); } - public F where(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + default F where(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { return where(existsPredicate, Arrays.asList(subCriteria)); } - public F where(ExistsPredicate existsPredicate, List subCriteria) { + default F where(ExistsPredicate existsPredicate, List subCriteria) { ExistsCriterion sqlCriterion = new ExistsCriterion.Builder() .withExistsPredicate(existsPredicate) .withSubCriteria(subCriteria) @@ -65,11 +65,11 @@ public F where(ExistsPredicate existsPredicate, List subCrit return initialize(sqlCriterion); } - public F where(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + default F where(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { return where(initialCriterion, Arrays.asList(subCriteria)); } - public F where(SqlCriterion initialCriterion, List subCriteria) { + default F where(SqlCriterion initialCriterion, List subCriteria) { SqlCriterion sqlCriterion = new CriteriaGroup.Builder() .withInitialCriterion(initialCriterion) .withSubCriteria(subCriteria) @@ -78,7 +78,7 @@ public F where(SqlCriterion initialCriterion, List subCriter return initialize(sqlCriterion); } - public F where(List subCriteria) { + default F where(List subCriteria) { SqlCriterion sqlCriterion = new CriteriaGroup.Builder() .withSubCriteria(subCriteria) .build(); @@ -86,9 +86,9 @@ public F where(List subCriteria) { return initialize(sqlCriterion); } - public abstract F where(); + F where(); - public F applyWhere(WhereApplier whereApplier) { + default F applyWhere(WhereApplier whereApplier) { F finisher = where(); whereApplier.accept(finisher); return finisher; diff --git a/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java b/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java index 0eb23f638..a4adc2967 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java @@ -26,7 +26,7 @@ * *

This can also be used to create reusable where clauses for different statements. */ -public class WhereDSL extends AbstractWhereStarter { +public class WhereDSL implements AbstractWhereStarter { private final StatementConfiguration statementConfiguration = new StatementConfiguration(); private final StandaloneWhereFinisher whereBuilder = new StandaloneWhereFinisher(); diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt index 3ba5c2acb..078ea2a34 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt @@ -17,39 +17,29 @@ package org.mybatis.dynamic.sql.util.kotlin import org.mybatis.dynamic.sql.BindableColumn import org.mybatis.dynamic.sql.SqlBuilder -import org.mybatis.dynamic.sql.select.join.JoinCondition -import org.mybatis.dynamic.sql.select.join.JoinCriterion +import org.mybatis.dynamic.sql.VisitableCondition typealias JoinReceiver = JoinCollector.() -> Unit @MyBatisDslMarker class JoinCollector { - private var onJoinCriterion: JoinCriterion<*>? = null - internal val andJoinCriteria = mutableListOf>() + private val criteriaCollector = GroupingCriteriaCollector() - internal fun onJoinCriterion() : JoinCriterion<*> = invalidIfNull(onJoinCriterion, "ERROR.22") //$NON-NLS-1$ + internal fun initialCriterion() = invalidIfNull(criteriaCollector.initialCriterion, "ERROR.22") //$NON-NLS-1$ + internal fun subCriteria() = criteriaCollector.subCriteria fun on(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { - onJoinCriterion = JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(leftColumn) - .withJoinCondition(it) - .build() + assertNull(criteriaCollector.initialCriterion, "ERROR.45") //$NON-NLS-1$ + criteriaCollector.apply { leftColumn.invoke(it) } } fun and(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { - andJoinCriteria.add( - JoinCriterion.Builder() - .withConnector("and") //$NON-NLS-1$ - .withJoinColumn(leftColumn) - .withJoinCondition(it) - .build() - ) + criteriaCollector.and { leftColumn.invoke(it) } } } -class RightColumnCollector(private val joinConditionConsumer: (JoinCondition) -> Unit) { - infix fun equalTo(rightColumn: BindableColumn) = joinConditionConsumer.invoke(SqlBuilder.equalTo(rightColumn)) +class RightColumnCollector(private val joinConditionConsumer: (VisitableCondition) -> Unit) { + infix fun equalTo(rightColumn: BindableColumn) = joinConditionConsumer.invoke(SqlBuilder.isEqualTo(rightColumn)) - infix fun equalTo(value: T) = joinConditionConsumer.invoke(SqlBuilder.equalTo(value)) + infix fun equalTo(value: T) = joinConditionConsumer.invoke(SqlBuilder.isEqualTo(value)) } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt index 4207905c6..059792b2c 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt @@ -64,76 +64,156 @@ abstract class KotlinBaseBuilder> { @Suppress("TooManyFunctions") abstract class KotlinBaseJoiningBuilder> : KotlinBaseBuilder() { + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun join(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - join(table, jc.onJoinCriterion(), jc.andJoinCriteria) + join(table, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun join(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - join(table, alias, jc.onJoinCriterion(), jc.andJoinCriteria) + join(table, alias, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun join( subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - join(sq, sq.correlationName, jc.onJoinCriterion(), jc.andJoinCriteria) + join(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } + fun join(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().join(table, it.initialCriterion, it.subCriteria) + } + + fun join(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().join(table, alias, it.initialCriterion, it.subCriteria) + } + + fun join( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().join(sq, sq.correlationName, it.initialCriterion, it.subCriteria) + } + + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun fullJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - fullJoin(table, jc.onJoinCriterion(), jc.andJoinCriteria) + fullJoin(table, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun fullJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - fullJoin(table, alias, jc.onJoinCriterion(), jc.andJoinCriteria) + fullJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun fullJoin( subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - fullJoin(sq, sq.correlationName, jc.onJoinCriterion(), jc.andJoinCriteria) + fullJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) + } + + fun fullJoin(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().fullJoin(table, it.initialCriterion, it.subCriteria) + } + + fun fullJoin(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().fullJoin(table, alias, it.initialCriterion, it.subCriteria) + } + + fun fullJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().fullJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun leftJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - leftJoin(table, jc.onJoinCriterion(), jc.andJoinCriteria) + leftJoin(table, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun leftJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - leftJoin(table, alias, jc.onJoinCriterion(), jc.andJoinCriteria) + leftJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun leftJoin( subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - leftJoin(sq, sq.correlationName, jc.onJoinCriterion(), jc.andJoinCriteria) + leftJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) + } + + fun leftJoin(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().leftJoin(table, it.initialCriterion, it.subCriteria) } + fun leftJoin(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().leftJoin(table, alias, it.initialCriterion, it.subCriteria) + } + + fun leftJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().leftJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria) + } + + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun rightJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - rightJoin(table, jc.onJoinCriterion(), jc.andJoinCriteria) + rightJoin(table, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun rightJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - rightJoin(table, alias, jc.onJoinCriterion(), jc.andJoinCriteria) + rightJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun rightJoin( subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - rightJoin(sq, sq.correlationName, jc.onJoinCriterion(), jc.andJoinCriteria) + rightJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) + } + + fun rightJoin(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().rightJoin(table, it.initialCriterion, it.subCriteria) + } + + fun rightJoin(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().rightJoin(table, alias, it.initialCriterion, it.subCriteria) + } + + fun rightJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().rightJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria) } private fun applyToDsl(joinCriteria: JoinReceiver, applyJoin: D.(JoinCollector) -> Unit) { @@ -148,3 +228,11 @@ abstract class KotlinBaseJoiningBuilder> : getDsl().applyJoin(KotlinQualifiedSubQueryBuilder().apply(subQuery), JoinCollector().apply(joinCriteria)) } } + +class JoinCriteriaGatherer(private val consumer: (GroupingCriteriaCollector) -> Unit) { + infix fun on (joinCriteria: GroupingCriteriaReceiver): Unit = + with(GroupingCriteriaCollector().apply(joinCriteria)) { + assertTrue(initialCriterion != null || subCriteria.isNotEmpty(), "ERROR.22") //$NON-NLS-1$ + consumer.invoke(this) + } +} diff --git a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties index ef092e651..05ecfcfe8 100644 --- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -61,4 +61,6 @@ ERROR.41=You cannot call "then" in a Kotlin case expression more than once ERROR.42=You cannot call `else` in a Kotlin case expression more than once ERROR.43=A Kotlin cast expression must have one, and only one, `as` element ERROR.44={0} conditions must contain at least one value +ERROR.45=You cannot call "on" in a Kotlin join expression more than once +ERROR.46=At least one join criterion must render INTERNAL.ERROR=Internal Error {0} diff --git a/src/site/markdown/docs/kotlinOverview.md b/src/site/markdown/docs/kotlinOverview.md index 5b646a7b0..78dba6a78 100644 --- a/src/site/markdown/docs/kotlinOverview.md +++ b/src/site/markdown/docs/kotlinOverview.md @@ -417,9 +417,9 @@ val selectStatement = select(orderMaster.orderId, orderMaster.orderDate, orderDe orderDetail.description, orderDetail.quantity ) { from(orderMaster, "om") - join(orderDetail, "od") { - on(orderMaster.orderId) equalTo orderDetail.orderId - and(orderMaster.orderId) equalTo orderDetail.orderId + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo orderDetail.orderId } } where { orderMaster.orderId isEqualTo 1 } or { @@ -433,8 +433,7 @@ val selectStatement = select(orderMaster.orderId, orderMaster.orderDate, orderDe In a select statement you must specify a table in a `from` clause. Everything else is optional. -Multiple join clauses can be specified if you need to join additional tables. In a join clause, you must -specify an `on` condition, and you may specify additional `and` conditions as necessary. Full, left, right, inner, +Multiple join clauses can be specified if you need to join additional tables. Full, left, right, inner, and outer joins are supported. Where clauses can be of arbitrary complexity and support all SQL operators including exists operators, subqueries, etc. diff --git a/src/site/markdown/docs/migratingV1toV2.md b/src/site/markdown/docs/migratingV1toV2.md new file mode 100644 index 000000000..12b6adc37 --- /dev/null +++ b/src/site/markdown/docs/migratingV1toV2.md @@ -0,0 +1,49 @@ +# V1 to V2 Migration Guide + +Version 2 of MyBatis Dynamic SQL introduced many new features. On this page we will provide examples for the more +significant changes - changes that are more substantial than following deprecation messages. + +## Kotlin Join Syntax + +The Java DSL for joins was changed to allow much more flexible joins. Of course, not all capabilities are supported in +all databases, but you should now be able to code most joins specification that are supported by your database. +The changes in the Java DSL are mostly internal and should not impact most users. The `equalTo` methods has been +deprecated in favor of `isEqualTo`, but all other changes should be hidden. + +Like the Java DSL, the V2 Kotlin DSL offers a fully flexible join specification and allows for much more flexible join +specifications. The changes in the Kotlin DSL allow a more natural expressions of a join specification. The main +difference is that the "on" keyword should be moved outside the join specification lambda (it is now an infix function). +Inside the lambda, the conditions should be rewritten to match the syntax of a where clause. + +V1 (Deprecated) Join Specification Example: +```kotlin +val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity +) { + from(orderMaster, "om") + join(orderDetail, "od") { + on(orderMaster.orderId) equalTo orderDetail.orderId + and(orderMaster.orderId) equalTo constant("1") + } +} +``` + +V2 Join Specification Example: +```kotlin +val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity +) { + from(orderMaster, "om") + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo constant("1") } + } +} +``` + +Notice that the "on" keyword has been moved outside the lambda, and the conditions are coded with the same syntax used +by WHERE, HAVING, and CASE expressions. + +The prior syntax is deprecated and will be removed in a future release. diff --git a/src/site/markdown/docs/select.md b/src/site/markdown/docs/select.md index 923c30c42..f73ef17dd 100644 --- a/src/site/markdown/docs/select.md +++ b/src/site/markdown/docs/select.md @@ -10,7 +10,7 @@ In general, the following are supported: 2. Tables can be aliased per select statement 3. Columns can be aliased per select statement 4. Some support for aggregates (avg, min, max, sum) -5. Equijoins of type INNER, LEFT OUTER, RIGHT OUTER, FULL OUTER +5. Joins of type INNER, LEFT OUTER, RIGHT OUTER, FULL OUTER 6. Subqueries in where clauses. For example, `where foo in (select foo from foos where id < 36)` 7. Select from another select. For example `select count(*) from (select foo from foos where id < 36)` 8. Multi-Selects. For example `(select * from foo order by id limit 3) union (select * from foo order by id desc limit 3)` @@ -21,47 +21,50 @@ At this time, the library does not support the following: 2. INTERSECT, EXCEPT, etc. The user guide page for WHERE Clauses shows examples of many types of SELECT statements with different complexities of -the WHERE clause including support for sub-queries. We will just show a single example here, including an ORDER BY clause: +the WHERE clause including support for sub-queries. We will just show a single example here, including an ORDER BY +clause: ```java - SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .where(id, isIn(1, 5, 7)) - .and(bodyWeight, isBetween(1.0).and(3.0)) - .orderBy(id.descending(), bodyWeight) - .build() - .render(RenderingStrategies.MYBATIS3); - - List animals = mapper.selectMany(selectStatement); +SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .where(id, isIn(1, 5, 7)) + .and(bodyWeight, isBetween(1.0).and(3.0)) + .orderBy(id.descending(), bodyWeight) + .build() + .render(RenderingStrategies.MYBATIS3); + +List animals = mapper.selectMany(selectStatement); ``` The WHERE and ORDER BY clauses are optional. ## Joins -The library supports the generation of equijoin statements - joins defined by column matching. For example: +The library supports the generation of join statements. For example: ```java - SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) - .from(orderMaster, "om") - .join(orderDetail, "od").on(orderMaster.orderId, equalTo(orderDetail.orderId)) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -Notice that you can give an alias to a table if desired. If you don't specify an alias, the full table name will be used in the generated SQL. +Notice that you can give an alias to a table if desired. If you don't specify an alias, the full table name will be +used in the generated SQL. Multiple tables can be joined in a single statement. For example: ```java - SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) - .from(orderMaster, "om") - .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId)) - .join(itemMaster, "im").on(orderLine.itemId, equalTo(itemMaster.itemId)) - .where(orderMaster.orderId, isEqualTo(2)) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2)) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -Join queries will likely require you to define a MyBatis result mapping in XML. This is the only instance where XML is required. This is due to the limitations of the MyBatis annotations when mapping collections. +Join queries will likely require you to define a MyBatis result mapping in XML. This is the only instance where XML is +required. This is due to the limitations of the MyBatis annotations when mapping collections. The library supports four join types: @@ -74,14 +77,14 @@ The library supports four join types: The library supports the generation of UNION and UNION ALL queries. For example: ```java - SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .union() - .selectDistinct(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .orderBy(id) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .union() + .selectDistinct(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); ``` Any number of SELECT statements can be added to a UNION query. Only one ORDER BY phrase is allowed. @@ -96,16 +99,16 @@ Multi-select queries are a special case of union select statements. The differen paging clauses can be applied to the merged queries. For example: ```java - SelectStatementProvider selectStatement = multiSelect( - select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .orderBy(id) - .limit(2) +SelectStatementProvider selectStatement = multiSelect( + select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .orderBy(id) + .limit(2) ).union( - selectDistinct(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .orderBy(id.descending()) - .limit(3) + selectDistinct(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .orderBy(id.descending()) + .limit(3) ) .build() .render(RenderingStrategies.MYBATIS3); @@ -114,7 +117,8 @@ paging clauses can be applied to the merged queries. For example: ## MyBatis Mapper for Select Statements The SelectStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you -are using an annotated mapper, the select method should look like this (note that we recommend coding a "selectMany" and a "selectOne" method with a shared result mapping): +are using an annotated mapper, the select method should look like this (note that we recommend coding a "selectMany" +and a "selectOne" method with a shared result mapping): ```java import org.apache.ibatis.annotations.Result; @@ -143,7 +147,9 @@ import org.mybatis.dynamic.sql.util.SqlProviderAdapter; ## XML Mapper for Join Statements -If you are coding a join, it is likely you will need to code an XML mapper to define the result map. This is due to a MyBatis limitation - the annotations cannot define a collection mapping. If you have to do this, the Java code looks like this: +If you are coding a join, it is likely you will need to code an XML mapper to define the result map. This is due to a +MyBatis limitation - the annotations cannot define a collection mapping. If you have to do this, the Java code looks +like this: ```java @SelectProvider(type=SqlProviderAdapter.class, method="select") @@ -171,7 +177,8 @@ And the corresponding XML looks like this: Notice that the resultMap is the only element in the XML mapper. This is our recommended practice. ## XML Mapper for Select Statements -We do not recommend using an XML mapper for select statements, but if you want to do so the SelectStatementProvider object can be used as a parameter to a MyBatis mapper method directly. +We do not recommend using an XML mapper for select statements, but if you want to do so the SelectStatementProvider +object can be used as a parameter to a MyBatis mapper method directly. If you are using an XML mapper, the select method should look like this in the Java interface: @@ -205,30 +212,33 @@ Order by phrases can be difficult to calculate when there are aliased columns, a This library has taken a relatively simple approach: 1. When specifying an SqlColumn in an ORDER BY phrase the library will either write the column alias or the column -name into the ORDER BY phrase. For the ORDER BY phrase, the table alias (if there is one) will be ignored. Use this pattern -when the ORDER BY column is a member of the select list. For example `orderBy(foo)`. If the column has an alias, then -it is easist to use the "arbitrary string" method with the column alias as shown below. -1. It is also possible to explicitly specify a table alias for a column in an ORDER BY phrase. Use this pattern when -there is a join, and the ORDER BY column is in two or more tables, and the ORDER BY column is not in the select -list. For example `orderBy(sortColumn("t1", foo))`. -1. If none of the above use cases meet your needs, then you can specify an arbitrary String to write into the rendered ORDER BY -phrase (see below for an example). + name into the ORDER BY phrase. For the ORDER BY phrase, the table alias (if there is one) will be ignored. Use this + pattern when the ORDER BY column is a member of the select list. For example `orderBy(foo)`. If the column has an + alias, then it is easiest to use the "arbitrary string" method with the column alias as shown below. +2. It is also possible to explicitly specify a table alias for a column in an ORDER BY phrase. Use this pattern when + there is a join, and the ORDER BY column is in two or more tables, and the ORDER BY column is not in the select + list. For example `orderBy(sortColumn("t1", foo))`. +3. If none of the above use cases meet your needs, then you can specify an arbitrary String to write into the rendered + ORDER BY phrase (see below for an example). In our testing, this caused an issue in only one case. When there is an outer join and the select list contains both the left and right join column. In that case, the workaround is to supply a column alias for both columns. -When using a column function (lower, upper, etc.), then it is customary to give the calculated column an alias so you will have a predictable result set. In cases like this there will not be a column to use for an alias. The library supports arbitrary values in an ORDER BY expression like this: +When using a column function (lower, upper, etc.), then it is customary to give the calculated column an alias so you +will have a predictable result set. In cases like this there will not be a column to use for an alias. The library +supports arbitrary values in an ORDER BY expression like this: ```java - SelectStatementProvider selectStatement = select(substring(gender, 1, 1).as("ShortGender"), avg(age).as("AverageAge")) - .from(person, "a") - .groupBy(substring(gender, 1, 1)) - .orderBy(sortColumn("ShortGender").descending()) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(substring(gender, 1, 1).as("ShortGender"), avg(age).as("AverageAge")) + .from(person, "a") + .groupBy(substring(gender, 1, 1)) + .orderBy(sortColumn("ShortGender").descending()) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -In this example the `substring` function is used in both the select list and the GROUP BY expression. In the ORDER BY expression, we use the `sortColumn` function to duplicate the alias given to the column in the select list. +In this example the `substring` function is used in both the select list and the GROUP BY expression. In the ORDER BY +expression, we use the `sortColumn` function to duplicate the alias given to the column in the select list. ## Limit and Offset Support Since version 1.1.1 the select statement supports limit and offset for paging (or slicing) queries. You can specify: @@ -237,18 +247,22 @@ Since version 1.1.1 the select statement supports limit and offset for paging (o - Offset only - Both limit and offset -It is important to note that the select renderer writes limit and offset clauses into the generated select statement as is. The library does not attempt to normalize those values for databases that don't support limit and offset directly. Therefore, it is very important for users to understand whether or not the target database supports limit and offset. If the target database does not support limit and offset, then it is likely that using this support will create SQL that has runtime errors. +It is important to note that the select renderer writes limit and offset clauses into the generated select statement as +is. The library does not attempt to normalize those values for databases that don't support limit and offset directly. +Therefore, it is very important for users to understand whether the target database supports limit and offset. +If the target database does not support limit and offset, then it is likely that using this support will create SQL +that has runtime errors. An example follows: ```java - SelectStatementProvider selectStatement = select(animalData.allColumns()) - .from(animalData) - .orderBy(id) - .limit(3) - .offset(22) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(animalData.allColumns()) + .from(animalData) + .orderBy(id) + .limit(3) + .offset(22) + .build() + .render(RenderingStrategies.MYBATIS3); ``` ## Fetch First Support @@ -263,11 +277,11 @@ Fetch first is an SQL standard and is supported by most databases. An example follows: ```java - SelectStatementProvider selectStatement = select(animalData.allColumns()) - .from(animalData) - .orderBy(id) - .offset(22) - .fetchFirst(3).rowsOnly() - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(animalData.allColumns()) + .from(animalData) + .orderBy(id) + .offset(22) + .fetchFirst(3).rowsOnly() + .build() + .render(RenderingStrategies.MYBATIS3); ``` diff --git a/src/site/site.xml b/src/site/site.xml index e22bf264d..2a02c4a79 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -36,6 +36,7 @@

+ diff --git a/src/test/java/examples/joins/NewSyntaxJoinMapperTest.java b/src/test/java/examples/joins/NewSyntaxJoinMapperTest.java new file mode 100644 index 000000000..ae8f5d0f0 --- /dev/null +++ b/src/test/java/examples/joins/NewSyntaxJoinMapperTest.java @@ -0,0 +1,1270 @@ +/* + * Copyright 2016-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.joins; + +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.jdbc.ScriptRunner; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.exception.DuplicateTableAliasException; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.QueryExpressionDSL; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.Messages; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.List; +import java.util.Map; + +import static examples.joins.ItemMasterDynamicSQLSupport.itemMaster; +import static examples.joins.OrderDetailDynamicSQLSupport.orderDetail; +import static examples.joins.OrderLineDynamicSQLSupport.orderLine; +import static examples.joins.OrderMasterDynamicSQLSupport.orderDate; +import static examples.joins.OrderMasterDynamicSQLSupport.orderMaster; +import static examples.joins.UserDynamicSQLSupport.user; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mybatis.dynamic.sql.SqlBuilder.and; +import static org.mybatis.dynamic.sql.SqlBuilder.constant; +import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; +import static org.mybatis.dynamic.sql.SqlBuilder.on; +import static org.mybatis.dynamic.sql.SqlBuilder.select; +import static org.mybatis.dynamic.sql.SqlBuilder.sortColumn; +import static org.mybatis.dynamic.sql.SqlBuilder.where; + +class NewSyntaxJoinMapperTest { + + private static final String JDBC_URL = "jdbc:hsqldb:mem:aname"; + private static final String JDBC_DRIVER = "org.hsqldb.jdbcDriver"; + + private SqlSessionFactory sqlSessionFactory; + + @BeforeEach + void setup() throws Exception { + Class.forName(JDBC_DRIVER); + InputStream is = getClass().getResourceAsStream("/examples/joins/CreateJoinDB.sql"); + assert is != null; + try (Connection connection = DriverManager.getConnection(JDBC_URL, "sa", "")) { + ScriptRunner sr = new ScriptRunner(connection); + sr.setLogWriter(null); + sr.runScript(new InputStreamReader(is)); + } + + UnpooledDataSource ds = new UnpooledDataSource(JDBC_DRIVER, JDBC_URL, "sa", ""); + Environment environment = new Environment("test", new JdbcTransactionFactory(), ds); + Configuration config = new Configuration(environment); + config.addMapper(JoinMapper.class); + config.addMapper(CommonSelectMapper.class); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(config); + } + + @Test + void testSingleTableJoin1() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(2); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(1); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + + orderMaster = rows.get(1); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(1); + orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + } + } + + @Test + void testSingleTableJoin2() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(2); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(1); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + + orderMaster = rows.get(1); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(1); + orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + } + } + + @Test + void testCompoundJoin1() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin2() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true)) + .and(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin3() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin4() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .leftJoin(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om left join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin5() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .rightJoin(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om right join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin6() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .fullJoin(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om full join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testMultipleTableJoinWithWhereClause() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(1); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testMultipleTableJoinWithApplyWhere() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .applyWhere(where(orderMaster.orderId, isEqualTo(2)).toWhereApplier()) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(1); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testMultipleTableJoinWithComplexWhereClause() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2), and(orderLine.lineNumber, isEqualTo(2))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER} and ol.line_number = #{parameters.p2,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(1); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(1); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testMultipleTableJoinWithOrderBy() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderMaster.orderId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(2); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(1); + assertThat(orderMaster.getDetails()).hasSize(1); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + + orderMaster = rows.get(1); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(2); + orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testMultipleTableJoinNoAliasWithOrderBy() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster) + .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2)) + .orderBy(orderMaster.orderId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select OrderMaster.order_id, OrderMaster.order_date, OrderLine.line_number, ItemMaster.description, OrderLine.quantity" + + " from OrderMaster join OrderLine on OrderMaster.order_id = OrderLine.order_id join ItemMaster on OrderLine.item_id = ItemMaster.item_id" + + " where OrderMaster.order_id = #{parameters.p1,jdbcType=INTEGER}" + + " order by order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(1); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testRightJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderLine, "ol") + .rightJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderLine ol right join ItemMaster im on ol.item_id = im.item_id" + + " order by item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + + row = rows.get(4); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + } + } + + @Test + void testRightJoin2() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .rightJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testRightJoin3() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .rightJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testRightJoinNoAliases() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster) + .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .rightJoin(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select OrderLine.order_id, OrderLine.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster join OrderLine on OrderMaster.order_id = OrderLine.order_id" + + " right join ItemMaster on OrderLine.item_id = ItemMaster.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testLeftJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " order by item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + + row = rows.get(4); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + } + } + + @Test + void testLeftJoin2() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .leftJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testLeftJoin3() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .leftJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testLeftJoinNoAliases() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster) + .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .leftJoin(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select OrderLine.order_id, OrderLine.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster join OrderLine on OrderMaster.order_id = OrderLine.order_id" + + " left join ItemMaster on OrderLine.item_id = ItemMaster.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testFullJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, orderLine.itemId.as("ol_itemid"), itemMaster.itemId.as("im_itemid"), itemMaster.description) + .from(itemMaster, "im") + .fullJoin(orderLine, "ol").on(itemMaster.itemId, isEqualTo(orderLine.itemId)) + .orderBy(orderLine.orderId, sortColumn("im_itemid")) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, ol.item_id as ol_itemid, im.item_id as im_itemid, im.description" + + " from ItemMaster im full join OrderLine ol on im.item_id = ol.item_id" + + " order by order_id, im_itemid"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("IM_ITEMID", 55); + + row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("IM_ITEMID", 33); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).containsEntry("OL_ITEMID", 66); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("IM_ITEMID"); + } + } + + @Test + void testFullJoin2() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .fullJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testFullJoin3() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .fullJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testFullJoin4() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .fullJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, sortColumn("im", itemMaster.itemId)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, im.item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + } + } + + @Test + void testFullJoin5() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .fullJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, sortColumn("im", itemMaster.itemId).descending()) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, im.item_id DESC"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + } + } + + @Test + void testFullJoinNoAliases() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster) + .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .fullJoin(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select OrderLine.order_id, OrderLine.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster join OrderLine on OrderMaster.order_id = OrderLine.order_id" + + " full join ItemMaster on OrderLine.item_id = ItemMaster.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testSelf() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + // create second table instance for self-join + UserDynamicSQLSupport.User user2 = new UserDynamicSQLSupport.User(); + + // get Bamm Bamm's parent - should be Barney + SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId) + .from(user, "u1") + .join(user2, "u2").on(user.userId, isEqualTo(user2.parentId)) + .where(user2.userId, isEqualTo(4)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectUsers(selectStatement); + + assertThat(rows).hasSize(1); + User row = rows.get(0); + assertThat(row.getUserId()).isEqualTo(2); + assertThat(row.getUserName()).isEqualTo("Barney"); + assertThat(row.getParentId()).isNull(); + } + } + + @Test + void testSelfWithDuplicateAlias() { + QueryExpressionDSL dsl = select(user.userId, user.userName, user.parentId) + .from(user, "u1"); + + assertThatExceptionOfType(DuplicateTableAliasException.class).isThrownBy(() -> dsl.join(user, "u2")) + .withMessage(Messages.getString("ERROR.1", user.tableName(), "u2", "u1")); + } + + @Test + void testSelfWithNewAlias() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + // create second table instance for self-join + UserDynamicSQLSupport.User user2 = user.withAlias("u2"); + + // get Bamm Bamm's parent - should be Barney + SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId) + .from(user) + .join(user2).on(user.userId, isEqualTo(user2.parentId)) + .where(user2.userId, isEqualTo(4)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select User.user_id, User.user_name, User.parent_id" + + " from User join User u2 on User.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectUsers(selectStatement); + + assertThat(rows).hasSize(1); + User row = rows.get(0); + assertThat(row.getUserId()).isEqualTo(2); + assertThat(row.getUserName()).isEqualTo("Barney"); + assertThat(row.getParentId()).isNull(); + } + } + + @Test + void testSelfWithNewAliasAndOverride() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + // create second table instance for self-join + UserDynamicSQLSupport.User user2 = user.withAlias("other_user"); + + // get Bamm Bamm's parent - should be Barney + SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId) + .from(user, "u1") + .join(user2, "u2").on(user.userId, isEqualTo(user2.parentId)) + .where(user2.userId, isEqualTo(4)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectUsers(selectStatement); + + assertThat(rows).hasSize(1); + User row = rows.get(0); + assertThat(row.getUserId()).isEqualTo(2); + assertThat(row.getUserName()).isEqualTo("Barney"); + assertThat(row.getParentId()).isNull(); + } + } + + @Test + void testLimitAndOffsetAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .limit(2) + .offset(1) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " limit #{parameters.p1} offset #{parameters.p2}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + } + } + + @Test + void testLimitOnlyAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .limit(2) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " limit #{parameters.p1}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + } + } + + @Test + void testOffsetOnlyAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .offset(2) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " offset #{parameters.p1} rows"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(3); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testOffsetAndFetchFirstAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .offset(1) + .fetchFirst(2).rowsOnly() + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " offset #{parameters.p1} rows fetch first #{parameters.p2} rows only"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + } + } + + @Test + void testFetchFirstOnlyAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .fetchFirst(2).rowsOnly() + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " fetch first #{parameters.p1} rows only"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + } + } + + @Test + void testJoinWithParameterValue() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .join(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .and(orderLine.orderId, isEqualTo(1)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im join OrderLine ol on ol.item_id = im.item_id" + + " and ol.order_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + } + } + + @Test + void testJoinWithConstant() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .join(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .and(orderLine.orderId, isEqualTo(constant("1"))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im join OrderLine ol on ol.item_id = im.item_id" + + " and ol.order_id = 1"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + } + } +} diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt new file mode 100644 index 000000000..1b69ba5ec --- /dev/null +++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt @@ -0,0 +1,836 @@ +/* + * Copyright 2016-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.kotlin.mybatis3.joins + +import examples.kotlin.mybatis3.TestUtils +import examples.kotlin.mybatis3.joins.ItemMasterDynamicSQLSupport.itemMaster +import examples.kotlin.mybatis3.joins.OrderDetailDynamicSQLSupport.orderDetail +import examples.kotlin.mybatis3.joins.OrderLineDynamicSQLSupport.orderLine +import examples.kotlin.mybatis3.joins.OrderMasterDynamicSQLSupport.orderMaster +import examples.kotlin.mybatis3.joins.UserDynamicSQLSupport.user +import org.apache.ibatis.session.SqlSessionFactory +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.assertj.core.api.Assertions.entry +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.mybatis.dynamic.sql.util.Messages +import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException +import org.mybatis.dynamic.sql.util.kotlin.elements.constant +import org.mybatis.dynamic.sql.util.kotlin.elements.invoke +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select + +@Suppress("LargeClass") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class JoinMapperNewSyntaxTest { + private lateinit var sqlSessionFactory: SqlSessionFactory + + @BeforeAll + fun setup() { + sqlSessionFactory = TestUtils.buildSqlSessionFactory { + withInitializationScript("/examples/kotlin/mybatis3/joins/CreateJoinDB.sql") + withMapper(JoinMapper::class) + } + } + + @Test + fun testSingleTableJoin() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectMany(selectStatement) + + assertThat(rows).hasSize(2) + + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(details).hasSize(2) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + assertThat(details?.get(1)?.lineNumber).isEqualTo(2) + } + + with(rows[1]) { + assertThat(id).isEqualTo(2) + assertThat(details).hasSize(1) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + } + } + } + + @Test + fun testSingleTableJoinWithValue() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo 1 } + } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id" + + " and om.order_id = #{parameters.p1,jdbcType=INTEGER}" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectMany(selectStatement) + + assertThat(rows).hasSize(1) + + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(details).hasSize(2) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + assertThat(details?.get(1)?.lineNumber).isEqualTo(2) + } + } + } + + @Test + fun testSingleTableJoinWithConstant() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo constant("1") } + } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id" + + " and om.order_id = 1" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectMany(selectStatement) + + assertThat(rows).hasSize(1) + + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(details).hasSize(2) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + assertThat(details?.get(1)?.lineNumber).isEqualTo(2) + } + } + } + + @Test + fun testCompoundJoin1() { + // this is a nonsensical join, but it does test the "and" capability + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, orderDetail.lineNumber, + orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo orderDetail.orderId } + } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + } + + @Test + fun testCompoundJoin2() { + // this is a nonsensical join, but it does test the "and" capability + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, orderDetail.lineNumber, + orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo orderDetail.orderId } + } + where { orderMaster.orderId isEqualTo 1 } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + } + + @Test + fun testMultipleTableJoinWithWhereClause() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, orderLine.lineNumber, + itemMaster.description, orderLine.quantity + ) { + from(orderMaster, "om") + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId + } + join(itemMaster, "im") on { + orderLine.itemId isEqualTo itemMaster.itemId + } + where { orderMaster.orderId isEqualTo 2 } + } + + val expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol" + + " on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectMany(selectStatement) + + assertThat(rows).hasSize(1) + with(rows[0]) { + assertThat(id).isEqualTo(2) + assertThat(details).hasSize(2) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + assertThat(details?.get(1)?.lineNumber).isEqualTo(2) + } + } + } + + @Test + fun testFullJoinWithAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId + } + fullJoin(itemMaster, "im") on { + orderLine.itemId isEqualTo itemMaster.itemId + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + data class OrderDetail(val itemId: Int?, val orderId: Int?, val quantity: Int?, val description: String?) + + val rows = mapper.selectMany(selectStatement) { + OrderDetail( + it["ITEM_ID"] as Int?, + it["ORDER_ID"] as Int?, + it["QUANTITY"] as Int?, + it["DESCRIPTION"] as String? + ) + } + + assertThat(rows).hasSize(6) + + with(rows[0]) { + assertThat(itemId).isEqualTo(55) + assertThat(orderId).isNull() + assertThat(quantity).isNull() + assertThat(description).isEqualTo("Catcher Glove") + } + + with(rows[3]) { + assertThat(itemId).isNull() + assertThat(orderId).isEqualTo(2) + assertThat(quantity).isEqualTo(6) + assertThat(description).isNull() + } + + with(rows[5]) { + assertThat(itemId).isEqualTo(44) + assertThat(orderId).isEqualTo(2) + assertThat(quantity).isEqualTo(1) + assertThat(description).isEqualTo("Outfield Glove") + } + } + } + + @Test + @Suppress("LongMethod") + fun testFullJoinWithSubQuery() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + "ol"(orderLine.orderId), orderLine.quantity, "im"(itemMaster.itemId), + itemMaster.description + ) { + from { + select(orderMaster.allColumns()) { + from(orderMaster) + } + + "om" + } + join { + select(orderLine.allColumns()) { + from(orderLine) + } + + "ol" + } on { + "om"(orderMaster.orderId) isEqualTo "ol"(orderLine.orderId) + } + fullJoin { + select(itemMaster.allColumns()) { + from(itemMaster) + } + +"im" + } on { + "ol"(orderLine.itemId) isEqualTo "im"(itemMaster.itemId) + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, quantity, im.item_id, description" + + " from (select * from OrderMaster) om" + + " join (select * from OrderLine) ol on om.order_id = ol.order_id" + + " full join (select * from ItemMaster) im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + data class OrderDetail(val itemId: Int?, val orderId: Int?, val quantity: Int?, val description: String?) + + val rows = mapper.selectMany(selectStatement) { + OrderDetail( + it["ITEM_ID"] as Int?, + it["ORDER_ID"] as Int?, + it["QUANTITY"] as Int?, + it["DESCRIPTION"] as String? + ) + } + + assertThat(rows).hasSize(6) + + with(rows[0]) { + assertThat(itemId).isEqualTo(55) + assertThat(orderId).isNull() + assertThat(quantity).isNull() + assertThat(description).isEqualTo("Catcher Glove") + } + + with(rows[3]) { + assertThat(itemId).isNull() + assertThat(orderId).isEqualTo(2) + assertThat(quantity).isEqualTo(6) + assertThat(description).isNull() + } + + with(rows[5]) { + assertThat(itemId).isEqualTo(44) + assertThat(orderId).isEqualTo(2) + assertThat(quantity).isEqualTo(1) + assertThat(description).isEqualTo("Outfield Glove") + } + } + } + + @Test + fun testFullJoinWithoutAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId + } + fullJoin(itemMaster) on { + orderLine.itemId isEqualTo itemMaster.itemId + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster on ol.item_id = ItemMaster.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(6) + + assertThat(rows[0]).containsExactly( + entry("DESCRIPTION", "Catcher Glove"), + entry("ITEM_ID", 55) + ) + + assertThat(rows[3]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 6) + ) + + assertThat(rows[5]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testLeftJoinWithAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId + } + leftJoin(itemMaster, "im") on { + orderLine.itemId isEqualTo itemMaster.itemId + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[2]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 6) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testLeftJoinWithSubQuery() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, "im"(itemMaster.itemId), + itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId + } + leftJoin( + { + select(itemMaster.allColumns()) { + from(itemMaster) + } + + "im" + } + ) on { + orderLine.itemId isEqualTo "im"(itemMaster.itemId) + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join (select * from ItemMaster) im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[2]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 6) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testLeftJoinWithoutAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId + } + leftJoin(itemMaster) on { + orderLine.itemId isEqualTo itemMaster.itemId + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join ItemMaster on ol.item_id = ItemMaster.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[2]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 6) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testRightJoinWithAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId + } + rightJoin(itemMaster, "im") on { + orderLine.itemId isEqualTo itemMaster.itemId + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[0]).containsExactly( + entry("DESCRIPTION", "Catcher Glove"), + entry("ITEM_ID", 55) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testRightJoinWithSubQuery() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, + "im"(itemMaster.itemId), itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId + } + rightJoin( + { + select(itemMaster.allColumns()) { + from(itemMaster) + } + + "im" + } + ) on { + orderLine.itemId isEqualTo "im"(itemMaster.itemId) + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join (select * from ItemMaster) im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[0]).containsExactly( + entry("DESCRIPTION", "Catcher Glove"), + entry("ITEM_ID", 55) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testRightJoinWithoutAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId + } + rightJoin(itemMaster) on { + orderLine.itemId isEqualTo itemMaster.itemId + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join ItemMaster on ol.item_id = ItemMaster.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[0]).containsExactly( + entry("DESCRIPTION", "Catcher Glove"), + entry("ITEM_ID", 55) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testSelf() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + // create second table instance for self-join + val user2 = UserDynamicSQLSupport.User() + + // get Bamm Bamm's parent - should be Barney + val selectStatement = select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") on { + user.userId isEqualTo user2.parentId + } + where { user2.userId isEqualTo 4 } + } + + val expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(1) + assertThat(rows[0]).containsExactly( + entry("USER_ID", 2), + entry("USER_NAME", "Barney"), + ) + } + } + + @Test + fun testSelfWithNewAlias() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + // create second table instance for self-join + val user2 = user.withAlias("u2") + + // get Bamm Bamm's parent - should be Barney + val selectStatement = select(user.userId, user.userName, user.parentId) { + from(user) + join(user2) on { + user.userId isEqualTo user2.parentId + } + where { user2.userId isEqualTo 4 } + } + + val expectedStatement = "select User.user_id, User.user_name, User.parent_id" + + " from User join User u2 on User.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + assertThat(rows).hasSize(1) + + assertThat(rows[0]).containsExactly( + entry("USER_ID", 2), + entry("USER_NAME", "Barney"), + ) + } + } + + @Test + fun testSelfWithNewAliasAndOverride() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + // create second table instance for self-join + val user2 = user.withAlias("other_user") + + // get Bamm Bamm's parent - should be Barney + val selectStatement = select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") on { + user.userId isEqualTo user2.parentId + } + where { user2.userId isEqualTo 4 } + } + + val expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + val rows = mapper.selectManyMappedRows(selectStatement) + assertThat(rows).hasSize(1) + + assertThat(rows[0]).containsExactly( + entry("USER_ID", 2), + entry("USER_NAME", "Barney"), + ) + } + } + + @Test + fun testSelfWithNewAliasAndOverrideOddUsage() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + // create second table instance for self-join + val user2 = user.withAlias("other_user") + + // get Bamm Bamm's parent - should be Barney + val selectStatement = select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") on { + and { user.userId isEqualTo user2.parentId } + } + where { user2.userId isEqualTo 4 } + } + + val expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + val rows = mapper.selectManyMappedRows(selectStatement) + assertThat(rows).hasSize(1) + + assertThat(rows[0]).containsExactly( + entry("USER_ID", 2), + entry("USER_NAME", "Barney"), + ) + } + } + + @Test + fun testJoinWithNoOnCondition() { + // create second table instance for self-join + val user2 = user.withAlias("other_user") + + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") on { } + where { user2.userId isEqualTo 4 } + } + }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$ + } +} diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt index a60bc2105..5754fe631 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt @@ -813,6 +813,23 @@ class JoinMapperTest { }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$ } + @Test + fun testJoinWithDoubleOnCondition() { + // create second table instance for self-join + val user2 = user.withAlias("other_user") + + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") { + on(user.userId) equalTo user2.parentId + on(user.userId) equalTo user2.parentId + } + where { user2.userId isEqualTo 4 } + } + }.withMessage(Messages.getString("ERROR.45")) //$NON-NLS-1$ + } + @Test fun testThatAliasesPropagateToSubQueryConditions() { sqlSessionFactory.openSession().use { session ->