SqlColumn.java
/*
* Copyright 2016-2026 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;
import java.sql.JDBCType;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;
import org.mybatis.dynamic.sql.render.RenderingContext;
import org.mybatis.dynamic.sql.render.RenderingStrategy;
import org.mybatis.dynamic.sql.util.FragmentAndParameters;
import org.mybatis.dynamic.sql.util.StringUtilities;
/**
* This class represents the definition of a column in a table.
*
* <p>The class contains many attributes that are helpful for use in MyBatis and Spring runtime
* environments, but the only required attributes are the name of the column and a reference to
* the {@link SqlTable} the column is a part of.
*
* <p>The class can be extended if you wish to associate additional attributes with a column for your
* own purposes. Extending the class is a bit more challenging than you might expect because you may need to
* handle the covariant types for many methods in {@code SqlColumn}. Additionally, many methods in {@code SqlColumn}
* create new instances of the class in keeping with the library's primary strategy of immutability. You will also
* need to ensure that these methods create instances of your extended class, rather than the base {@code SqlColumn}
* class. We have worked to keep this process as simple as possible.
*
* <p>Extending the class involves the following activities:
* <ol>
* <li>Create a class that extends {@link SqlColumn}</li>
* <li>In your extended class, create a static builder class that extends {@link SqlColumn.AbstractBuilder}</li>
* <li>Add your desired attributes to the class and the builder</li>
* <li>You MUST override the {@link SqlColumn#copyBuilder()} method and return a new instance of
* your builder with all attributes set. In the overridden method you should call the superclass
* {@link SqlColumn#populateBaseBuilder(AbstractBuilder)} method
* to set the attributes from the base {@code SqlColumn}, then populate your extended attributes. During normal
* usage, the library may create additional instances of your class. If you do not override the
* {@link SqlColumn#copyBuilder()} method properly, then your extended attributes will be lost.
* </li>
* <li>You MAY override the following methods. These methods are used with regular operations in the library and
* create new instances of the class. However, these methods are not typically chained, so losing the specific
* type may not be a problem. If you want to preserve the type, then you can override these methods
* to specify the covariant return type. See below for usage of the {@link SqlColumn#cast(SqlColumn)} method
* to make it easier to override these methods.
* <ul>
* <li>{@link SqlColumn#as(String)}</li>
* <li>{@link SqlColumn#asCamelCase()}</li>
* <li>{@link SqlColumn#descending()}</li>
* <li>{@link SqlColumn#qualifiedWith(String)}</li>
* </ul>
* </li>
* <li>You SHOULD override the following methods. These methods can be used to add additional attributes to a
* column by creating a new instance with a specified attribute set. These methods are used during the
* construction of columns. If you do not override these methods, and a user calls them, then the specific type
* will be lost. If you want to preserve the type, then you can override these methods
* to specify the covariant return type. See below for usage of the {@link SqlColumn#cast(SqlColumn)} method
* to make it easier to override these methods.
* <ul>
* <li>{@link SqlColumn#withJavaProperty(String)}</li>
* <li>{@link SqlColumn#withRenderingStrategy(RenderingStrategy)}</li>
* <li>{@link SqlColumn#withTypeHandler(String)}</li>
* <li>{@link SqlColumn#withJavaType(Class)}</li>
* <li>{@link SqlColumn#withParameterTypeConverter(ParameterTypeConverter)}</li>
* </ul>
* </li>
* </ol>
*
* <p>For all overridden methods except {@code copyBuilder()}, the process is to call the superclass
* method and cast the result properly. We provide a {@link SqlColumn#cast(SqlColumn)} method to aid with this
* process. For example, overriding the {@code descending} method could look like this:
*
* <pre>
* {@code
* @Override
* public MyExtendedColumn<T> descending() {
* return cast(super.descending());
* }
* }
* </pre>
*
* <p>The test code for this library contains an example of a fully executed extension of this class.
*
* @param <T> the Java type associated with the column
*/
public class SqlColumn<T> implements BindableColumn<T>, SortSpecification {
protected final String name;
protected final SqlTable table;
protected final @Nullable JDBCType jdbcType;
protected final String descendingPhrase;
protected final @Nullable String alias;
protected final @Nullable String typeHandler;
protected final @Nullable RenderingStrategy renderingStrategy;
protected final ParameterTypeConverter<T, ?> parameterTypeConverter;
protected final @Nullable String tableQualifier;
protected final @Nullable Class<T> javaType;
protected final @Nullable String javaProperty;
protected SqlColumn(AbstractBuilder<T, ?> builder) {
name = Objects.requireNonNull(builder.name);
table = Objects.requireNonNull(builder.table);
jdbcType = builder.jdbcType;
descendingPhrase = builder.descendingPhrase;
alias = builder.alias;
typeHandler = builder.typeHandler;
renderingStrategy = builder.renderingStrategy;
parameterTypeConverter = Objects.requireNonNull(builder.parameterTypeConverter);
tableQualifier = builder.tableQualifier;
javaType = builder.javaType;
javaProperty = builder.javaProperty;
}
public String name() {
return name;
}
public SqlTable table() {
return table;
}
@Override
public Optional<JDBCType> jdbcType() {
return Optional.ofNullable(jdbcType);
}
@Override
public Optional<String> alias() {
return Optional.ofNullable(alias);
}
@Override
public Optional<String> typeHandler() {
return Optional.ofNullable(typeHandler);
}
@Override
public Optional<Class<T>> javaType() {
return Optional.ofNullable(javaType);
}
public Optional<String> javaProperty() {
return Optional.ofNullable(javaProperty);
}
@Override
public @Nullable Object convertParameterType(@Nullable T value) {
return value == null ? null : parameterTypeConverter.convert(value);
}
/**
* Create a new column instance that will render as descending when used in an order by phrase.
*
* @return a new column instance that will render as descending when used in an order by phrase
*/
@Override
public SqlColumn<T> descending() {
return copyBuilder().withDescendingPhrase(" DESC").build(); //$NON-NLS-1$
}
/**
* Create a new column instance with the specified alias that will render as "as alias" in a column list.
*
* @param alias
* the column alias to set
*
* @return a new column instance with the specified alias
*/
@Override
public SqlColumn<T> as(String alias) {
return copyBuilder().withAlias(alias).build();
}
/**
* Override the calculated table qualifier if there is one. This is useful for sub-queries
* where the calculated table qualifier may not be correct in all cases.
*
* @param tableQualifier the table qualifier to apply to the rendered column name
* @return a new column that will be rendered with the specified table qualifier
*/
public SqlColumn<T> qualifiedWith(String tableQualifier) {
return copyBuilder().withTableQualifier(tableQualifier).build();
}
/**
* Set an alias with a camel-cased string based on the column name. This can be useful for queries using
* the {@link org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper} where the columns are placed into
* a map based on the column name returned from the database.
*
* <p>A camel case string is a mixed case string, and most databases do not support unquoted mixed case strings
* as identifiers. Therefore, the generated alias will be surrounded by double quotes thereby making it a
* quoted identifier. Most databases will respect quoted mixed case identifiers.
*
* @return a new column aliased with a camel case version of the column name
*/
public SqlColumn<T> asCamelCase() {
return copyBuilder()
.withAlias("\"" + StringUtilities.toCamelCase(name) + "\"") //$NON-NLS-1$ //$NON-NLS-2$
.build();
}
@Override
public FragmentAndParameters renderForOrderBy(RenderingContext renderingContext) {
return FragmentAndParameters.fromFragment(alias().orElse(name) + descendingPhrase);
}
@Override
public FragmentAndParameters render(RenderingContext renderingContext) {
if (tableQualifier == null) {
return FragmentAndParameters.fromFragment(renderingContext.aliasedColumnName(this));
} else {
return FragmentAndParameters.fromFragment(renderingContext.aliasedColumnName(this, tableQualifier));
}
}
@Override
public Optional<RenderingStrategy> renderingStrategy() {
return Optional.ofNullable(renderingStrategy);
}
/**
* Create a new column instance with the specified type handler.
*
* <p>This method uses a different type (S). This allows it to be chained with the other
* with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
* Without this different type (for example, if we used T), the compiler would erase the type after the call
* and method chaining would not work. This is a workaround for Java's lack of reification.
*
* @param typeHandler the type handler to set
* @param <S> the type of the new column (will be the same as T)
* @return a new column instance with the specified type handler
*/
public <S> SqlColumn<S> withTypeHandler(String typeHandler) {
return cast(copyBuilder().withTypeHandler(typeHandler).build());
}
/**
* Create a new column instance with the specified rendering strategy.
*
* <p>This method uses a different type (S). This allows it to be chained with the other
* with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
* Without this different type (for example, if we used T), the compiler would erase the type after the call
* and method chaining would not work. This is a workaround for Java's lack of reification.
*
* @param renderingStrategy the rendering strategy to set
* @param <S> the type of the new column (will be the same as T)
* @return a new column instance with the specified type handler
*/
public <S> SqlColumn<S> withRenderingStrategy(RenderingStrategy renderingStrategy) {
return cast(copyBuilder().withRenderingStrategy(renderingStrategy).build());
}
/**
* Create a new column instance with the specified parameter type converter.
*
* <p>Parameter type converters are useful with Spring JDBC. Typically, they are not needed for MyBatis.
*
* <p>This method uses a different type (S). This allows it to be chained with the other
* with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
* Without this different type (for example, if we used T), the compiler would erase the type after the call
* and method chaining would not work. This is a workaround for Java's lack of reification.
*
* @param parameterTypeConverter the parameter type converter to set
* @param <S> the type of the new column (will be the same as T)
* @return a new column instance with the specified type handler
*/
@SuppressWarnings("unchecked")
public <S> SqlColumn<S> withParameterTypeConverter(ParameterTypeConverter<S, ?> parameterTypeConverter) {
return cast(copyBuilder().withParameterTypeConverter((ParameterTypeConverter<T, ?>) parameterTypeConverter)
.build());
}
/**
* Create a new column instance with the specified Java type.
*
* <p>Specifying a Java type will force rendering of the Java type for MyBatis parameters. This can be useful
* with some MyBatis type handlers.
*
* <p>This method uses a different type (S). This allows it to be chained with the other
* with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
* Without this different type (for example, if we used T), the compiler would erase the type after the call
* and method chaining would not work. This is a workaround for Java's lack of reification.
*
* @param javaType the Java type to set
* @param <S> the type of the new column (will be the same as T)
* @return a new column instance with the specified type handler
*/
@SuppressWarnings("unchecked")
public <S> SqlColumn<S> withJavaType(Class<S> javaType) {
return cast(copyBuilder().withJavaType((Class<T>) javaType).build());
}
/**
* Create a new column instance with the specified Java property.
*
* <p>Specifying a Java property in the column will allow usage of the column as a "mapped column" in record-based
* insert statements.
*
* <p>This method uses a different type (S). This allows it to be chained with the other
* with* methods. Using new types forces the compiler to delay type inference until the end of a call chain.
* Without this different type (for example, if we used T), the compiler would erase the type after the call
* and method chaining would not work. This is a workaround for Java's lack of reification.
*
* @param javaProperty the Java property to set
* @param <S> the type of the new column (will be the same as T)
* @return a new column instance with the specified type handler
*/
public <S> SqlColumn<S> withJavaProperty(String javaProperty) {
return cast(copyBuilder().withJavaProperty(javaProperty).build());
}
/**
* Create a new Builder, then populate all attributes in the builder with current values.
*
* <p>This method is used to create copies of the class during normal operations (e.g. when calling the
* {@link SqlColumn#as(String)} method). Any subclass of {@code SqlColumn} MUST override this method.
*
* @return a new Builder instance with all current values populated
*/
protected AbstractBuilder<T, ?> copyBuilder() {
return populateBaseBuilder(new Builder<>());
}
@SuppressWarnings("unchecked")
protected <S extends SqlColumn<?>> S cast(SqlColumn<?> column) {
return (S) column;
}
/**
* This method will add all current attributes to the specified builder. It is useful when creating
* new class instances that only change one attribute - we set all current attributes, then
* change the one attribute. This utility can be used with the with* methods and other methods that
* create new instances.
*
* @param <B> the concrete builder type
* @return the populated builder
*/
protected <B extends AbstractBuilder<T, B>> B populateBaseBuilder(B builder) {
return builder
.withName(this.name)
.withTable(this.table)
.withJdbcType(this.jdbcType)
.withDescendingPhrase(this.descendingPhrase)
.withAlias(this.alias)
.withTypeHandler(this.typeHandler)
.withRenderingStrategy(this.renderingStrategy)
.withParameterTypeConverter(this.parameterTypeConverter)
.withTableQualifier(this.tableQualifier)
.withJavaType(this.javaType)
.withJavaProperty(this.javaProperty);
}
public static <T> SqlColumn<T> of(String name, SqlTable table) {
return new Builder<T>().withName(name)
.withTable(table)
.build();
}
public static <T> SqlColumn<T> of(String name, SqlTable table, JDBCType jdbcType) {
return new Builder<T>().withName(name)
.withTable(table)
.withJdbcType(jdbcType)
.build();
}
public abstract static class AbstractBuilder<T, B extends AbstractBuilder<T, B>> {
protected @Nullable String name;
protected @Nullable SqlTable table;
protected @Nullable JDBCType jdbcType;
protected String descendingPhrase = ""; //$NON-NLS-1$
protected @Nullable String alias;
protected @Nullable String typeHandler;
protected @Nullable RenderingStrategy renderingStrategy;
protected ParameterTypeConverter<T, ?> parameterTypeConverter = v -> v;
protected @Nullable String tableQualifier;
protected @Nullable Class<T> javaType;
protected @Nullable String javaProperty;
public B withName(String name) {
this.name = name;
return getThis();
}
public B withTable(SqlTable table) {
this.table = table;
return getThis();
}
public B withJdbcType(@Nullable JDBCType jdbcType) {
this.jdbcType = jdbcType;
return getThis();
}
public B withDescendingPhrase(String descendingPhrase) {
this.descendingPhrase = descendingPhrase;
return getThis();
}
public B withAlias(@Nullable String alias) {
this.alias = alias;
return getThis();
}
public B withTypeHandler(@Nullable String typeHandler) {
this.typeHandler = typeHandler;
return getThis();
}
public B withRenderingStrategy(@Nullable RenderingStrategy renderingStrategy) {
this.renderingStrategy = renderingStrategy;
return getThis();
}
public B withParameterTypeConverter(ParameterTypeConverter<T, ?> parameterTypeConverter) {
this.parameterTypeConverter = parameterTypeConverter;
return getThis();
}
public B withTableQualifier(@Nullable String tableQualifier) {
this.tableQualifier = tableQualifier;
return getThis();
}
public B withJavaType(@Nullable Class<T> javaType) {
this.javaType = javaType;
return getThis();
}
public B withJavaProperty(@Nullable String javaProperty) {
this.javaProperty = javaProperty;
return getThis();
}
protected abstract B getThis();
public abstract SqlColumn<T> build();
}
public static class Builder<T> extends AbstractBuilder<T, Builder<T>> {
@Override
public SqlColumn<T> build() {
return new SqlColumn<>(this);
}
@Override
protected Builder<T> getThis() {
return this;
}
}
}