Skip to content

Add a SortSpecification for Columns in Joins #269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

This log will detail notable changes to MyBatis Dynamic SQL. Full details are available on the GitHub milestone pages.

## Release 1.3.0 - Unreleased

### Added

- Added a new sort specification that is useful in selects with joins ([#269](https://github.com/mybatis/mybatis-dynamic-sql/pull/269))

## Release 1.2.1 - September 29, 2020

GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.2.1+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.2.1+)
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/org/mybatis/dynamic/sql/SortSpecification.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ public interface SortSpecification {
SortSpecification descending();

/**
* Return the column alias or column name.
* Return the phrase that should be written into a rendered order by clause. This should
* NOT include the "DESC" word for descending sort specifications.
*
* @return the column alias if one has been specified by the user, or else the column name
* @return the order by phrase
*/
String aliasOrName();
String orderByName();

/**
* Return true if the sort order is descending.
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.mybatis.dynamic.sql.insert.InsertDSL;
import org.mybatis.dynamic.sql.insert.InsertSelectDSL;
import org.mybatis.dynamic.sql.insert.MultiRowInsertDSL;
import org.mybatis.dynamic.sql.select.ColumnSortSpecification;
import org.mybatis.dynamic.sql.select.CountDSL;
import org.mybatis.dynamic.sql.select.QueryExpressionDSL.FromGatherer;
import org.mybatis.dynamic.sql.select.SelectDSL;
Expand Down Expand Up @@ -737,10 +738,37 @@ static IsNotInCaseInsensitiveWhenPresent isNotInCaseInsensitiveWhenPresent(Colle
}

// order by support

/**
* Creates a sort specification based on a String. This is useful when a column has been
* aliased in the select list. For example:
*
* <pre>
* select(foo.as("bar"))
* .from(baz)
* .orderBy(sortColumn("bar"))
* </pre>
*
* @param name the string to use as a sort specification
* @return a sort specification
*/
static SortSpecification sortColumn(String name) {
return SimpleSortSpecification.of(name);
}

/**
* Creates a sort specification based on a column and a table alias. This can be useful in a join
* where the desired sort order is based on a column not in the select list. This will likely
* fail in union queries depending on database support.
*
* @param tableAlias the table alias
* @param column the column
* @return a sort specification
*/
static SortSpecification sortColumn(String tableAlias, SqlColumn<?> column) {
return new ColumnSortSpecification(tableAlias, column);
}

class InsertIntoNextStep {

private final SqlTable table;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/mybatis/dynamic/sql/SqlColumn.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public boolean isDescending() {
}

@Override
public String aliasOrName() {
public String orderByName() {
return alias().orElse(name);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2016-2020 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
*
* http://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;

import java.util.Objects;

import org.mybatis.dynamic.sql.SortSpecification;
import org.mybatis.dynamic.sql.SqlColumn;

public class ColumnSortSpecification implements SortSpecification {
private final String tableAlias;
private final SqlColumn<?> column;
private final boolean isDescending;

public ColumnSortSpecification(String tableAlias, SqlColumn<?> column) {
this(tableAlias, column, false);
}

private ColumnSortSpecification(String tableAlias, SqlColumn<?> column, boolean isDescending) {
this.tableAlias = Objects.requireNonNull(tableAlias);
this.column = Objects.requireNonNull(column);
this.isDescending = isDescending;
}

@Override
public SortSpecification descending() {
return new ColumnSortSpecification(tableAlias, column, true);
}

@Override
public String orderByName() {
return tableAlias + "." + column.name(); //$NON-NLS-1$
}

@Override
public boolean isDescending() {
return isDescending;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ public class SimpleSortSpecification implements SortSpecification {
private final boolean isDescending;

private SimpleSortSpecification(String name) {
this.name = Objects.requireNonNull(name);
this.isDescending = false;
this(name, false);
}

private SimpleSortSpecification(String name, boolean isDescending) {
Expand All @@ -46,7 +45,7 @@ public SortSpecification descending() {
}

@Override
public String aliasOrName() {
public String orderByName() {
return name;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private void renderOrderBy(FragmentCollector fragmentCollector, OrderByModel ord
}

private String calculateOrderByPhrase(SortSpecification column) {
String phrase = column.aliasOrName();
String phrase = column.orderByName();
if (column.isDescending()) {
phrase = phrase + " DESC"; //$NON-NLS-1$
}
Expand Down
15 changes: 12 additions & 3 deletions src/site/markdown/docs/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,22 @@ The XML element should look like this:
## Notes on Order By

Order by phrases can be difficult to calculate when there are aliased columns, aliased tables, unions, and joins.
This library has taken a simple approach - 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.
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).

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 is 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"))
Expand Down
78 changes: 78 additions & 0 deletions src/test/java/examples/joins/JoinMapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,84 @@ void testFullJoin3() {
}
}

@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, equalTo(orderLine.orderId)))
.fullJoin(itemMaster, "im", on(orderLine.itemId, equalTo(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<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);

assertThat(rows).hasSize(6);
Map<String, Object> 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, equalTo(orderLine.orderId)))
.fullJoin(itemMaster, "im", on(orderLine.itemId, equalTo(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<Map<String, Object>> rows = mapper.selectManyMappedRows(selectStatement);

assertThat(rows).hasSize(6);
Map<String, Object> 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()) {
Expand Down