Skip to content

Commit d1a4207

Browse files
authored
Merge pull request #426 from jeffgbutler/new-alias-strategy
Add the Ability to Specify an Alias in a Table Object
2 parents 8fa2e76 + 2a03903 commit d1a4207

File tree

12 files changed

+339
-32
lines changed

12 files changed

+339
-32
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

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

5+
## Release 1.3.1 - Unreleased
6+
7+
GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.3.1+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.3.1+)
8+
9+
### Added
10+
11+
- Added the ability to specify a JavaType associated with a column. The JavaType will be rendered properly for MyBatis ([#386](https://github.com/mybatis/mybatis-dynamic-sql/pull/386))
12+
- Added a few missing groupBy and orderBy methods on the `select` statement ([#409](https://github.com/mybatis/mybatis-dynamic-sql/pull/409))
13+
- Added a check for when a table alias is re-used in error (typically in a self-join) ([#425](https://github.com/mybatis/mybatis-dynamic-sql/pull/425))
14+
- Added a new extension of SqlTable that supports setting a table alias directly within the table definition ([#426](https://github.com/mybatis/mybatis-dynamic-sql/pull/426))
15+
516
## Release 1.3.0 - May 6, 2021
617

718
GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.3.0+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.3.0+)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2016-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.mybatis.dynamic.sql;
17+
18+
import java.util.Objects;
19+
import java.util.Optional;
20+
import java.util.function.Supplier;
21+
22+
public abstract class AliasableSqlTable<T extends AliasableSqlTable<T>> extends SqlTable {
23+
24+
private String tableAlias;
25+
private final Supplier<T> constructor;
26+
27+
protected AliasableSqlTable(String tableName, Supplier<T> constructor) {
28+
super(tableName);
29+
this.constructor = Objects.requireNonNull(constructor);
30+
}
31+
32+
protected AliasableSqlTable(Supplier<String> tableNameSupplier, Supplier<T> constructor) {
33+
super(tableNameSupplier);
34+
this.constructor = Objects.requireNonNull(constructor);
35+
}
36+
37+
public T withAlias(String alias) {
38+
T newTable = constructor.get();
39+
((AliasableSqlTable<T>) newTable).tableAlias = alias;
40+
return newTable;
41+
}
42+
43+
@Override
44+
public Optional<String> tableAlias() {
45+
return Optional.ofNullable(tableAlias);
46+
}
47+
}

src/main/java/org/mybatis/dynamic/sql/SqlTable.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -108,6 +108,10 @@ public <R> R accept(TableExpressionVisitor<R> visitor) {
108108
return visitor.visit(this);
109109
}
110110

111+
public Optional<String> tableAlias() {
112+
return Optional.empty();
113+
}
114+
111115
public static SqlTable of(String name) {
112116
return new SqlTable(name);
113117
}

src/main/java/org/mybatis/dynamic/sql/exception/DuplicateTableAliasException.java

+4-9
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,13 @@
3232
*/
3333
public class DuplicateTableAliasException extends RuntimeException {
3434

35-
private final SqlTable table;
36-
private final String newAlias;
37-
private final String existingAlias;
38-
3935
public DuplicateTableAliasException(SqlTable table, String newAlias, String existingAlias) {
40-
this.table = Objects.requireNonNull(table);
41-
this.newAlias = Objects.requireNonNull(newAlias);
42-
this.existingAlias = Objects.requireNonNull(existingAlias);
36+
super(generateMessage(Objects.requireNonNull(table),
37+
Objects.requireNonNull(newAlias),
38+
Objects.requireNonNull(existingAlias)));
4339
}
4440

45-
@Override
46-
public String getMessage() {
41+
private static String generateMessage(SqlTable table, String newAlias, String existingAlias) {
4742
return "Table \"" + table.tableNameAtRuntime() //$NON-NLS-1$
4843
+ "\" with requested alias \"" + newAlias //$NON-NLS-1$
4944
+ "\" is already aliased in this query with alias \"" + existingAlias //$NON-NLS-1$

src/main/java/org/mybatis/dynamic/sql/render/GuaranteedTableAliasCalculator.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,9 +35,12 @@ private GuaranteedTableAliasCalculator(Map<SqlTable, String> aliases) {
3535

3636
@Override
3737
public Optional<String> aliasForColumn(SqlTable table) {
38-
return super.aliasForColumn(table)
39-
.map(Optional::of)
40-
.orElseGet(() -> Optional.of(table.tableNameAtRuntime()));
38+
Optional<String> alias = super.aliasForColumn(table);
39+
if (alias.isPresent()) {
40+
return alias;
41+
} else {
42+
return Optional.of(table.tableNameAtRuntime());
43+
}
4144
}
4245

4346
public static TableAliasCalculator of(Map<SqlTable, String> aliases) {

src/main/java/org/mybatis/dynamic/sql/render/TableAliasCalculator.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,11 +32,20 @@ protected TableAliasCalculator(Map<SqlTable, String> aliases) {
3232
}
3333

3434
public Optional<String> aliasForColumn(SqlTable table) {
35-
return Optional.ofNullable(aliases.get(table));
35+
return explicitAliasOrTableAlias(table);
3636
}
3737

3838
public Optional<String> aliasForTable(SqlTable table) {
39-
return Optional.ofNullable(aliases.get(table));
39+
return explicitAliasOrTableAlias(table);
40+
}
41+
42+
private Optional<String> explicitAliasOrTableAlias(SqlTable table) {
43+
String alias = aliases.get(table);
44+
if (alias == null) {
45+
return table.tableAlias();
46+
} else {
47+
return Optional.of(alias);
48+
}
4049
}
4150

4251
public static TableAliasCalculator of(SqlTable table, String alias) {

src/site/markdown/docs/databaseObjects.md

+82
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,88 @@ public static final class User extends SqlTable {
141141

142142
Whenever the table is referenced for rendering SQL, the name will be calculated based on the current value of the system property.
143143

144+
## Aliased Tables
145+
146+
In join queries, it is usually a good practice to specify table aliases. The `select` statement includes
147+
support for specifying table aliases in each query in a way that looks like natural SQL. For example:
148+
149+
```java
150+
SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity)
151+
.from(orderMaster, "om")
152+
.join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId))
153+
.join(itemMaster, "im").on(orderLine.itemId, equalTo(itemMaster.itemId))
154+
.where(orderMaster.orderId, isEqualTo(2))
155+
.build()
156+
.render(RenderingStrategies.MYBATIS3);
157+
```
158+
159+
In a query like this, the library will automatically append the table alias to the column name when the query is rendered.
160+
Internally, the alias for a column is determined by looking up the associated table in a HashMap maintained within the
161+
query model. If you do not specify a table alias, the library will automatically append the table name in join queries.
162+
163+
Unfortunately, this strategy fails for self-joins. It can also get confusing when there are sub-queries. Imagine a
164+
query like this:
165+
166+
```java
167+
SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId)
168+
.from(user, "u1")
169+
.join(user, "u2").on(user.userId, equalTo(user.parentId))
170+
.where(user.userId, isEqualTo(4))
171+
.build()
172+
.render(RenderingStrategies.MYBATIS3);
173+
```
174+
175+
In this query it is not clear which instance of the `user` table is used for each column, and there will only be entry in the
176+
HashMap for the `user` table - so only one of the aliases specified in the select statement will be in effect.
177+
There are two ways to deal with this problem.
178+
179+
The first is to simply create another instance of the User SqlTable object. With this method it is very clear which column
180+
belongs to which instance of the table and the library can easily calculate aliases:
181+
182+
```java
183+
User user1 = new User();
184+
User user2 = new User();
185+
SelectStatementProvider selectStatement = select(user1.userId, user1.userName, user1.parentId)
186+
.from(user1, "u1")
187+
.join(user2, "u2").on(user1.userId, equalTo(user2.parentId))
188+
.where(user2.userId, isEqualTo(4))
189+
.build()
190+
.render(RenderingStrategies.MYBATIS3);
191+
```
192+
193+
Starting with version 1.3.1, there is new method where the alias can be specified in the table object itself. This allows
194+
you to move the aliases out of the `select` statement.
195+
196+
```java
197+
User user1 = user.withAlias("u1");
198+
User user2 = user.withAlias("u2");
199+
200+
SelectStatementProvider selectStatement = select(user1.userId, user1.userName, user1.parentId)
201+
.from(user1)
202+
.join(user2).on(user1.userId, equalTo(user2.parentId))
203+
.where(user2.userId, isEqualTo(4))
204+
.build()
205+
.render(RenderingStrategies.MYBATIS3);
206+
```
207+
208+
To enable this support, your table objects should extend `org.mybatis.dynamic.sql.AliasableSqlTable` rather than
209+
`org.mybatis.dynamic.sql.SqlTable` as follows:
210+
211+
```java
212+
public static final class User extends AliasableSqlTable<User> {
213+
public final SqlColumn<Integer> userId = column("user_id", JDBCType.INTEGER);
214+
public final SqlColumn<String> userName = column("user_name", JDBCType.VARCHAR);
215+
public final SqlColumn<Integer> parentId = column("parent_id", JDBCType.INTEGER);
216+
217+
public User() {
218+
super("User", User::new);
219+
}
220+
}
221+
```
222+
223+
If you use an aliased table object, and also specify an alias in the `select` statement, the alias from the `select`
224+
statement will override the alias in the table object.
225+
144226
## Column Representation
145227

146228
The class `org.mybatis.dynamic.sql.SqlColumn` is used to represent a column in a table or view. An `SqlColumn` is always

src/site/markdown/docs/select.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ The library supports the generation of UNION and UNION ALL queries. For example:
8585

8686
Any number of SELECT statements can be added to a UNION query. Only one ORDER BY phrase is allowed.
8787

88-
## Annotated Mapper for Select Statements
88+
## MyBatis Mapper for Select Statements
8989

9090
The SelectStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you
9191
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):

src/test/java/examples/joins/JoinMapperTest.java

+69-8
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import org.junit.jupiter.api.Test;
4545
import org.mybatis.dynamic.sql.exception.DuplicateTableAliasException;
4646
import org.mybatis.dynamic.sql.render.RenderingStrategies;
47+
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
48+
import org.mybatis.dynamic.sql.select.SelectModel;
4749
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
4850
import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper;
4951

@@ -948,14 +950,73 @@ void testSelf() {
948950

949951
@Test
950952
void testSelfWithDuplicateAlias() {
951-
assertThatExceptionOfType(DuplicateTableAliasException.class).isThrownBy(() ->
952-
select(user.userId, user.userName, user.parentId)
953-
.from(user, "u1")
954-
.join(user, "u2").on(user.userId, equalTo(user.parentId))
955-
.where(user.userId, isEqualTo(4))
956-
.build()
957-
.render(RenderingStrategies.MYBATIS3)
958-
).withMessage("Table \"User\" with requested alias \"u2\" is already aliased in this query with alias \"u1\". Attempting to re-alias a table in the same query is not supported.");
953+
QueryExpressionDSL<SelectModel> dsl = select(user.userId, user.userName, user.parentId)
954+
.from(user, "u1");
955+
956+
assertThatExceptionOfType(DuplicateTableAliasException.class).isThrownBy(() -> dsl.join(user, "u2"))
957+
.withMessage("Table \"User\" with requested alias \"u2\" is already aliased in this query with alias \"u1\". Attempting to re-alias a table in the same query is not supported.");
958+
}
959+
960+
@Test
961+
void testSelfWithNewAlias() {
962+
try (SqlSession session = sqlSessionFactory.openSession()) {
963+
JoinMapper mapper = session.getMapper(JoinMapper.class);
964+
965+
// create second table instance for self-join
966+
UserDynamicSQLSupport.User user2 = user.withAlias("u2");
967+
968+
// get Bamm Bamm's parent - should be Barney
969+
SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId)
970+
.from(user)
971+
.join(user2).on(user.userId, equalTo(user2.parentId))
972+
.where(user2.userId, isEqualTo(4))
973+
.build()
974+
.render(RenderingStrategies.MYBATIS3);
975+
976+
String expectedStatement = "select User.user_id, User.user_name, User.parent_id"
977+
+ " from User join User u2 on User.user_id = u2.parent_id"
978+
+ " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}";
979+
assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement);
980+
981+
List<User> rows = mapper.selectUsers(selectStatement);
982+
983+
assertThat(rows).hasSize(1);
984+
User row = rows.get(0);
985+
assertThat(row.getUserId()).isEqualTo(2);
986+
assertThat(row.getUserName()).isEqualTo("Barney");
987+
assertThat(row.getParentId()).isNull();
988+
}
989+
}
990+
991+
@Test
992+
void testSelfWithNewAliasAndOverride() {
993+
try (SqlSession session = sqlSessionFactory.openSession()) {
994+
JoinMapper mapper = session.getMapper(JoinMapper.class);
995+
996+
// create second table instance for self-join
997+
UserDynamicSQLSupport.User user2 = user.withAlias("other_user");
998+
999+
// get Bamm Bamm's parent - should be Barney
1000+
SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId)
1001+
.from(user, "u1")
1002+
.join(user2, "u2").on(user.userId, equalTo(user2.parentId))
1003+
.where(user2.userId, isEqualTo(4))
1004+
.build()
1005+
.render(RenderingStrategies.MYBATIS3);
1006+
1007+
String expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id"
1008+
+ " from User u1 join User u2 on u1.user_id = u2.parent_id"
1009+
+ " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}";
1010+
assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement);
1011+
1012+
List<User> rows = mapper.selectUsers(selectStatement);
1013+
1014+
assertThat(rows).hasSize(1);
1015+
User row = rows.get(0);
1016+
assertThat(row.getUserId()).isEqualTo(2);
1017+
assertThat(row.getUserName()).isEqualTo("Barney");
1018+
assertThat(row.getParentId()).isNull();
1019+
}
9591020
}
9601021

9611022
@Test

src/test/java/examples/joins/UserDynamicSQLSupport.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,22 +17,22 @@
1717

1818
import java.sql.JDBCType;
1919

20+
import org.mybatis.dynamic.sql.AliasableSqlTable;
2021
import org.mybatis.dynamic.sql.SqlColumn;
21-
import org.mybatis.dynamic.sql.SqlTable;
2222

2323
public class UserDynamicSQLSupport {
2424
public static final User user = new User();
2525
public final SqlColumn<Integer> userId = user.userId;
2626
public final SqlColumn<String> userName = user.userName;
2727
public final SqlColumn<Integer> parentId = user.parentId;
2828

29-
public static final class User extends SqlTable {
29+
public static final class User extends AliasableSqlTable<User> {
3030
public final SqlColumn<Integer> userId = column("user_id", JDBCType.INTEGER);
3131
public final SqlColumn<String> userName = column("user_name", JDBCType.VARCHAR);
3232
public final SqlColumn<Integer> parentId = column("parent_id", JDBCType.INTEGER);
3333

3434
public User() {
35-
super("User");
35+
super(() -> "User", User::new);
3636
}
3737
}
3838
}

0 commit comments

Comments
 (0)