Skip to content

Commit d4099b7

Browse files
committed
Support java.util.Optional as return type of mapper method
1 parent 17e9687 commit d4099b7

File tree

8 files changed

+354
-5
lines changed

8 files changed

+354
-5
lines changed

src/main/java/org/apache/ibatis/binding/MapperMethod.java

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.apache.ibatis.annotations.Flush;
1919
import org.apache.ibatis.annotations.MapKey;
2020
import org.apache.ibatis.cursor.Cursor;
21+
import org.apache.ibatis.io.Resources;
2122
import org.apache.ibatis.mapping.MappedStatement;
2223
import org.apache.ibatis.mapping.SqlCommandType;
2324
import org.apache.ibatis.reflection.MetaObject;
@@ -28,19 +29,29 @@
2829
import org.apache.ibatis.session.RowBounds;
2930
import org.apache.ibatis.session.SqlSession;
3031

31-
import java.lang.reflect.Array;
32-
import java.lang.reflect.Method;
33-
import java.lang.reflect.ParameterizedType;
34-
import java.lang.reflect.Type;
32+
import java.lang.reflect.*;
3533
import java.util.*;
3634

3735
/**
3836
* @author Clinton Begin
3937
* @author Eduardo Macarron
4038
* @author Lasse Voss
39+
* @author Kazuki Shimizu
4140
*/
4241
public class MapperMethod {
4342

43+
private static Method optionalFactoryMethod = null;
44+
45+
static {
46+
try {
47+
optionalFactoryMethod = Resources.classForName("java.util.Optional").getMethod("ofNullable", Object.class);
48+
} catch (ClassNotFoundException e) {
49+
// Ignore
50+
} catch (NoSuchMethodException e) {
51+
// Ignore
52+
}
53+
}
54+
4455
private final SqlCommand command;
4556
private final MethodSignature method;
4657

@@ -53,7 +64,7 @@ public Object execute(SqlSession sqlSession, Object[] args) {
5364
Object result;
5465
switch (command.getType()) {
5566
case INSERT: {
56-
Object param = method.convertArgsToSqlCommandParam(args);
67+
Object param = method.convertArgsToSqlCommandParam(args);
5768
result = rowCountResult(sqlSession.insert(command.getName(), param));
5869
break;
5970
}
@@ -80,6 +91,10 @@ public Object execute(SqlSession sqlSession, Object[] args) {
8091
} else {
8192
Object param = method.convertArgsToSqlCommandParam(args);
8293
result = sqlSession.selectOne(command.getName(), param);
94+
if (method.returnsOptional() &&
95+
(result == null || !method.getReturnType().equals(result.getClass()))) {
96+
result = wrapWithOptional(result);
97+
}
8398
}
8499
break;
85100
case FLUSH:
@@ -95,6 +110,20 @@ public Object execute(SqlSession sqlSession, Object[] args) {
95110
return result;
96111
}
97112

113+
private Object wrapWithOptional(Object result) {
114+
if (optionalFactoryMethod == null) {
115+
throw new BindingException("Can't use the java.util.Optional");
116+
}
117+
try {
118+
return optionalFactoryMethod.invoke(null, result);
119+
} catch (IllegalAccessException e) {
120+
throw new BindingException("Can't create a java.util.Optional instance.", e);
121+
} catch (InvocationTargetException e) {
122+
throw new BindingException("Can't create a java.util.Optional instance.", e);
123+
}
124+
}
125+
126+
98127
private Object rowCountResult(int rowCount) {
99128
final Object result;
100129
if (method.returnsVoid()) {
@@ -246,6 +275,7 @@ public static class MethodSignature {
246275
private final boolean returnsMap;
247276
private final boolean returnsVoid;
248277
private final boolean returnsCursor;
278+
private final boolean returnsOptional;
249279
private final Class<?> returnType;
250280
private final String mapKey;
251281
private final Integer resultHandlerIndex;
@@ -264,6 +294,7 @@ public MethodSignature(Configuration configuration, Class<?> mapperInterface, Me
264294
this.returnsVoid = void.class.equals(this.returnType);
265295
this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
266296
this.returnsCursor = Cursor.class.equals(this.returnType);
297+
this.returnsOptional = "java.util.Optional".equals(returnType.getName());
267298
this.mapKey = getMapKey(method);
268299
this.returnsMap = (this.mapKey != null);
269300
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
@@ -315,6 +346,15 @@ public boolean returnsCursor() {
315346
return returnsCursor;
316347
}
317348

349+
/**
350+
* return whether return type is {@code java.util.Optional}
351+
* @return return {@code true}, if return type is {@code java.util.Optional}
352+
* @since 3.4.2
353+
*/
354+
public boolean returnsOptional() {
355+
return returnsOptional;
356+
}
357+
318358
private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
319359
Integer index = null;
320360
final Class<?>[] argTypes = method.getParameterTypes();

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888

8989
/**
9090
* @author Clinton Begin
91+
* @author Kazuki Shimizu
9192
*/
9293
public class MapperAnnotationBuilder {
9394

@@ -421,6 +422,14 @@ private Class<?> getReturnType(Method method) {
421422
returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
422423
}
423424
}
425+
} else if ("java.util.Optional".equals(rawType.getName())) {
426+
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
427+
if (actualTypeArguments != null && actualTypeArguments.length == 1) {
428+
Type returnTypeParameter = actualTypeArguments[0];
429+
if (returnTypeParameter instanceof Class<?>) {
430+
returnType = (Class<?>) returnTypeParameter;
431+
}
432+
}
424433
}
425434
}
426435

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--
2+
-- Copyright 2009-2016 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+
);
23+
24+
insert into users (id, name) values
25+
(1, 'User1'), (2, 'User2');
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright 2009-2016 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.usesjava8.optional_on_mapper_method;
17+
18+
import org.apache.ibatis.annotations.Select;
19+
20+
import java.util.List;
21+
import java.util.Optional;
22+
23+
public interface Mapper {
24+
25+
@Select("select * from users where id = #{id}")
26+
Optional<User> getUserUsingAnnotation(Integer id);
27+
28+
Optional<User> getUserUsingXml(Integer id);
29+
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Copyright 2009-2016 the original author or authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
18+
-->
19+
<!DOCTYPE mapper
20+
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
21+
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
22+
23+
<mapper namespace="org.apache.ibatis.submitted.usesjava8.optional_on_mapper_method.Mapper">
24+
25+
<select id="getUserUsingXml" resultType="org.apache.ibatis.submitted.usesjava8.optional_on_mapper_method.User">
26+
select * from users where id = #{id}
27+
</select>
28+
29+
</mapper>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Copyright 2009-2016 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.usesjava8.optional_on_mapper_method;
17+
18+
import org.apache.ibatis.io.Resources;
19+
import org.apache.ibatis.jdbc.ScriptRunner;
20+
import org.apache.ibatis.session.SqlSession;
21+
import org.apache.ibatis.session.SqlSessionFactory;
22+
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
23+
import org.junit.BeforeClass;
24+
import org.junit.Test;
25+
import org.mockito.Mockito;
26+
27+
import java.io.Reader;
28+
import java.sql.Connection;
29+
import java.util.Optional;
30+
31+
import static org.hamcrest.core.Is.is;
32+
import static org.junit.Assert.assertFalse;
33+
import static org.junit.Assert.assertThat;
34+
import static org.junit.Assert.assertTrue;
35+
36+
import static org.mockito.Mockito.*;
37+
38+
/**
39+
* Tests for support the {@code java.util.Optional} as return type of mapper method.
40+
*
41+
* @since 3.4.2
42+
* @author Kazuki Shimizu
43+
*/
44+
public class OptionalOnMapperMethodTest {
45+
46+
private static SqlSessionFactory sqlSessionFactory;
47+
48+
@BeforeClass
49+
public static void setUp() throws Exception {
50+
// create an SqlSessionFactory
51+
Reader reader = Resources.getResourceAsReader(
52+
"org/apache/ibatis/submitted/usesjava8/optional_on_mapper_method/mybatis-config.xml");
53+
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
54+
reader.close();
55+
56+
// populate in-memory database
57+
SqlSession session = sqlSessionFactory.openSession();
58+
Connection conn = session.getConnection();
59+
reader = Resources.getResourceAsReader(
60+
"org/apache/ibatis/submitted/usesjava8/optional_on_mapper_method/CreateDB.sql");
61+
ScriptRunner runner = new ScriptRunner(conn);
62+
runner.setLogWriter(null);
63+
runner.runScript(reader);
64+
reader.close();
65+
session.close();
66+
}
67+
68+
@Test
69+
public void returnNotNullOnAnnotation() {
70+
SqlSession sqlSession = sqlSessionFactory.openSession();
71+
try {
72+
Mapper mapper = sqlSession.getMapper(Mapper.class);
73+
Optional<User> user = mapper.getUserUsingAnnotation(1);
74+
assertTrue(user.isPresent());
75+
assertThat(user.get().getName(), is("User1"));
76+
} finally {
77+
sqlSession.close();
78+
}
79+
}
80+
81+
@Test
82+
public void returnNullOnAnnotation() {
83+
SqlSession sqlSession = sqlSessionFactory.openSession();
84+
try {
85+
Mapper mapper = sqlSession.getMapper(Mapper.class);
86+
Optional<User> user = mapper.getUserUsingAnnotation(3);
87+
assertFalse(user.isPresent());
88+
} finally {
89+
sqlSession.close();
90+
}
91+
}
92+
93+
@Test
94+
public void returnNotNullOnXml() {
95+
SqlSession sqlSession = sqlSessionFactory.openSession();
96+
try {
97+
Mapper mapper = sqlSession.getMapper(Mapper.class);
98+
Optional<User> user = mapper.getUserUsingXml(2);
99+
assertTrue(user.isPresent());
100+
assertThat(user.get().getName(), is("User2"));
101+
} finally {
102+
sqlSession.close();
103+
}
104+
}
105+
106+
@Test
107+
public void returnNullOnXml() {
108+
SqlSession sqlSession = sqlSessionFactory.openSession();
109+
try {
110+
Mapper mapper = sqlSession.getMapper(Mapper.class);
111+
Optional<User> user = mapper.getUserUsingXml(3);
112+
assertFalse(user.isPresent());
113+
} finally {
114+
sqlSession.close();
115+
}
116+
}
117+
118+
@Test
119+
public void returnOptionalFromSqlSession() {
120+
SqlSession sqlSession = Mockito.spy(sqlSessionFactory.openSession());
121+
122+
User mockUser = new User();
123+
mockUser.setName("mock user");
124+
Optional<User> optionalMockUser = Optional.of(mockUser);
125+
doReturn(optionalMockUser).when(sqlSession).selectOne(any(String.class), any(Object.class));
126+
127+
try {
128+
Mapper mapper = sqlSession.getMapper(Mapper.class);
129+
Optional<User> user = mapper.getUserUsingAnnotation(3);
130+
assertTrue(user == optionalMockUser);
131+
} finally {
132+
sqlSession.close();
133+
}
134+
}
135+
136+
}

0 commit comments

Comments
 (0)