<select id="findTicketGrades" parameterType="java.lang.Long"
resultType="com.heeverse.ticket.domain.entity.TicketGrade">
SELECT
ticket_grade_id,
grade,
grade_name,
seat_count,
concert_id,
create_datetime
FROM
ticket_grade
WHERE
concert_id = #{concertId}
</select>
쿼리 결과를 List<TicketGrade>
로 맵핑하려고 했다. 그러나 결과는 리스트에 널이 담겨졌다.
[null, null, null, null, ....]
디버깅할 때 삽질을 하여 애먼 클래스를 뒤지느라 시간을 많이 소요했다.
그러던중 무심코 추가한 옵션이 생각이 났다.
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
arg-name-based-constructor-auto-mapping: true // 이 부분!
이 옵션이 무엇이냐면
argNameBasedConstructorAutoMapping
When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on the column order. (Since 3.5.10)
default : false
쿼리 결과를 객체로 맵핑할때 디폴트 동작 방식은 클래스 생성자의 매개변수 순서를 참고하여 컬럼과 맵핑한다. 즉, 매개변수가 선언된 순서와 동일한 순서의 컬럼과 매핑을 한다.
그래서 생성자 자동 매핑을 적용할 때 칼럼 순서가 아닌 칼럼 이름과 생성자 인수들의 이름을 기반으로 매핑하도록 저 옵션을 활성화했다.
당연히 생성자 파라미터 변수명과 컬럼명을 맵핑할 줄 알았는데 디버깅 결과 예상과 전혀 달랐다.
쿼리 결과인 ResultSet
을 처리하는 DefaultResultSetHandler
을 확인해야한다.
public class DefaultResultSetHandler implements ResultSetHandler
위에서 말한 옵션을 활성화 하면 아래 메서드를 통해 맵핑을 한다.
private boolean applyArgNameBasedConstructorAutoMapping(ResultSetWrapper rsw, ResultMap resultMap,
String columnPrefix, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor,
boolean foundValues) throws SQLException {
List<String> missingArgs = null;
// 원인 코드 시작
Parameter[] params = constructor.getParameters();
for (Parameter param : params) {
boolean columnNotFound = true;
Param paramAnno = param.getAnnotation(Param.class);
String paramName = paramAnno == null ? param.getName() : paramAnno.value();
for (String columnName : rsw.getColumnNames()) {
if (columnMatchesParam(columnName, paramName, columnPrefix)) {
// 원인 코드 끝
Class<?> paramType = param.getType();
TypeHandler<?> typeHandler = rsw.getTypeHandler(paramType, columnName);
Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
constructorArgTypes.add(paramType);
constructorArgs.add(value);
final String mapKey = resultMap.getId() + ":" + columnPrefix;
if (!autoMappingsCache.containsKey(mapKey)) {
MapUtil.computeIfAbsent(constructorAutoMappingColumns, mapKey, k -> new ArrayList<>()).add(columnName);
}
columnNotFound = false;
foundValues = value != null || foundValues;
}
}
if (columnNotFound) {
if (missingArgs == null) {
missingArgs = new ArrayList<>();
}
missingArgs.add(paramName);
}
}
if (foundValues && constructorArgs.size() < params.length) {
throw new ExecutorException(MessageFormat.format(
"Constructor auto-mapping of ''{1}'' failed " + "because ''{0}'' were not found in the result set; "
+ "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''.",
missingArgs, constructor, rsw.getColumnNames(), configuration.isMapUnderscoreToCamelCase()));
}
return foundValues;
}
원인을 찾아낸 코드만 발췌해보겠다.
Parameter[] params = constructor.getParameters();
for (Parameter param : params) {
boolean columnNotFound = true;
Param paramAnno = param.getAnnotation(Param.class);
String paramName = paramAnno == null ? param.getName() : paramAnno.value();
for (String columnName : rsw.getColumnNames()) {
if (columnMatchesParam(columnName, paramName, columnPrefix)) {
paramAnno
가 null 이 아니면 어노테이션 value에 선언한 이름을 가져오고, null 이면 Parameter
객체에서 변수명을 조회한다. 나는 생성자 파라미터에 @Param
을 선언 안했으니 Parameter
객체에 생성자 매개변수 이름이 어떻게 들어있는지 확인해봤다.
Parameter.getName()
메서드 내부 코드를 까보자
/**
* Returns the name of the parameter. If the parameter's name is
* {@linkplain #isNamePresent() present}, then this method returns
* the name provided by the class file. Otherwise, this method
* synthesizes a name of the form argN, where N is the index of
* the parameter in the descriptor of the method which declares
* the parameter.
*
* @return The name of the parameter, either provided by the class
* file or synthesized if the class file does not provide
* a name.
*/
public String getName() {
// Note: empty strings as parameter names are now outlawed.
// The .isEmpty() is for compatibility with current JVM
// behavior. It may be removed at some point.
if(name == null || name.isEmpty())
return "arg" + index;
else
return name;
}
분기문에서 첫번째 if 절을 충족해서 "arg" + index
로 이름이 생성되었다.
그 결과 arg0, arg1 과 같은 변수명과 테이블의 컬럼명을 비교하니 일치하는 이름을 찾지 못해 객체 생성자를 호출하지 못한것이고 그 결과 리스트에 null 만 잔뜩 담긴 것이다.
@Param
어노테이션으로 이름을 명시해줬다.