View Javadoc
1   /*
2    *    Copyright 2016-2025 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    *       https://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.sql.JDBCType;
19  import java.util.Objects;
20  import java.util.Optional;
21  
22  import org.jspecify.annotations.Nullable;
23  import org.mybatis.dynamic.sql.render.RenderingContext;
24  import org.mybatis.dynamic.sql.render.RenderingStrategy;
25  import org.mybatis.dynamic.sql.util.FragmentAndParameters;
26  import org.mybatis.dynamic.sql.util.StringUtilities;
27  
28  /**
29   * This class represents the definition of a column in a table.
30   *
31   * <p>The class contains many attributes that are helpful for use in MyBatis and Spring runtime
32   * environments, but the only required attributes are the name of the column and a reference to
33   * the {@link SqlTable} the column is a part of.
34   *
35   * <p>The class can be extended if you wish to associate additional attributes with a column for your
36   * own purposes. Extending the class is a bit more challenging than you might expect because you may need to
37   * handle the covariant types for many methods in {@code SqlColumn}. Additionally, many methods in {@code SqlColumn}
38   * create new instances of the class in keeping with the library's primary strategy of immutability. You will also
39   * need to ensure that these methods create instances of your extended class, rather than the base {@code SqlColumn}
40   * class. We have worked to keep this process as simple as possible.
41   *
42   * <p>Extending the class involves the following activities:
43   * <ol>
44   *     <li>Create a class that extends {@link SqlColumn}</li>
45   *     <li>In your extended class, create a static builder class that extends {@link SqlColumn.AbstractBuilder}</li>
46   *     <li>Add your desired attributes to the class and the builder</li>
47   *     <li>You MUST override the {@link SqlColumn#copyBuilder()} method and return a new instance of
48   *       your builder with all attributes set. In the overridden method you should call the superclass
49   *       {@link SqlColumn#populateBaseBuilder(AbstractBuilder)} method
50   *       to set the attributes from the base {@code SqlColumn}, then populate your extended attributes. During normal
51   *       usage, the library may create additional instances of your class. If you do not override the
52   *       {@link SqlColumn#copyBuilder()} method properly, then your extended attributes will be lost.
53   *     </li>
54   *     <li>You MAY override the following methods. These methods are used with regular operations in the library and
55   *         create new instances of the class. However, these methods are not typically chained, so losing the specific
56   *         type may not be a problem. If you want to preserve the type, then you can override these methods
57   *         to specify the covariant return type. See below for usage of the {@link SqlColumn#cast(SqlColumn)} method
58   *         to make it easier to override these methods.
59   *       <ul>
60   *           <li>{@link SqlColumn#as(String)}</li>
61   *           <li>{@link SqlColumn#asCamelCase()}</li>
62   *           <li>{@link SqlColumn#descending()}</li>
63   *           <li>{@link SqlColumn#qualifiedWith(String)}</li>
64   *       </ul>
65   *     </li>
66   *     <li>You SHOULD override the following methods. These methods can be used to add additional attributes to a
67   *         column by creating a new instance with a specified attribute set. These methods are used during the
68   *         construction of columns. If you do not override these methods, and a user calls them, then the specific type
69   *         will be lost. If you want to preserve the type, then you can override these methods
70   *         to specify the covariant return type. See below for usage of the {@link SqlColumn#cast(SqlColumn)} method
71   *         to make it easier to override these methods.
72   *       <ul>
73   *           <li>{@link SqlColumn#withJavaProperty(String)}</li>
74   *           <li>{@link SqlColumn#withRenderingStrategy(RenderingStrategy)}</li>
75   *           <li>{@link SqlColumn#withTypeHandler(String)}</li>
76   *           <li>{@link SqlColumn#withJavaType(Class)}</li>
77   *           <li>{@link SqlColumn#withParameterTypeConverter(ParameterTypeConverter)}</li>
78   *       </ul>
79   *     </li>
80   * </ol>
81   *
82   * <p>For all overridden methods except {@code copyBuilder()}, the process is to call the superclass
83   * method and cast the result properly. We provide a {@link SqlColumn#cast(SqlColumn)} method to aid with this
84   * process. For example, overriding the {@code descending} method could look like this:
85   *
86   * <pre>
87   * {@code
88   * @Override
89   * public MyExtendedColumn<T> descending() {
90   *     return cast(super.descending());
91   * }
92   * }
93   * </pre>
94   *
95   * <p>The test code for this library contains an example of a fully executed extension of this class.
96   *
97   * @param <T> the Java type associated with the column
98   */
99  public class SqlColumn<T> implements BindableColumn<T>, SortSpecification {
100 
101     protected final String name;
102     protected final SqlTable table;
103     protected final @Nullable JDBCType jdbcType;
104     protected final String descendingPhrase;
105     protected final @Nullable String alias;
106     protected final @Nullable String typeHandler;
107     protected final @Nullable RenderingStrategy renderingStrategy;
108     protected final ParameterTypeConverter<T, ?> parameterTypeConverter;
109     protected final @Nullable String tableQualifier;
110     protected final @Nullable Class<T> javaType;
111     protected final @Nullable String javaProperty;
112 
113     protected SqlColumn(AbstractBuilder<T, ?, ?> builder) {
114         name = Objects.requireNonNull(builder.name);
115         table = Objects.requireNonNull(builder.table);
116         jdbcType = builder.jdbcType;
117         descendingPhrase = builder.descendingPhrase;
118         alias = builder.alias;
119         typeHandler = builder.typeHandler;
120         renderingStrategy = builder.renderingStrategy;
121         parameterTypeConverter = Objects.requireNonNull(builder.parameterTypeConverter);
122         tableQualifier = builder.tableQualifier;
123         javaType = builder.javaType;
124         javaProperty = builder.javaProperty;
125     }
126 
127     public String name() {
128         return name;
129     }
130 
131     public SqlTable table() {
132         return table;
133     }
134 
135     @Override
136     public Optional<JDBCType> jdbcType() {
137         return Optional.ofNullable(jdbcType);
138     }
139 
140     @Override
141     public Optional<String> alias() {
142         return Optional.ofNullable(alias);
143     }
144 
145     @Override
146     public Optional<String> typeHandler() {
147         return Optional.ofNullable(typeHandler);
148     }
149 
150     @Override
151     public Optional<Class<T>> javaType() {
152         return Optional.ofNullable(javaType);
153     }
154 
155     public Optional<String> javaProperty() {
156         return Optional.ofNullable(javaProperty);
157     }
158 
159     @Override
160     public @Nullable Object convertParameterType(@Nullable T value) {
161         return value == null ? null : parameterTypeConverter.convert(value);
162     }
163 
164     /**
165      * Create a new column instance that will render as descending when used in an order by phrase.
166      *
167      * @return a new column instance that will render as descending when used in an order by phrase
168      */
169     @Override
170     public SqlColumn<T> descending() {
171         return copyBuilder().withDescendingPhrase(" DESC").build(); //$NON-NLS-1$
172     }
173 
174     /**
175      * Create a new column instance with the specified alias that will render as "as alias" in a column list.
176      *
177      * @param alias
178      *            the column alias to set
179      *
180      * @return a new column instance with the specified alias
181      */
182     @Override
183     public SqlColumn<T> as(String alias) {
184         return copyBuilder().withAlias(alias).build();
185     }
186 
187     /**
188      * Override the calculated table qualifier if there is one. This is useful for sub-queries
189      * where the calculated table qualifier may not be correct in all cases.
190      *
191      * @param tableQualifier the table qualifier to apply to the rendered column name
192      * @return a new column that will be rendered with the specified table qualifier
193      */
194     public SqlColumn<T> qualifiedWith(String tableQualifier) {
195         return copyBuilder().withTableQualifier(tableQualifier).build();
196     }
197 
198     /**
199      * Set an alias with a camel-cased string based on the column name. This can be useful for queries using
200      * the {@link org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper} where the columns are placed into
201      * a map based on the column name returned from the database.
202      *
203      * <p>A camel case string is a mixed case string, and most databases do not support unquoted mixed case strings
204      * as identifiers. Therefore, the generated alias will be surrounded by double quotes thereby making it a
205      * quoted identifier. Most databases will respect quoted mixed case identifiers.
206      *
207      * @return a new column aliased with a camel case version of the column name
208      */
209     public SqlColumn<T> asCamelCase() {
210         return copyBuilder()
211                 .withAlias("\"" + StringUtilities.toCamelCase(name) + "\"") //$NON-NLS-1$ //$NON-NLS-2$
212                 .build();
213     }
214 
215     @Override
216     public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) {
217         return FragmentAndParameters.fromFragment(alias().orElse(name) + descendingPhrase);
218     }
219 
220     @Override
221     public FragmentAndParameters render(RenderingContext renderingContext) {
222         if (tableQualifier == null) {
223             return FragmentAndParameters.fromFragment(renderingContext.aliasedColumnName(this));
224         } else {
225             return FragmentAndParameters.fromFragment(renderingContext.aliasedColumnName(this, tableQualifier));
226         }
227     }
228 
229     @Override
230     public Optional<RenderingStrategy> renderingStrategy() {
231         return Optional.ofNullable(renderingStrategy);
232     }
233 
234     /**
235      * Create a new column instance with the specified type handler.
236      *
237      * <p>This method uses a different type (S). This allows it to be chained with the other
238      * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
239      * Without this different type (for example, if we used T), the compiler would erase the type after the call
240      * and method chaining would not work. This is a workaround for Java's lack of reification.
241      *
242      * @param typeHandler the type handler to set
243      * @param <S> the type of the new column (will be the same as T)
244      * @return a new column instance with the specified type handler
245      */
246     public <S> SqlColumn<S> withTypeHandler(String typeHandler) {
247         return cast(copyBuilder().withTypeHandler(typeHandler).build());
248     }
249 
250     /**
251      * Create a new column instance with the specified rendering strategy.
252      *
253      * <p>This method uses a different type (S). This allows it to be chained with the other
254      * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
255      * Without this different type (for example, if we used T), the compiler would erase the type after the call
256      * and method chaining would not work. This is a workaround for Java's lack of reification.
257      *
258      * @param renderingStrategy the rendering strategy to set
259      * @param <S> the type of the new column (will be the same as T)
260      * @return a new column instance with the specified type handler
261      */
262     public <S> SqlColumn<S> withRenderingStrategy(RenderingStrategy renderingStrategy) {
263         return cast(copyBuilder().withRenderingStrategy(renderingStrategy).build());
264     }
265 
266     /**
267      * Create a new column instance with the specified parameter type converter.
268      *
269      * <p>Parameter type converters are useful with Spring JDBC. Typically, they are not needed for MyBatis.
270      *
271      * <p>This method uses a different type (S). This allows it to be chained with the other
272      * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
273      * Without this different type (for example, if we used T), the compiler would erase the type after the call
274      * and method chaining would not work. This is a workaround for Java's lack of reification.
275      *
276      * @param parameterTypeConverter the parameter type converter to set
277      * @param <S> the type of the new column (will be the same as T)
278      * @return a new column instance with the specified type handler
279      */
280     @SuppressWarnings("unchecked")
281     public <S> SqlColumn<S> withParameterTypeConverter(ParameterTypeConverter<S, ?> parameterTypeConverter) {
282         return cast(copyBuilder().withParameterTypeConverter((ParameterTypeConverter<T, ?>) parameterTypeConverter)
283                 .build());
284     }
285 
286     /**
287      * Create a new column instance with the specified Java type.
288      *
289      * <p>Specifying a Java type will force rendering of the Java type for MyBatis parameters. This can be useful
290      * with some MyBatis type handlers.
291      *
292      * <p>This method uses a different type (S). This allows it to be chained with the other
293      * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
294      * Without this different type (for example, if we used T), the compiler would erase the type after the call
295      * and method chaining would not work. This is a workaround for Java's lack of reification.
296      *
297      * @param javaType the Java type to set
298      * @param <S> the type of the new column (will be the same as T)
299      * @return a new column instance with the specified type handler
300      */
301     @SuppressWarnings("unchecked")
302     public <S> SqlColumn<S> withJavaType(Class<S> javaType) {
303         return cast(copyBuilder().withJavaType((Class<T>) javaType).build());
304     }
305 
306     /**
307      * Create a new column instance with the specified Java property.
308      *
309      * <p>Specifying a Java property in the column will allow usage of the column as a "mapped column" in record-based
310      * insert statements.
311      *
312      * <p>This method uses a different type (S). This allows it to be chained with the other
313      * with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
314      * Without this different type (for example, if we used T), the compiler would erase the type after the call
315      * and method chaining would not work. This is a workaround for Java's lack of reification.
316      *
317      * @param javaProperty the Java property to set
318      * @param <S> the type of the new column (will be the same as T)
319      * @return a new column instance with the specified type handler
320      */
321     public <S> SqlColumn<S> withJavaProperty(String javaProperty) {
322         return cast(copyBuilder().withJavaProperty(javaProperty).build());
323     }
324 
325     /**
326      * Create a new Builder, then populate all attributes in the builder with current values.
327      *
328      * <p>This method is used to create copies of the class during normal operations (e.g. when calling the
329      * {@link SqlColumn#as(String)} method). Any subclass of {@code SqlColumn} MUST override this method.
330      *
331      * @return a new Builder instance with all current values populated
332      */
333     protected AbstractBuilder<T, ?, ?> copyBuilder() {
334         return populateBaseBuilder(new Builder<>());
335     }
336 
337     @SuppressWarnings("unchecked")
338     protected <S extends SqlColumn<?>> S cast(SqlColumn<?> column) {
339         return (S) column;
340     }
341 
342     /**
343      * This method will add all current attributes to the specified builder. It is useful when creating
344      * new class instances that only change one attribute - we set all current attributes, then
345      * change the one attribute. This utility can be used with the with* methods and other methods that
346      * create new instances.
347      *
348      * @param <B> the concrete builder type
349      * @return the populated builder
350      */
351     @SuppressWarnings("unchecked")
352     protected <B extends AbstractBuilder<T, ?, ?>> B populateBaseBuilder(B builder) {
353         return (B) builder
354                 .withName(this.name)
355                 .withTable(this.table)
356                 .withJdbcType(this.jdbcType)
357                 .withDescendingPhrase(this.descendingPhrase)
358                 .withAlias(this.alias)
359                 .withTypeHandler(this.typeHandler)
360                 .withRenderingStrategy(this.renderingStrategy)
361                 .withParameterTypeConverter(this.parameterTypeConverter)
362                 .withTableQualifier(this.tableQualifier)
363                 .withJavaType(this.javaType)
364                 .withJavaProperty(this.javaProperty);
365     }
366 
367     public static <T> SqlColumn<T> of(String name, SqlTable table) {
368         return new Builder<T>().withName(name)
369                 .withTable(table)
370                 .build();
371     }
372 
373     public static <T> SqlColumn<T> of(String name, SqlTable table, JDBCType jdbcType) {
374         return new Builder<T>().withName(name)
375                 .withTable(table)
376                 .withJdbcType(jdbcType)
377                 .build();
378     }
379 
380     public abstract static class AbstractBuilder<T, C extends SqlColumn<T>, B extends AbstractBuilder<T, C, B>> {
381         protected @Nullable String name;
382         protected @Nullable SqlTable table;
383         protected @Nullable JDBCType jdbcType;
384         protected String descendingPhrase = ""; //$NON-NLS-1$
385         protected @Nullable String alias;
386         protected @Nullable String typeHandler;
387         protected @Nullable RenderingStrategy renderingStrategy;
388         protected ParameterTypeConverter<T, ?> parameterTypeConverter = v -> v;
389         protected @Nullable String tableQualifier;
390         protected @Nullable Class<T> javaType;
391         protected @Nullable String javaProperty;
392 
393         public B withName(String name) {
394             this.name = name;
395             return getThis();
396         }
397 
398         public B withTable(SqlTable table) {
399             this.table = table;
400             return getThis();
401         }
402 
403         public B withJdbcType(@Nullable JDBCType jdbcType) {
404             this.jdbcType = jdbcType;
405             return getThis();
406         }
407 
408         public B withDescendingPhrase(String descendingPhrase) {
409             this.descendingPhrase = descendingPhrase;
410             return getThis();
411         }
412 
413         public B withAlias(@Nullable String alias) {
414             this.alias = alias;
415             return getThis();
416         }
417 
418         public B withTypeHandler(@Nullable String typeHandler) {
419             this.typeHandler = typeHandler;
420             return getThis();
421         }
422 
423         public B withRenderingStrategy(@Nullable RenderingStrategy renderingStrategy) {
424             this.renderingStrategy = renderingStrategy;
425             return getThis();
426         }
427 
428         public B withParameterTypeConverter(ParameterTypeConverter<T, ?> parameterTypeConverter) {
429             this.parameterTypeConverter = parameterTypeConverter;
430             return getThis();
431         }
432 
433         public B withTableQualifier(@Nullable String tableQualifier) {
434             this.tableQualifier = tableQualifier;
435             return getThis();
436         }
437 
438         public B withJavaType(@Nullable Class<T> javaType) {
439             this.javaType = javaType;
440             return getThis();
441         }
442 
443         public B withJavaProperty(@Nullable String javaProperty) {
444             this.javaProperty = javaProperty;
445             return getThis();
446         }
447 
448         protected abstract B getThis();
449 
450         public abstract C build();
451     }
452 
453     public static class Builder<T> extends AbstractBuilder<T, SqlColumn<T>, Builder<T>> {
454         @Override
455         public SqlColumn<T> build() {
456             return new SqlColumn<>(this);
457         }
458 
459         @Override
460         protected Builder<T> getThis() {
461             return this;
462         }
463     }
464 }