diff --git a/src/main/java/org/apache/ibatis/annotations/Arg.java b/src/main/java/org/apache/ibatis/annotations/Arg.java index 66e037698bd..3aadcca8930 100644 --- a/src/main/java/org/apache/ibatis/annotations/Arg.java +++ b/src/main/java/org/apache/ibatis/annotations/Arg.java @@ -16,6 +16,8 @@ package org.apache.ibatis.annotations; import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -32,7 +34,8 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target({}) +@Target(ElementType.METHOD) +@Repeatable(ConstructorArgs.class) public @interface Arg { /** diff --git a/src/main/java/org/apache/ibatis/annotations/Result.java b/src/main/java/org/apache/ibatis/annotations/Result.java index f2096ccdd14..86db4a8d526 100644 --- a/src/main/java/org/apache/ibatis/annotations/Result.java +++ b/src/main/java/org/apache/ibatis/annotations/Result.java @@ -16,6 +16,8 @@ package org.apache.ibatis.annotations; import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -32,7 +34,8 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target({}) +@Target(ElementType.METHOD) +@Repeatable(Results.class) public @interface Result { /** * Returns whether id column or not. diff --git a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java index 72051b8b7e0..20b14e69557 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java @@ -39,7 +39,6 @@ import org.apache.ibatis.annotations.CacheNamespace; import org.apache.ibatis.annotations.CacheNamespaceRef; import org.apache.ibatis.annotations.Case; -import org.apache.ibatis.annotations.ConstructorArgs; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.DeleteProvider; import org.apache.ibatis.annotations.Insert; @@ -228,11 +227,11 @@ private void parseCacheRef() { private String parseResultMap(Method method) { Class returnType = getReturnType(method); - ConstructorArgs args = method.getAnnotation(ConstructorArgs.class); - Results results = method.getAnnotation(Results.class); + Arg[] args = method.getAnnotationsByType(Arg.class); + Result[] results = method.getAnnotationsByType(Result.class); TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class); String resultMapId = generateResultMapName(method); - applyResultMap(resultMapId, returnType, argsIf(args), resultsIf(results), typeDiscriminator); + applyResultMap(resultMapId, returnType, args, results, typeDiscriminator); return resultMapId; } @@ -626,14 +625,6 @@ private String nullOrEmpty(String value) { return value == null || value.trim().length() == 0 ? null : value; } - private Result[] resultsIf(Results results) { - return results == null ? new Result[0] : results.value(); - } - - private Arg[] argsIf(ConstructorArgs args) { - return args == null ? new Arg[0] : args.value(); - } - private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId, Class parameterTypeClass, LanguageDriver languageDriver) { String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX; Class resultTypeClass = selectKeyAnnotation.resultType(); diff --git a/src/test/java/org/apache/ibatis/binding/BindingTest.java b/src/test/java/org/apache/ibatis/binding/BindingTest.java index 6edf63fe12f..b1dfb91dcaf 100644 --- a/src/test/java/org/apache/ibatis/binding/BindingTest.java +++ b/src/test/java/org/apache/ibatis/binding/BindingTest.java @@ -36,6 +36,7 @@ import net.sf.cglib.proxy.Factory; import org.apache.ibatis.BaseDataTest; +import org.apache.ibatis.builder.BuilderException; import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.domain.blog.Author; import org.apache.ibatis.domain.blog.Blog; @@ -688,4 +689,94 @@ void registeredMappers() { assertTrue(mapperClasses.contains(BoundAuthorMapper.class)); } + @Test + void shouldMapPropertiesUsingRepeatableAnnotation() { + try (SqlSession session = sqlSessionFactory.openSession()) { + BoundAuthorMapper mapper = session.getMapper(BoundAuthorMapper.class); + Author author = new Author(-1, "cbegin", "******", "cbegin@nowhere.com", "N/A", Section.NEWS); + mapper.insertAuthor(author); + Author author2 = mapper.selectAuthorMapToPropertiesUsingRepeatable(author.getId()); + assertNotNull(author2); + assertEquals(author.getId(), author2.getId()); + assertEquals(author.getUsername(), author2.getUsername()); + assertEquals(author.getPassword(), author2.getPassword()); + assertEquals(author.getBio(), author2.getBio()); + assertEquals(author.getEmail(), author2.getEmail()); + session.rollback(); + } + } + + @Test + void shouldMapConstructorUsingRepeatableAnnotation() { + try (SqlSession session = sqlSessionFactory.openSession()) { + BoundAuthorMapper mapper = session.getMapper(BoundAuthorMapper.class); + Author author = new Author(-1, "cbegin", "******", "cbegin@nowhere.com", "N/A", Section.NEWS); + mapper.insertAuthor(author); + Author author2 = mapper.selectAuthorMapToConstructorUsingRepeatable(author.getId()); + assertNotNull(author2); + assertEquals(author.getId(), author2.getId()); + assertEquals(author.getUsername(), author2.getUsername()); + assertEquals(author.getPassword(), author2.getPassword()); + assertEquals(author.getBio(), author2.getBio()); + assertEquals(author.getEmail(), author2.getEmail()); + assertEquals(author.getFavouriteSection(), author2.getFavouriteSection()); + session.rollback(); + } + } + + @Test + void shouldMapUsingSingleRepeatableAnnotation() { + try (SqlSession session = sqlSessionFactory.openSession()) { + BoundAuthorMapper mapper = session.getMapper(BoundAuthorMapper.class); + Author author = new Author(-1, "cbegin", "******", "cbegin@nowhere.com", "N/A", Section.NEWS); + mapper.insertAuthor(author); + Author author2 = mapper.selectAuthorUsingSingleRepeatable(author.getId()); + assertNotNull(author2); + assertEquals(author.getId(), author2.getId()); + assertEquals(author.getUsername(), author2.getUsername()); + assertNull(author2.getPassword()); + assertNull(author2.getBio()); + assertNull(author2.getEmail()); + assertNull(author2.getFavouriteSection()); + session.rollback(); + } + } + + @Test + void shouldMapWhenSpecifyBothArgAndConstructorArgs() { + try (SqlSession session = sqlSessionFactory.openSession()) { + BoundAuthorMapper mapper = session.getMapper(BoundAuthorMapper.class); + Author author = new Author(-1, "cbegin", "******", "cbegin@nowhere.com", "N/A", Section.NEWS); + mapper.insertAuthor(author); + Author author2 = mapper.selectAuthorUsingBothArgAndConstructorArgs(author.getId()); + assertNotNull(author2); + assertEquals(author.getId(), author2.getId()); + assertEquals(author.getUsername(), author2.getUsername()); + assertEquals(author.getPassword(), author2.getPassword()); + assertEquals(author.getBio(), author2.getBio()); + assertEquals(author.getEmail(), author2.getEmail()); + assertEquals(author.getFavouriteSection(), author2.getFavouriteSection()); + session.rollback(); + } + } + + @Test + void shouldMapWhenSpecifyBothResultAndResults() { + try (SqlSession session = sqlSessionFactory.openSession()) { + BoundAuthorMapper mapper = session.getMapper(BoundAuthorMapper.class); + Author author = new Author(-1, "cbegin", "******", "cbegin@nowhere.com", "N/A", Section.NEWS); + mapper.insertAuthor(author); + Author author2 = mapper.selectAuthorUsingBothResultAndResults(author.getId()); + assertNotNull(author2); + assertEquals(author.getId(), author2.getId()); + assertEquals(author.getUsername(), author2.getUsername()); + assertNull(author2.getPassword()); + assertNull(author2.getBio()); + assertNull(author2.getEmail()); + assertNull(author2.getFavouriteSection()); + session.rollback(); + } + } + } + diff --git a/src/test/java/org/apache/ibatis/binding/BoundAuthorMapper.java b/src/test/java/org/apache/ibatis/binding/BoundAuthorMapper.java index a240332f50e..d47954feb3f 100644 --- a/src/test/java/org/apache/ibatis/binding/BoundAuthorMapper.java +++ b/src/test/java/org/apache/ibatis/binding/BoundAuthorMapper.java @@ -1,5 +1,5 @@ /** - * Copyright 2009-2018 the original author or authors. + * Copyright 2009-2019 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. @@ -68,6 +68,23 @@ public interface BoundAuthorMapper { //====================================================== + @Result(property = "id", column = "AUTHOR_ID", id = true) + @Result(property = "username", column = "AUTHOR_USERNAME") + @Result(property = "password", column = "AUTHOR_PASSWORD") + @Result(property = "email", column = "AUTHOR_EMAIL") + @Result(property = "bio", column = "AUTHOR_BIO") + @Select({ + "SELECT ", + " ID as AUTHOR_ID,", + " USERNAME as AUTHOR_USERNAME,", + " PASSWORD as AUTHOR_PASSWORD,", + " EMAIL as AUTHOR_EMAIL,", + " BIO as AUTHOR_BIO", + "FROM AUTHOR WHERE ID = #{id}"}) + Author selectAuthorMapToPropertiesUsingRepeatable(int id); + + //====================================================== + @ConstructorArgs({ @Arg(column = "AUTHOR_ID", javaType = Integer.class), @Arg(column = "AUTHOR_USERNAME", javaType = String.class), @@ -89,6 +106,73 @@ public interface BoundAuthorMapper { //====================================================== + @Arg(column = "AUTHOR_ID", javaType = Integer.class, id = true) + @Arg(column = "AUTHOR_USERNAME", javaType = String.class) + @Arg(column = "AUTHOR_PASSWORD", javaType = String.class) + @Arg(column = "AUTHOR_EMAIL", javaType = String.class) + @Arg(column = "AUTHOR_BIO", javaType = String.class) + @Arg(column = "AUTHOR_SECTION", javaType = Section.class) + @Select({ + "SELECT ", + " ID as AUTHOR_ID,", + " USERNAME as AUTHOR_USERNAME,", + " PASSWORD as AUTHOR_PASSWORD,", + " EMAIL as AUTHOR_EMAIL,", + " BIO as AUTHOR_BIO," + + " FAVOURITE_SECTION as AUTHOR_SECTION", + "FROM AUTHOR WHERE ID = #{id}"}) + Author selectAuthorMapToConstructorUsingRepeatable(int id); + + //====================================================== + + @Arg(column = "AUTHOR_ID", javaType = int.class) + @Result(property = "username", column = "AUTHOR_USERNAME") + @Select({ + "SELECT ", + " ID as AUTHOR_ID,", + " USERNAME as AUTHOR_USERNAME,", + " PASSWORD as AUTHOR_PASSWORD,", + " EMAIL as AUTHOR_EMAIL,", + " BIO as AUTHOR_BIO", + "FROM AUTHOR WHERE ID = #{id}"}) + Author selectAuthorUsingSingleRepeatable(int id); + + //====================================================== + + @ConstructorArgs({ + @Arg(column = "AUTHOR_ID", javaType = Integer.class), + @Arg(column = "AUTHOR_USERNAME", javaType = String.class), + @Arg(column = "AUTHOR_PASSWORD", javaType = String.class), + @Arg(column = "AUTHOR_EMAIL", javaType = String.class), + @Arg(column = "AUTHOR_BIO", javaType = String.class) + }) + @Arg(column = "AUTHOR_SECTION", javaType = Section.class) + @Select({ + "SELECT ", + " ID as AUTHOR_ID,", + " USERNAME as AUTHOR_USERNAME,", + " PASSWORD as AUTHOR_PASSWORD,", + " EMAIL as AUTHOR_EMAIL,", + " BIO as AUTHOR_BIO," + + " FAVOURITE_SECTION as AUTHOR_SECTION", + "FROM AUTHOR WHERE ID = #{id}"}) + Author selectAuthorUsingBothArgAndConstructorArgs(int id); + + //====================================================== + + @Results( + @Result(property = "id", column = "AUTHOR_ID") + ) + @Result(property = "username", column = "AUTHOR_USERNAME") + @Select({ + "SELECT ", + " ID as AUTHOR_ID,", + " USERNAME as AUTHOR_USERNAME", + "FROM AUTHOR WHERE ID = #{id}"}) + Author selectAuthorUsingBothResultAndResults(int id); + + //====================================================== + List findThreeSpecificPosts(@Param("one") int one, RowBounds rowBounds, @Param("two") int two,