-
Notifications
You must be signed in to change notification settings - Fork 214
Description
Background:
Currently, SqlColumn in MyBatis Dynamic SQL has a private constructor, which makes subclassing difficult.
Allowing it to be protected could provide flexibility for developers to build the following — this is an extension I want to implement:
MappedColumn<E, T> — type-safe, entity-bound column storing metadata such as primary key.
MappedTable<T, E> — table object exposing all columns and primary key columns.
BaseMapper<T, E> — generic CRUD mapper that can reuse most logic automatically, reducing boilerplate.
This approach could make mapper code cleaner and more maintainable, especially for record-style entities or tables with many columns.
public class MappedColumn<E, T> extends SqlColumn<T> {
private final boolean primaryKey;
private final String javaProperty;
private final Function<E, T> propertyGetter;
private MappedColumn(String name, JDBCType jdbcType, Function<E, T> getter, boolean primaryKey, String javaProperty) {
super(name, jdbcType);
this.primaryKey = primaryKey;
this.propertyGetter = getter;
this.javaProperty = javaProperty;
}
/** Default: primaryKey = false, javaProperty inferred from method reference */
public static <E, T> MappedColumn<E, T> of(String name, JDBCType jdbcType, Function<E, T> getter) {
return of(name, jdbcType, getter, false);
}
/** Full constructor: primaryKey specified */
public static <E, T> MappedColumn<E, T> of(String name, JDBCType jdbcType, Function<E, T> getter, boolean primaryKey) {
String propName = resolvePropertyName(getter);
return new MappedColumn<>(name, jdbcType, getter, primaryKey, propName);
}
public boolean isPrimaryKey() { return primaryKey; }
public T valueFrom(E entity) { return propertyGetter.apply(entity); }
public String javaProperty() { return javaProperty; }
private static <T> String resolvePropertyName(Function<T, ?> getter) {
if (!(getter instanceof Serializable s)) {
throw new IllegalArgumentException("Getter must be Serializable to extract property name");
}
try {
Method writeReplace = s.getClass().getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true);
SerializedLambda lambda = (SerializedLambda) writeReplace.invoke(s);
String implMethod = lambda.getImplMethodName();
if (implMethod.startsWith("get") && implMethod.length() > 3) {
return Character.toLowerCase(implMethod.charAt(3)) + implMethod.substring(4);
} else {
return implMethod;
}
} catch (Exception e) {
throw new RuntimeException("Failed to resolve property name from method reference", e);
}
}
}
Notes / Rationale:
propertyGetter is a method reference to the JavaBean property.
If the entity is a classic JavaBean with getters, we may need to perform simple handling (e.g., via SerializedLambda) to retrieve the method name.
If the entity is a record, obtaining the property name is easier.
This allows MappedColumn to tie directly to the entity field, making BaseMapper operations (insert, update, select) much more reusable and type-safe.
public abstract class MappedTable<T extends MappedTable<T, E>, E> extends AliasableSqlTable<T> {
protected MappedTable(String tableName, Supplier<T> constructor) {
super(tableName, constructor);
}
/** All columns of the table */
public abstract MappedColumn<E, ?>[] columns();
/** Only primary key columns */
public MappedColumn<E, ?>[] primaryKeyColumns() {
return Arrays.stream(columns())
.filter(MappedColumn::isPrimaryKey)
.toArray(MappedColumn[]::new);
}
}
public interface BaseMapper<T extends MappedTable<T, R>, R>
extends
CommonCountMapper,
CommonDeleteMapper,
CommonInsertMapper<R>,
CommonUpdateMapper {
T table();
@SelectProvider(type = SqlProviderAdapter.class, method = "select")
List<T> selectMany(SelectStatementProvider selectStatement);
@SelectProvider(type = SqlProviderAdapter.class, method = "select")
Optional<T> selectOne(SelectStatementProvider selectStatement);
default long count(CountDSLCompleter completer) {
return MyBatis3Utils.countFrom(this::count, table(), completer);
}
default int delete(DeleteDSLCompleter completer) {
return MyBatis3Utils.deleteFrom(this::delete, table(), completer);
}
default int insert(R row) {
return MyBatis3Utils.insert(this::insert, row, table());
}
default int insertMultiple(Collection<R> records) {
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, table());
}
default int insertSelective(R row) {
return MyBatis3Utils.insertSelective(this::insert, row, table());
}
default Optional<R> selectOne(SelectDSLCompleter completer) {
return MyBatis3Utils.selectOne(this::selectOne, table(), completer);
}
default List<R> select(SelectDSLCompleter completer) {
return MyBatis3Utils.selectList(this::selectMany, table(), completer);
}
default List<R> selectDistinct(SelectDSLCompleter completer) {
return MyBatis3Utils.selectDistinct(this::selectMany, table(), completer);
}
default int update(UpdateDSLCompleter completer) {
return MyBatis3Utils.update(this::update, table(), completer);
}
default int updateByPrimaryKey(R row) {
return MyBatis3Utils.updateByPrimaryKey(this::update, row, table());
}
default int updateByPrimaryKeySelective(Category row) {
return MyBatis3Utils.updateByPrimaryKeySelective(this::update, row, table());
}
}
With this design:
Mapper implementations no longer need to provide JavaBean getter method references for most operations.
By providing a table() object that exposes all columns and primary key columns, the BaseMapper can reuse most logic.
Remaining challenge: primary key operations
Methods such as selectByPrimaryKey and deleteByPrimaryKey are not fully solved for composite primary keys:
Single-primary-key tables are straightforward (table().primaryKeyColumns()[0]).
Composite-primary-key tables require further design for type-safe reuse.
Possible approaches could involve using a record/DTO as a PK, or overloading BaseMapper with a generic PK type.
Request
Consider making SqlColumn extensible by exposing a protected constructor.
Provide a more elegant way to build MappedColumn / MappedTable / BaseMapper abstractions for reusable, type-safe CRUD logic, including support for composite primary key operations.