Skip to content

Commit b0d582b

Browse files
committed
fixes mybatis#721 Allow specifying constructor arg by its parameter name.
1 parent 5163708 commit b0d582b

File tree

15 files changed

+668
-5
lines changed

15 files changed

+668
-5
lines changed

src/main/java/org/apache/ibatis/annotations/Arg.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2016 the original author or authors.
2+
* Copyright 2009-2017 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.
@@ -44,4 +44,6 @@
4444
String select() default "";
4545

4646
String resultMap() default "";
47+
48+
String name() default "";
4749
}

src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMa
592592
(arg.typeHandler() == UnknownTypeHandler.class ? null : arg.typeHandler());
593593
ResultMapping resultMapping = assistant.buildResultMapping(
594594
resultType,
595-
null,
595+
nullOrEmpty(arg.name()),
596596
nullOrEmpty(arg.column()),
597597
arg.javaType() == void.class ? null : arg.javaType(),
598598
arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(),

src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2016 the original author or authors.
2+
* Copyright 2009-2017 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.
@@ -358,7 +358,12 @@ private boolean databaseIdMatchesCurrent(String id, String databaseId, String re
358358
}
359359

360360
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
361-
String property = context.getStringAttribute("property");
361+
String property;
362+
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
363+
property = context.getStringAttribute("name");
364+
} else {
365+
property = context.getStringAttribute("property");
366+
}
362367
String column = context.getStringAttribute("column");
363368
String javaType = context.getStringAttribute("javaType");
364369
String jdbcType = context.getStringAttribute("jdbcType");

src/main/java/org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8" ?>
22
<!--
33
4-
Copyright 2009-2016 the original author or authors.
4+
Copyright 2009-2017 the original author or authors.
55
66
Licensed under the Apache License, Version 2.0 (the "License");
77
you may not use this file except in compliance with the License.
@@ -89,6 +89,7 @@ jdbcType CDATA #IMPLIED
8989
typeHandler CDATA #IMPLIED
9090
select CDATA #IMPLIED
9191
resultMap CDATA #IMPLIED
92+
name CDATA #IMPLIED
9293
>
9394

9495
<!ELEMENT arg EMPTY>
@@ -99,6 +100,7 @@ jdbcType CDATA #IMPLIED
99100
typeHandler CDATA #IMPLIED
100101
select CDATA #IMPLIED
101102
resultMap CDATA #IMPLIED
103+
name CDATA #IMPLIED
102104
>
103105

104106
<!ELEMENT collection (constructor?,id*,result*,association*,collection*, discriminator?)>

src/main/java/org/apache/ibatis/mapping/ResultMap.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,30 @@
1515
*/
1616
package org.apache.ibatis.mapping;
1717

18+
import java.lang.annotation.Annotation;
19+
import java.lang.reflect.Constructor;
1820
import java.util.ArrayList;
1921
import java.util.Collections;
22+
import java.util.Comparator;
2023
import java.util.HashSet;
2124
import java.util.List;
2225
import java.util.Locale;
2326
import java.util.Set;
2427

28+
import org.apache.ibatis.annotations.Param;
29+
import org.apache.ibatis.builder.BuilderException;
30+
import org.apache.ibatis.logging.Log;
31+
import org.apache.ibatis.logging.LogFactory;
32+
import org.apache.ibatis.reflection.Jdk;
33+
import org.apache.ibatis.reflection.ParamNameUtil;
2534
import org.apache.ibatis.session.Configuration;
2635

2736
/**
2837
* @author Clinton Begin
2938
*/
3039
public class ResultMap {
40+
private Configuration configuration;
41+
3142
private String id;
3243
private Class<?> type;
3344
private List<ResultMapping> resultMappings;
@@ -45,13 +56,16 @@ private ResultMap() {
4556
}
4657

4758
public static class Builder {
59+
private static final Log log = LogFactory.getLog(Builder.class);
60+
4861
private ResultMap resultMap = new ResultMap();
4962

5063
public Builder(Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings) {
5164
this(configuration, id, type, resultMappings, null);
5265
}
5366

5467
public Builder(Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings, Boolean autoMapping) {
68+
resultMap.configuration = configuration;
5569
resultMap.id = id;
5670
resultMap.type = type;
5771
resultMap.resultMappings = resultMappings;
@@ -76,6 +90,7 @@ public ResultMap build() {
7690
resultMap.idResultMappings = new ArrayList<ResultMapping>();
7791
resultMap.constructorResultMappings = new ArrayList<ResultMapping>();
7892
resultMap.propertyResultMappings = new ArrayList<ResultMapping>();
93+
final List<String> constructorArgNames = new ArrayList<String>();
7994
for (ResultMapping resultMapping : resultMap.resultMappings) {
8095
resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
8196
resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
@@ -96,6 +111,9 @@ public ResultMap build() {
96111
}
97112
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
98113
resultMap.constructorResultMappings.add(resultMapping);
114+
if (resultMapping.getProperty() != null) {
115+
constructorArgNames.add(resultMapping.getProperty());
116+
}
99117
} else {
100118
resultMap.propertyResultMappings.add(resultMapping);
101119
}
@@ -106,6 +124,13 @@ public ResultMap build() {
106124
if (resultMap.idResultMappings.isEmpty()) {
107125
resultMap.idResultMappings.addAll(resultMap.resultMappings);
108126
}
127+
if (!constructorArgNames.isEmpty()) {
128+
if (!sortConstructorResultMapping(constructorArgNames)) {
129+
throw new BuilderException("Failed to find a constructor in '"
130+
+ resultMap.getType().getName() + "' by arg names " + constructorArgNames
131+
+ ". There might be more info in debug log.");
132+
}
133+
}
109134
// lock down collections
110135
resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
111136
resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
@@ -114,6 +139,72 @@ public ResultMap build() {
114139
resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
115140
return resultMap;
116141
}
142+
143+
private boolean sortConstructorResultMapping(final List<String> constructorArgNames) {
144+
Constructor<?>[] constructors = resultMap.type.getDeclaredConstructors();
145+
// Search constructors by arg names and types.
146+
for (Constructor<?> constructor : constructors) {
147+
Class<?>[] paramTypes = constructor.getParameterTypes();
148+
if (constructorArgNames.size() == paramTypes.length) {
149+
final List<String> paramNames = getArgNames(constructor);
150+
if (constructorArgNames.containsAll(paramNames)) {
151+
if (!argTypesMatch(constructorArgNames, paramTypes, paramNames)) {
152+
continue;
153+
}
154+
// Found a matching constructor.
155+
Collections.sort(resultMap.constructorResultMappings, new Comparator<ResultMapping>() {
156+
@Override
157+
public int compare(ResultMapping o1, ResultMapping o2) {
158+
int paramIdx1 = paramNames.indexOf(o1.getProperty());
159+
int paramIdx2 = paramNames.indexOf(o2.getProperty());
160+
return paramIdx1 - paramIdx2;
161+
}
162+
});
163+
return true;
164+
}
165+
}
166+
}
167+
return false;
168+
}
169+
170+
private boolean argTypesMatch(final List<String> constructorArgNames,
171+
Class<?>[] paramTypes, List<String> paramNames) {
172+
for (int i = 0; i < constructorArgNames.size(); i++) {
173+
Class<?> actualType = paramTypes[paramNames.indexOf(constructorArgNames.get(i))];
174+
Class<?> specifiedType = resultMap.constructorResultMappings.get(i).getJavaType();
175+
if (!actualType.equals(specifiedType)) {
176+
if (log.isDebugEnabled()) {
177+
log.debug("Found a constructor with arg names " + constructorArgNames
178+
+ ", but the type of '" + constructorArgNames.get(i)
179+
+ "' did not match. Specified: [" + specifiedType.getName() + "] Declared: ["
180+
+ actualType.getName() + "]");
181+
}
182+
return false;
183+
}
184+
}
185+
return true;
186+
}
187+
188+
private List<String> getArgNames(Constructor<?> constructor) {
189+
if (resultMap.configuration.isUseActualParamName() && Jdk.parameterExists) {
190+
return ParamNameUtil.getParamNames(constructor);
191+
} else {
192+
List<String> paramNames = new ArrayList<String>();
193+
final Annotation[][] paramAnnotations = constructor.getParameterAnnotations();
194+
int paramCount = paramAnnotations.length;
195+
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
196+
String name = null;
197+
for (Annotation annotation : paramAnnotations[paramIndex]) {
198+
if (annotation instanceof Param) {
199+
name = ((Param) annotation).value();
200+
break;
201+
}
202+
}
203+
paramNames.add(name != null ? name : "arg" + paramIndex);
204+
}
205+
return paramNames;
206+
}
207+
}
117208
}
118209

119210
public String getId() {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
--
2+
-- Copyright 2009-2017 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+
17+
drop table users if exists;
18+
19+
create table users (
20+
id int,
21+
name varchar(20),
22+
team int
23+
);
24+
25+
insert into users (id, name, team) values
26+
(1, 'User1', 99);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Copyright 2009-2017 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.apache.ibatis.submitted.named_constructor_args;
17+
18+
import static org.hamcrest.CoreMatchers.*;
19+
20+
import java.io.Reader;
21+
import java.sql.Connection;
22+
23+
import org.apache.ibatis.annotations.Arg;
24+
import org.apache.ibatis.annotations.ConstructorArgs;
25+
import org.apache.ibatis.annotations.Select;
26+
import org.apache.ibatis.builder.BuilderException;
27+
import org.apache.ibatis.io.Resources;
28+
import org.apache.ibatis.jdbc.ScriptRunner;
29+
import org.apache.ibatis.session.Configuration;
30+
import org.apache.ibatis.session.SqlSession;
31+
import org.apache.ibatis.session.SqlSessionFactory;
32+
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
33+
import org.junit.BeforeClass;
34+
import org.junit.Rule;
35+
import org.junit.Test;
36+
import org.junit.rules.ExpectedException;
37+
38+
public class InvalidNamedConstructorArgsTest {
39+
@Rule
40+
public ExpectedException ex = ExpectedException.none();
41+
42+
private static SqlSessionFactory sqlSessionFactory;
43+
44+
@BeforeClass
45+
public static void setUp() throws Exception {
46+
// create an SqlSessionFactory
47+
Reader reader = Resources.getResourceAsReader(
48+
"org/apache/ibatis/submitted/named_constructor_args/mybatis-config.xml");
49+
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
50+
reader.close();
51+
52+
// populate in-memory database
53+
SqlSession session = sqlSessionFactory.openSession();
54+
Connection conn = session.getConnection();
55+
reader = Resources
56+
.getResourceAsReader("org/apache/ibatis/submitted/named_constructor_args/CreateDB.sql");
57+
ScriptRunner runner = new ScriptRunner(conn);
58+
runner.setLogWriter(null);
59+
runner.runScript(reader);
60+
reader.close();
61+
session.close();
62+
}
63+
64+
interface NoMatchingConstructorMapper {
65+
@ConstructorArgs({
66+
@Arg(column = "id", name = "noSuchConstructorArg"),
67+
})
68+
@Select("select * from users ")
69+
User select();
70+
}
71+
72+
@Test
73+
public void noMatchingConstructorArgName() {
74+
ex.expect(BuilderException.class);
75+
ex.expectMessage(startsWith("Failed to find a constructor in "
76+
+ "'org.apache.ibatis.submitted.named_constructor_args.User' by arg names [noSuchConstructorArg]."));
77+
78+
Configuration configuration = sqlSessionFactory.getConfiguration();
79+
configuration.addMapper(NoMatchingConstructorMapper.class);
80+
}
81+
82+
interface ConstructorWithWrongJavaType {
83+
// There is a constructor with arg name 'id', but
84+
// its type is different from the specified javaType.
85+
@ConstructorArgs({
86+
@Arg(column = "id", name = "id", javaType = Integer.class),
87+
})
88+
@Select("select * from users ")
89+
User select();
90+
}
91+
92+
@Test
93+
public void wrongJavaType() {
94+
ex.expect(BuilderException.class);
95+
ex.expectMessage(startsWith("Failed to find a constructor in "
96+
+ "'org.apache.ibatis.submitted.named_constructor_args.User' by arg names [id]."));
97+
98+
Configuration configuration = sqlSessionFactory.getConfiguration();
99+
configuration.addMapper(ConstructorWithWrongJavaType.class);
100+
}
101+
102+
interface ConstructorMissingRequiresJavaType {
103+
// There is a constructor with arg name 'id', but its type
104+
// is different from the type of a property with the same name.
105+
// javaType is required in this case.
106+
// Debug log shows the detail of the matching error.
107+
@ConstructorArgs({
108+
@Arg(column = "id", name = "id"),
109+
})
110+
@Select("select * from users ")
111+
User select();
112+
}
113+
114+
@Test
115+
public void missingRequiredJavaType() {
116+
ex.expect(BuilderException.class);
117+
ex.expectMessage(startsWith("Failed to find a constructor in "
118+
+ "'org.apache.ibatis.submitted.named_constructor_args.User' by arg names [id]."));
119+
120+
Configuration configuration = sqlSessionFactory.getConfiguration();
121+
configuration.addMapper(ConstructorMissingRequiresJavaType.class);
122+
}
123+
}

0 commit comments

Comments
 (0)