From c8d3b1e950ecbb1899468f4746bd3c677de3f1a4 Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Thu, 21 Mar 2019 12:12:51 +0900 Subject: [PATCH 1/5] Allow omit a 'method' attribute on SqlProvider annotation Fixes gh-1279 --- .../ibatis/annotations/DeleteProvider.java | 4 +- .../ibatis/annotations/InsertProvider.java | 4 +- .../ibatis/annotations/SelectProvider.java | 4 +- .../ibatis/annotations/UpdateProvider.java | 4 +- .../annotation/ProviderMethodResolver.java | 69 ++++++ .../builder/annotation/ProviderSqlSource.java | 25 +- .../ibatis/submitted/sqlprovider/CreateDB.sql | 9 +- .../ProviderMethodResolutionTest.java | 233 ++++++++++++++++++ 8 files changed, 334 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/apache/ibatis/builder/annotation/ProviderMethodResolver.java create mode 100644 src/test/java/org/apache/ibatis/submitted/sqlprovider/ProviderMethodResolutionTest.java diff --git a/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java b/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java index fb315fc0aa7..f2c2aad01cd 100644 --- a/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java @@ -1,5 +1,5 @@ /** - * Copyright 2009-2016 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. @@ -30,5 +30,5 @@ public @interface DeleteProvider { Class type(); - String method(); + String method() default ""; } diff --git a/src/main/java/org/apache/ibatis/annotations/InsertProvider.java b/src/main/java/org/apache/ibatis/annotations/InsertProvider.java index bb38bcdc6fc..c3a3c48ebf0 100644 --- a/src/main/java/org/apache/ibatis/annotations/InsertProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/InsertProvider.java @@ -1,5 +1,5 @@ /** - * Copyright 2009-2016 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. @@ -30,5 +30,5 @@ public @interface InsertProvider { Class type(); - String method(); + String method() default ""; } diff --git a/src/main/java/org/apache/ibatis/annotations/SelectProvider.java b/src/main/java/org/apache/ibatis/annotations/SelectProvider.java index 68e95e566e4..2f8063399b7 100644 --- a/src/main/java/org/apache/ibatis/annotations/SelectProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/SelectProvider.java @@ -1,5 +1,5 @@ /** - * Copyright 2009-2016 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. @@ -30,5 +30,5 @@ public @interface SelectProvider { Class type(); - String method(); + String method() default ""; } diff --git a/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java b/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java index 695bfa7819b..dc5b7e36345 100644 --- a/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java @@ -1,5 +1,5 @@ /** - * Copyright 2009-2016 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. @@ -30,5 +30,5 @@ public @interface UpdateProvider { Class type(); - String method(); + String method() default ""; } diff --git a/src/main/java/org/apache/ibatis/builder/annotation/ProviderMethodResolver.java b/src/main/java/org/apache/ibatis/builder/annotation/ProviderMethodResolver.java new file mode 100644 index 00000000000..3db14ef227d --- /dev/null +++ b/src/main/java/org/apache/ibatis/builder/annotation/ProviderMethodResolver.java @@ -0,0 +1,69 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ibatis.builder.annotation; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.ibatis.builder.BuilderException; + +/** + * The interface that resolve an SQL provider method via an SQL provider class. + * + *

This interface need to implements at an SQL provider class and + * it need to define the default constructor for creating a new instance. + * + * @since 3.5.1 + * @author Kazuki Shimizu + */ +public interface ProviderMethodResolver { + + /** + * Resolve an SQL provider method. + * + *

The default implementation return a method that matches following conditions. + *

+ * If matched method is zero or multiple, it throws a {@link BuilderException}. + * + * @param context a context for SQL provider + * @return an SQL provider method + * @throws BuilderException Throws when cannot resolve a target method + */ + default Method resolveMethod(ProviderContext context) { + List targetMethods = Arrays.stream(getClass().getMethods()) + .filter(m -> m.getName().equals(context.getMapperMethod().getName())) + .filter(m -> CharSequence.class.isAssignableFrom(m.getReturnType())) + .collect(Collectors.toList()); + if (targetMethods.size() == 1) { + return targetMethods.get(0); + } + if (targetMethods.isEmpty()) { + throw new BuilderException("Cannot resolve the provide method because '" + + context.getMapperMethod().getName() + "' not found in SqlProvider '" + getClass().getName() + "'."); + } else { + throw new BuilderException("Cannot resolve the provide method because '" + + context.getMapperMethod().getName() + "' is found multiple in SqlProvider '" + getClass().getName() + "'."); + } + } + +} diff --git a/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java b/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java index 48d2a697276..cc047832d05 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java @@ -62,16 +62,21 @@ public ProviderSqlSource(Configuration configuration, Object provider, Class this.providerType = (Class) provider.getClass().getMethod("type").invoke(provider); providerMethodName = (String) provider.getClass().getMethod("method").invoke(provider); - for (Method m : this.providerType.getMethods()) { - if (providerMethodName.equals(m.getName()) && CharSequence.class.isAssignableFrom(m.getReturnType())) { - if (providerMethod != null) { - throw new BuilderException("Error creating SqlSource for SqlProvider. Method '" - + providerMethodName + "' is found multiple in SqlProvider '" + this.providerType.getName() - + "'. Sql provider method can not overload."); + if (providerMethodName.length() == 0 && ProviderMethodResolver.class.isAssignableFrom(this.providerType)) { + this.providerMethod = ((ProviderMethodResolver) this.providerType.getDeclaredConstructor().newInstance()) + .resolveMethod(new ProviderContext(mapperType, mapperMethod)); + } + if (this.providerMethod == null) { + providerMethodName = providerMethodName.length() == 0 ? "provideSql" : providerMethodName; + for (Method m : this.providerType.getMethods()) { + if (providerMethodName.equals(m.getName()) && CharSequence.class.isAssignableFrom(m.getReturnType())) { + if (this.providerMethod != null) { + throw new BuilderException("Error creating SqlSource for SqlProvider. Method '" + + providerMethodName + "' is found multiple in SqlProvider '" + this.providerType.getName() + + "'. Sql provider method can not overload."); + } + this.providerMethod = m; } - this.providerMethod = m; - this.providerMethodArgumentNames = new ParamNameResolver(configuration, m).getNames(); - this.providerMethodParameterTypes = m.getParameterTypes(); } } } catch (BuilderException e) { @@ -83,6 +88,8 @@ public ProviderSqlSource(Configuration configuration, Object provider, Class throw new BuilderException("Error creating SqlSource for SqlProvider. Method '" + providerMethodName + "' not found in SqlProvider '" + this.providerType.getName() + "'."); } + this.providerMethodArgumentNames = new ParamNameResolver(configuration, this.providerMethod).getNames(); + this.providerMethodParameterTypes = this.providerMethod.getParameterTypes(); for (int i = 0; i < this.providerMethodParameterTypes.length; i++) { Class parameterType = this.providerMethodParameterTypes[i]; if (parameterType == ProviderContext.class) { diff --git a/src/test/java/org/apache/ibatis/submitted/sqlprovider/CreateDB.sql b/src/test/java/org/apache/ibatis/submitted/sqlprovider/CreateDB.sql index 5b9447bc93b..473ef879db3 100644 --- a/src/test/java/org/apache/ibatis/submitted/sqlprovider/CreateDB.sql +++ b/src/test/java/org/apache/ibatis/submitted/sqlprovider/CreateDB.sql @@ -1,5 +1,5 @@ -- --- Copyright 2009-2017 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. @@ -15,6 +15,7 @@ -- drop table users if exists; +drop table memos if exists; create table users ( id int, @@ -22,8 +23,14 @@ create table users ( logical_delete boolean default false ); +create table memos ( + id int, + memo varchar(1024), +); + insert into users (id, name) values(1, 'User1'); insert into users (id, name) values(2, 'User2'); insert into users (id, name) values(3, 'User3'); insert into users (id, name, logical_delete) values(4, 'User4', true); +insert into memos (id, memo) values(1, 'memo1'); diff --git a/src/test/java/org/apache/ibatis/submitted/sqlprovider/ProviderMethodResolutionTest.java b/src/test/java/org/apache/ibatis/submitted/sqlprovider/ProviderMethodResolutionTest.java new file mode 100644 index 00000000000..2104d70efd3 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/sqlprovider/ProviderMethodResolutionTest.java @@ -0,0 +1,233 @@ +/** + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.sqlprovider; + +import java.io.Reader; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.ibatis.BaseDataTest; +import org.apache.ibatis.annotations.DeleteProvider; +import org.apache.ibatis.annotations.InsertProvider; +import org.apache.ibatis.annotations.SelectProvider; +import org.apache.ibatis.annotations.UpdateProvider; +import org.apache.ibatis.builder.BuilderException; +import org.apache.ibatis.builder.annotation.ProviderContext; +import org.apache.ibatis.builder.annotation.ProviderMethodResolver; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test for https://github.com/mybatis/mybatis-3/issues/1279 + */ +class ProviderMethodResolutionTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeAll + static void setUp() throws Exception { + try (Reader reader = Resources + .getResourceAsReader("org/apache/ibatis/submitted/sqlprovider/mybatis-config.xml")) { + sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); + sqlSessionFactory.getConfiguration().addMapper(ProvideMethodResolverMapper.class); + } + BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), + "org/apache/ibatis/submitted/sqlprovider/CreateDB.sql"); + } + + @Test + void shouldResolveWhenDefaultResolverMatchedMethodIsOne() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + ProvideMethodResolverMapper mapper = sqlSession.getMapper(ProvideMethodResolverMapper.class); + assertEquals(1, mapper.select()); + } + } + + @Test + void shouldErrorWhenDefaultResolverMatchedMethodIsNone() { + BuilderException e = Assertions.assertThrows(BuilderException.class, + () -> sqlSessionFactory.getConfiguration().addMapper(DefaultProvideMethodResolverMatchedMethodIsNoneMapper.class)); + assertEquals( + "Cannot resolve the provide method because 'insert' not found in SqlProvider 'org.apache.ibatis.submitted.sqlprovider.ProviderMethodResolutionTest$DefaultProvideMethodResolverMatchedMethodIsNoneMapper$MethodResolverBasedSqlProvider'.", + e.getCause().getMessage()); + } + + @Test + void shouldErrorWhenDefaultResolverMatchedMethodIsMultiple() { + BuilderException e = Assertions.assertThrows(BuilderException.class, + () -> sqlSessionFactory.getConfiguration().addMapper(DefaultProvideMethodResolverMatchedMethodIsMultipleMapper.class)); + assertEquals( + "Cannot resolve the provide method because 'update' is found multiple in SqlProvider 'org.apache.ibatis.submitted.sqlprovider.ProviderMethodResolutionTest$DefaultProvideMethodResolverMatchedMethodIsMultipleMapper$MethodResolverBasedSqlProvider'.", + e.getCause().getMessage()); + } + + @Test + void shouldResolveReservedMethod() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + ProvideMethodResolverMapper mapper = sqlSession.getMapper(ProvideMethodResolverMapper.class); + assertEquals(1, mapper.delete()); + } + } + + @Test + void shouldUseSpecifiedMethodOnSqlProviderAnnotation() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + ProvideMethodResolverMapper mapper = sqlSession.getMapper(ProvideMethodResolverMapper.class); + assertEquals(2, mapper.select2()); + } + } + + @Test + void shouldResolveMethodUsingCustomResolver() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + ProvideMethodResolverMapper mapper = sqlSession.getMapper(ProvideMethodResolverMapper.class); + assertEquals(3, mapper.select3()); + } + } + + @Test + void shouldResolveReservedNameMethodWhenCustomResolverReturnNull() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + ProvideMethodResolverMapper mapper = sqlSession.getMapper(ProvideMethodResolverMapper.class); + assertEquals(99, mapper.select4()); + } + } + + @Test + void shouldErrorWhenCannotDetectsReservedNameMethod() { + BuilderException e = Assertions.assertThrows(BuilderException.class, + () -> sqlSessionFactory.getConfiguration().addMapper(ReservedNameMethodIsNoneMapper.class)); + assertEquals( + "Error creating SqlSource for SqlProvider. Method 'provideSql' not found in SqlProvider 'org.apache.ibatis.submitted.sqlprovider.ProviderMethodResolutionTest$ReservedNameMethodIsNoneMapper$SqlProvider'.", + e.getCause().getMessage()); + } + + interface ProvideMethodResolverMapper { + + @SelectProvider(type = MethodResolverBasedSqlProvider.class) + int select(); + + @SelectProvider(type = MethodResolverBasedSqlProvider.class, method = "provideSelect2Sql") + int select2(); + + @SelectProvider(type = CustomMethodResolverBasedSqlProvider.class) + int select3(); + + @SelectProvider(type = CustomMethodResolverBasedSqlProvider.class) + int select4(); + + @DeleteProvider(type = ReservedMethodNameBasedSqlProvider.class) + int delete(); + + class MethodResolverBasedSqlProvider implements ProviderMethodResolver { + public static String select() { + return "SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS"; + } + + public static String select2() { + throw new IllegalStateException("This method should not called when specify `method` attribute on @SelectProvider."); + } + + public static String provideSelect2Sql() { + return "SELECT 2 FROM INFORMATION_SCHEMA.SYSTEM_USERS"; + } + } + + class ReservedMethodNameBasedSqlProvider { + public static String provideSql() { + return "DELETE FROM memos WHERE id = 1"; + } + } + + class CustomMethodResolverBasedSqlProvider implements CustomProviderMethodResolver { + public static String select3Sql() { + return "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS"; + } + + public static String provideSql() { + return "SELECT 99 FROM INFORMATION_SCHEMA.SYSTEM_USERS"; + } + } + + } + + interface CustomProviderMethodResolver extends ProviderMethodResolver { + @Override + default Method resolveMethod(ProviderContext context) { + List targetMethods = Arrays.stream(getClass().getMethods()) + .filter(m -> m.getName().equals(context.getMapperMethod().getName() + "Sql")) + .filter(m -> CharSequence.class.isAssignableFrom(m.getReturnType())) + .collect(Collectors.toList()); + if (targetMethods.size() == 1) { + return targetMethods.get(0); + } + return null; + } + } + + interface DefaultProvideMethodResolverMatchedMethodIsNoneMapper { + + @InsertProvider(type = MethodResolverBasedSqlProvider.class) + int insert(); + + class MethodResolverBasedSqlProvider implements ProviderMethodResolver { + public static int insert() { + return 1; + } + } + + } + + interface DefaultProvideMethodResolverMatchedMethodIsMultipleMapper { + + @UpdateProvider(type = MethodResolverBasedSqlProvider.class) + int update(); + + class MethodResolverBasedSqlProvider implements ProviderMethodResolver { + public static String update() { + return "UPDATE foo SET name = #{name} WHERE id = #{id}"; + } + + public static StringBuilder update(ProviderContext context) { + return new StringBuilder("UPDATE foo SET name = #{name} WHERE id = #{id}"); + } + } + + } + + interface ReservedNameMethodIsNoneMapper { + + @UpdateProvider(type = SqlProvider.class) + int update(); + + class SqlProvider { + public static String select() { + return "SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS"; + } + } + + } + +} From a7869b22a6f9b1b69d433ffbcc97ada628f915c3 Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Thu, 21 Mar 2019 14:42:18 +0900 Subject: [PATCH 2/5] Update documentation for gh-1279 --- src/site/es/xdoc/java-api.xml | 46 ++++++++++++++++++++++++++++++++++- src/site/ja/xdoc/java-api.xml | 26 +++++++++++++++++++- src/site/ko/xdoc/java-api.xml | 24 +++++++++++++++++- src/site/xdoc/java-api.xml | 26 +++++++++++++++++++- src/site/zh/xdoc/java-api.xml | 28 ++++++++++++++++++++- 5 files changed, 145 insertions(+), 5 deletions(-) diff --git a/src/site/es/xdoc/java-api.xml b/src/site/es/xdoc/java-api.xml index 829f0741269..331399f3d25 100644 --- a/src/site/es/xdoc/java-api.xml +++ b/src/site/es/xdoc/java-api.xml @@ -464,7 +464,13 @@ try (SqlSession session = sqlSessionFactory.openSession()) { Estas anotaciones SQL alternativas te permiten especificar un nombre de clases y un método que devolverán la SQL que debe ejecutarse (Since 3.4.6, you can specify the CharSequence instead of String as a method return type). Cuando se ejecute el método MyBatis instanciará la clase y ejecutará el método especificados en el provider. You can pass objects that passed to arguments of a mapper method, "Mapper interface type" and "Mapper method" - via the ProviderContext(available since MyBatis 3.4.5 or later) as method argument.(In MyBatis 3.4 or later, it's allow multiple parameters) Atributos: type, method. El atributo type es el nombre completamente cualificado de una clase. El method es el nombre un método de dicha clase. Nota: A continuación hay una sección sobre la clase, que puede ayudar a construir SQL dinámico de una forma más clara y sencilla de leer. + via the ProviderContext(available since MyBatis 3.4.5 or later) as method argument.(In MyBatis 3.4 or later, it's allow multiple parameters) + Atributos: type, method. El atributo type es el nombre completamente cualificado de una clase. + El method es el nombre un método de dicha clase + (Since 3.5.1, you can omit method attribute, the MyBatis will resolve a target method via the + ProviderMethodResolver interface. + If not resolve by it, the MyBatis use the reserved fallback method that named provideSql). + Nota: A continuación hay una sección sobre la clase, que puede ayudar a construir SQL dinámico de una forma más clara y sencilla de leer. @Param @@ -583,6 +589,44 @@ class UserSqlBuilder { } }]]> +

This example shows usage the default implementation of ProviderMethodResolver(available since MyBatis 3.5.1 or later):

+ getUsersByName(String name); + +// Implements the ProviderMethodResolver on your provider class +class UserSqlProvider implements ProviderMethodResolver { + // In default implementation, it will resolve a method that method name is matched with mapper method + public static String getUsersByName(final String name) { + return new SQL(){{ + SELECT("*"); + FROM("users"); + if (name != null) { + WHERE("name like #{value} || '%'"); + } + ORDER_BY("id"); + }}.toString(); + } +}]]> + +

This example shows usage the default implementation of ProviderMethodResolver(available since MyBatis 3.5.1 or later):

+ getUsersByName(String name); + +// Implements the ProviderMethodResolver on your provider class +class UserSqlProvider implements ProviderMethodResolver { + // In default implementation, it will resolve a method that method name is matched with mapper method + public static String getUsersByName(final String name) { + return new SQL(){{ + SELECT("*"); + FROM("users"); + if (name != null) { + WHERE("name like #{value} || '%'"); + } + ORDER_BY("id"); + }}.toString(); + } +}]]> + diff --git a/src/site/ja/xdoc/java-api.xml b/src/site/ja/xdoc/java-api.xml index df8633766e7..52a4b89e4e5 100644 --- a/src/site/ja/xdoc/java-api.xml +++ b/src/site/ja/xdoc/java-api.xml @@ -477,7 +477,10 @@ try (SqlSession session = sqlSessionFactory.openSession()) { これらのアノテーションは動的 SQL を生成するためのものです。実行時に指定されたメソッドが呼び出され、メソッドから返された SQL ステートメントが実行されます (MyBatis 3.4.6以降では、メソッドの返り値として String ではなく CharSequence を指定することができます)。 マップドステートメントを実行する際、プロバイダーによって指定したクラスのインスタンスが作成され、指定されたメソッドが実行されます。 なお、メソッド引数にはMapperメソッドの引数に渡したオブジェクトに加え、ProviderContext(MyBatis 3.4.5以降で利用可能)を介して「Mapperインタフェースの型」と「Mapperメソッド」を渡すことができます。(MyBatis 3.4以降では、複数の引数を渡すことができます) - キー: type, method. type にはクラスオブジェクト、method にはメソッド名を指定します。 NOTE 次の章で、クリーンで可読性の高いコードで動的 SQL を構築するためのクラスについて説明します。 + キー: type, method. type にはクラスオブジェクト、method にはメソッド名を指定します + (MyBatis 3.5.1以降では、method 属性を省略することができます。その際MyBatisは、ProviderMethodResolver インタフェースを介して対象メソッドの解決を試み、 + 対象メソッドが解決できない場合は、provideSqlという名前のメソッドを代替メソッドとして利用します)。 + NOTE 次の章で、クリーンで可読性の高いコードで動的 SQL を構築するためのクラスについて説明します。 @@ -591,6 +594,27 @@ class UserSqlBuilder { ORDER_BY(orderByColumn); }}.toString(); } +}]]> + +

次のコードは、ProviderMethodResolver(MyBatis 3.5.1以降で利用可能)のデフォルト実装の利用例です。

+ getUsersByName(String name); + +// SQLプロバイダクラスにProviderMethodResolverを実装する +class UserSqlProvider implements ProviderMethodResolver { + + // デフォルト実装では、マッパーメソッドと同名のメソッドが対象メソッドとして扱われます。 + public static String getUsersByName(final String name) { + return new SQL(){{ + SELECT("*"); + FROM("users"); + if (name != null) { + WHERE("name like #{value} || '%'"); + } + ORDER_BY("id"); + }}.toString(); + } + }]]> diff --git a/src/site/ko/xdoc/java-api.xml b/src/site/ko/xdoc/java-api.xml index 2d612f921e9..dd5aad87e0d 100644 --- a/src/site/ko/xdoc/java-api.xml +++ b/src/site/ko/xdoc/java-api.xml @@ -601,7 +601,10 @@ try (SqlSession session = sqlSessionFactory.openSession()) { Mapper 메서드의 인수인 "Mapper interface type" 과 ProviderContext(Mybatis 3.4.5 부터) 를 이용한 "Mapper method" 로 전달 된 객체를 메서드 매개변수로 전달할 수 있다.(마이바티스 3.4이상에서는 복수 파라미터를 허용한다.) 사용가능한 속성들 : type, method. type 속성은 클래스. - method 속성은 메소드명이다. + method 속성은 메소드명이다 + (Since 3.5.1, you can omit method attribute, the MyBatis will resolve a target method via the + ProviderMethodResolver interface. + If not resolve by it, the MyBatis use the reserved fallback method that named provideSql). Note: 이 섹션은 클래스에 대한 설명으로 동적 SQL 을 좀더 깔끔하고 읽기 쉽게 만드는데 도움이 될 수 있다. @@ -734,6 +737,25 @@ class UserSqlBuilder { } }]]> +

This example shows usage the default implementation of ProviderMethodResolver(available since MyBatis 3.5.1 or later):

+ getUsersByName(String name); + +// Implements the ProviderMethodResolver on your provider class +class UserSqlProvider implements ProviderMethodResolver { + // In default implementation, it will resolve a method that method name is matched with mapper method + public static String getUsersByName(final String name) { + return new SQL(){{ + SELECT("*"); + FROM("users"); + if (name != null) { + WHERE("name like #{value} || '%'"); + } + ORDER_BY("id"); + }}.toString(); + } +}]]> + diff --git a/src/site/xdoc/java-api.xml b/src/site/xdoc/java-api.xml index 74e53617053..55f9ecb04e8 100644 --- a/src/site/xdoc/java-api.xml +++ b/src/site/xdoc/java-api.xml @@ -515,7 +515,11 @@ try (SqlSession session = sqlSessionFactory.openSession()) { via the ProviderContext(available since MyBatis 3.4.5 or later) as method argument. (In MyBatis 3.4 or later, it's allow multiple parameters) Attributes: type, method. The type attribute is a class. - The method is the name of the method on that class. NOTE + The method is the name of the method on that class + (Since 3.5.1, you can omit method attribute, the MyBatis will resolve a target method via the + ProviderMethodResolver interface. + If not resolve by it, the MyBatis use the reserved fallback method that named provideSql). + NOTE Following this section is a discussion about the class, which can help build dynamic SQL in a cleaner, easier to read way. @@ -648,6 +652,26 @@ class UserSqlBuilder { }}.toString(); } }]]> + +

This example shows usage the default implementation of ProviderMethodResolver(available since MyBatis 3.5.1 or later):

+ getUsersByName(String name); + +// Implements the ProviderMethodResolver on your provider class +class UserSqlProvider implements ProviderMethodResolver { + // In default implementation, it will resolve a method that method name is matched with mapper method + public static String getUsersByName(final String name) { + return new SQL(){{ + SELECT("*"); + FROM("users"); + if (name != null) { + WHERE("name like #{value} || '%'"); + } + ORDER_BY("id"); + }}.toString(); + } +}]]> + diff --git a/src/site/zh/xdoc/java-api.xml b/src/site/zh/xdoc/java-api.xml index f35c5bdda64..d36515c1f3b 100644 --- a/src/site/zh/xdoc/java-api.xml +++ b/src/site/zh/xdoc/java-api.xml @@ -465,7 +465,14 @@ try (SqlSession session = sqlSessionFactory.openSession()) {
  • <select>
  • -        允许构建动态 SQL。这些备选的 SQL 注解允许你指定类名和返回在运行时执行的 SQL 语句的方法。(自从MyBatis 3.4.6开始,你可以用 CharSequence 代替 String 来返回类型返回值了。)当执行映射语句的时候,MyBatis 会实例化类并执行方法,类和方法就是填入了注解的值。你可以把已经传递给映射方法了的对象作为参数,"Mapper interface type" 和 "Mapper method" 会经过 ProviderContext (仅在MyBatis 3.4.5及以上支持)作为参数值。(MyBatis 3.4及以上的版本,支持多参数传入)属性有: type, methodtype 属性需填入类。method 需填入该类定义了的方法名。注意 接下来的小节将会讨论类,能帮助你更轻松地构建动态 SQL。 +        允许构建动态 SQL。这些备选的 SQL 注解允许你指定类名和返回在运行时执行的 SQL 语句的方法。(自从MyBatis 3.4.6开始,你可以用 CharSequence 代替 String 来返回类型返回值了。)当执行映射语句的时候,MyBatis 会实例化类并执行方法,类和方法就是填入了注解的值。你可以把已经传递给映射方法了的对象作为参数,"Mapper interface type" 和 "Mapper method" 会经过 ProviderContext (仅在MyBatis 3.4.5及以上支持)作为参数值。(MyBatis 3.4及以上的版本,支持多参数传入) + 属性有: type, method。 + type 属性需填入类。 + method 需填入该类定义了的方法名 + (Since 3.5.1, you can omit method attribute, the MyBatis will resolve a target method via the + ProviderMethodResolver interface. + If not resolve by it, the MyBatis use the reserved fallback method that named provideSql)。 + 注意 接下来的小节将会讨论类,能帮助你更轻松地构建动态 SQL。 @Param @@ -578,6 +585,25 @@ class UserSqlBuilder { } }]]> +

    This example shows usage the default implementation of ProviderMethodResolver(available since MyBatis 3.5.1 or later):

    + getUsersByName(String name); + +// Implements the ProviderMethodResolver on your provider class +class UserSqlProvider implements ProviderMethodResolver { + // In default implementation, it will resolve a method that method name is matched with mapper method + public static String getUsersByName(final String name) { + return new SQL(){{ + SELECT("*"); + FROM("users"); + if (name != null) { + WHERE("name like #{value} || '%'"); + } + ORDER_BY("id"); + }}.toString(); + } +}]]> + From cf0a588a9590ff1714b51071f2ade46c2bc697e0 Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Fri, 22 Mar 2019 08:33:28 +0900 Subject: [PATCH 3/5] Add javadoc on SQL provider annotation Related with gh-1279 --- .../ibatis/annotations/DeleteProvider.java | 26 +++++++++++++++++++ .../ibatis/annotations/InsertProvider.java | 26 +++++++++++++++++++ .../ibatis/annotations/SelectProvider.java | 26 +++++++++++++++++++ .../ibatis/annotations/UpdateProvider.java | 26 +++++++++++++++++++ 4 files changed, 104 insertions(+) diff --git a/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java b/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java index f2c2aad01cd..11f4ddf3d7f 100644 --- a/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java @@ -28,7 +28,33 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DeleteProvider { + + /** + * Specify a type that implements an SQL provider method. + * + * @return a type that implements an SQL provider method + */ Class type(); + /** + * Specify a method for providing an SQL. + * + *

    + * Since 3.5.1, this attribute can omit. + * If this attribute omit, the MyBatis will call a method that decide by following rules. + *

      + *
    • + * If class that specified the {@link #type()} attribute implements the {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}, + * the MyBatis use a method that returned by it + *
    • + *
    • + * If cannot resolve a method by {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}(= not implement it or it was returned {@code null}), + * the MyBatis will search and use a fallback method that named {@code resolveSql} from specified type + *
    • + *
    + * + * @return a method name of method for providing an SQL + */ String method() default ""; + } diff --git a/src/main/java/org/apache/ibatis/annotations/InsertProvider.java b/src/main/java/org/apache/ibatis/annotations/InsertProvider.java index c3a3c48ebf0..0099e94c8f6 100644 --- a/src/main/java/org/apache/ibatis/annotations/InsertProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/InsertProvider.java @@ -28,7 +28,33 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface InsertProvider { + + /** + * Specify a type that implements an SQL provider method. + * + * @return a type that implements an SQL provider method + */ Class type(); + /** + * Specify a method for providing an SQL. + * + *

    + * Since 3.5.1, this attribute can omit. + * If this attribute omit, the MyBatis will call a method that decide by following rules. + *

      + *
    • + * If class that specified the {@link #type()} attribute implements the {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}, + * the MyBatis use a method that returned by it + *
    • + *
    • + * If cannot resolve a method by {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}(= not implement it or it was returned {@code null}), + * the MyBatis will search and use a fallback method that named {@code resolveSql} from specified type + *
    • + *
    + * + * @return a method name of method for providing an SQL + */ String method() default ""; + } diff --git a/src/main/java/org/apache/ibatis/annotations/SelectProvider.java b/src/main/java/org/apache/ibatis/annotations/SelectProvider.java index 2f8063399b7..7bc8d36c22f 100644 --- a/src/main/java/org/apache/ibatis/annotations/SelectProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/SelectProvider.java @@ -28,7 +28,33 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface SelectProvider { + + /** + * Specify a type that implements an SQL provider method. + * + * @return a type that implements an SQL provider method + */ Class type(); + /** + * Specify a method for providing an SQL. + * + *

    + * Since 3.5.1, this attribute can omit. + * If this attribute omit, the MyBatis will call a method that decide by following rules. + *

      + *
    • + * If class that specified the {@link #type()} attribute implements the {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}, + * the MyBatis use a method that returned by it + *
    • + *
    • + * If cannot resolve a method by {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}(= not implement it or it was returned {@code null}), + * the MyBatis will search and use a fallback method that named {@code resolveSql} from specified type + *
    • + *
    + * + * @return a method name of method for providing an SQL + */ String method() default ""; + } diff --git a/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java b/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java index dc5b7e36345..6c54c5d1a85 100644 --- a/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java @@ -28,7 +28,33 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface UpdateProvider { + + /** + * Specify a type that implements an SQL provider method. + * + * @return a type that implements an SQL provider method + */ Class type(); + /** + * Specify a method for providing an SQL. + * + *

    + * Since 3.5.1, this attribute can omit. + * If this attribute omit, the MyBatis will call a method that decide by following rules. + *

      + *
    • + * If class that specified the {@link #type()} attribute implements the {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}, + * the MyBatis use a method that returned by it + *
    • + *
    • + * If cannot resolve a method by {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}(= not implement it or it was returned {@code null}), + * the MyBatis will search and use a fallback method that named {@code resolveSql} from specified type + *
    • + *
    + * + * @return a method name of method for providing an SQL + */ String method() default ""; + } From b1cfb84f6f651cd01ddfc5f993ad1e191474213d Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Sun, 24 Mar 2019 23:40:11 +0900 Subject: [PATCH 4/5] Fix to use correct method name at JavaDoc See gh-1279 --- src/main/java/org/apache/ibatis/annotations/DeleteProvider.java | 2 +- src/main/java/org/apache/ibatis/annotations/InsertProvider.java | 2 +- src/main/java/org/apache/ibatis/annotations/SelectProvider.java | 2 +- src/main/java/org/apache/ibatis/annotations/UpdateProvider.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java b/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java index 11f4ddf3d7f..b4856476c7b 100644 --- a/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/DeleteProvider.java @@ -49,7 +49,7 @@ * *
  • * If cannot resolve a method by {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}(= not implement it or it was returned {@code null}), - * the MyBatis will search and use a fallback method that named {@code resolveSql} from specified type + * the MyBatis will search and use a fallback method that named {@code provideSql} from specified type *
  • * * diff --git a/src/main/java/org/apache/ibatis/annotations/InsertProvider.java b/src/main/java/org/apache/ibatis/annotations/InsertProvider.java index 0099e94c8f6..de5f395185d 100644 --- a/src/main/java/org/apache/ibatis/annotations/InsertProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/InsertProvider.java @@ -49,7 +49,7 @@ * *
  • * If cannot resolve a method by {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}(= not implement it or it was returned {@code null}), - * the MyBatis will search and use a fallback method that named {@code resolveSql} from specified type + * the MyBatis will search and use a fallback method that named {@code provideSql} from specified type *
  • * * diff --git a/src/main/java/org/apache/ibatis/annotations/SelectProvider.java b/src/main/java/org/apache/ibatis/annotations/SelectProvider.java index 7bc8d36c22f..d533c0bc677 100644 --- a/src/main/java/org/apache/ibatis/annotations/SelectProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/SelectProvider.java @@ -49,7 +49,7 @@ * *
  • * If cannot resolve a method by {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}(= not implement it or it was returned {@code null}), - * the MyBatis will search and use a fallback method that named {@code resolveSql} from specified type + * the MyBatis will search and use a fallback method that named {@code provideSql} from specified type *
  • * * diff --git a/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java b/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java index 6c54c5d1a85..bc765336fed 100644 --- a/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java +++ b/src/main/java/org/apache/ibatis/annotations/UpdateProvider.java @@ -49,7 +49,7 @@ * *
  • * If cannot resolve a method by {@link org.apache.ibatis.builder.annotation.ProviderMethodResolver}(= not implement it or it was returned {@code null}), - * the MyBatis will search and use a fallback method that named {@code resolveSql} from specified type + * the MyBatis will search and use a fallback method that named {@code provideSql} from specified type *
  • * * From 877284ac59e5dfb65f979b15d7360fb032a9759e Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Mon, 25 Mar 2019 00:22:25 +0900 Subject: [PATCH 5/5] Improve the error message when return type does not matched the CharSequence or its subclass See gh-1279 --- .../annotation/ProviderMethodResolver.java | 16 +++++++--- .../ProviderMethodResolutionTest.java | 32 ++++++++++++++++--- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/apache/ibatis/builder/annotation/ProviderMethodResolver.java b/src/main/java/org/apache/ibatis/builder/annotation/ProviderMethodResolver.java index 3db14ef227d..3bd775e5f53 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/ProviderMethodResolver.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/ProviderMethodResolver.java @@ -18,7 +18,6 @@ import java.lang.reflect.Method; import java.util.Arrays; -import java.util.Date; import java.util.List; import java.util.stream.Collectors; @@ -50,18 +49,25 @@ public interface ProviderMethodResolver { * @throws BuilderException Throws when cannot resolve a target method */ default Method resolveMethod(ProviderContext context) { - List targetMethods = Arrays.stream(getClass().getMethods()) + List sameNameMethods = Arrays.stream(getClass().getMethods()) .filter(m -> m.getName().equals(context.getMapperMethod().getName())) + .collect(Collectors.toList()); + if (sameNameMethods.isEmpty()) { + throw new BuilderException("Cannot resolve the provider method because '" + + context.getMapperMethod().getName() + "' not found in SqlProvider '" + getClass().getName() + "'."); + } + List targetMethods = sameNameMethods.stream() .filter(m -> CharSequence.class.isAssignableFrom(m.getReturnType())) .collect(Collectors.toList()); if (targetMethods.size() == 1) { return targetMethods.get(0); } if (targetMethods.isEmpty()) { - throw new BuilderException("Cannot resolve the provide method because '" - + context.getMapperMethod().getName() + "' not found in SqlProvider '" + getClass().getName() + "'."); + throw new BuilderException("Cannot resolve the provider method because '" + + context.getMapperMethod().getName() + "' does not return the CharSequence or its subclass in SqlProvider '" + + getClass().getName() + "'."); } else { - throw new BuilderException("Cannot resolve the provide method because '" + throw new BuilderException("Cannot resolve the provider method because '" + context.getMapperMethod().getName() + "' is found multiple in SqlProvider '" + getClass().getName() + "'."); } } diff --git a/src/test/java/org/apache/ibatis/submitted/sqlprovider/ProviderMethodResolutionTest.java b/src/test/java/org/apache/ibatis/submitted/sqlprovider/ProviderMethodResolutionTest.java index 2104d70efd3..98ef91e57b3 100644 --- a/src/test/java/org/apache/ibatis/submitted/sqlprovider/ProviderMethodResolutionTest.java +++ b/src/test/java/org/apache/ibatis/submitted/sqlprovider/ProviderMethodResolutionTest.java @@ -66,11 +66,20 @@ void shouldResolveWhenDefaultResolverMatchedMethodIsOne() { } @Test - void shouldErrorWhenDefaultResolverMatchedMethodIsNone() { + void shouldErrorWhenDefaultResolverMethodNameMatchedMethodIsNone() { BuilderException e = Assertions.assertThrows(BuilderException.class, - () -> sqlSessionFactory.getConfiguration().addMapper(DefaultProvideMethodResolverMatchedMethodIsNoneMapper.class)); + () -> sqlSessionFactory.getConfiguration().addMapper(DefaultProvideMethodResolverMethodNameMatchedMethodIsNoneMapper.class)); assertEquals( - "Cannot resolve the provide method because 'insert' not found in SqlProvider 'org.apache.ibatis.submitted.sqlprovider.ProviderMethodResolutionTest$DefaultProvideMethodResolverMatchedMethodIsNoneMapper$MethodResolverBasedSqlProvider'.", + "Cannot resolve the provider method because 'insert' not found in SqlProvider 'org.apache.ibatis.submitted.sqlprovider.ProviderMethodResolutionTest$DefaultProvideMethodResolverMethodNameMatchedMethodIsNoneMapper$MethodResolverBasedSqlProvider'.", + e.getCause().getMessage()); + } + + @Test + void shouldErrorWhenDefaultResolverReturnTypeMatchedMethodIsNone() { + BuilderException e = Assertions.assertThrows(BuilderException.class, + () -> sqlSessionFactory.getConfiguration().addMapper(DefaultProvideMethodResolverReturnTypeMatchedMethodIsNoneMapper.class)); + assertEquals( + "Cannot resolve the provider method because 'insert' does not return the CharSequence or its subclass in SqlProvider 'org.apache.ibatis.submitted.sqlprovider.ProviderMethodResolutionTest$DefaultProvideMethodResolverReturnTypeMatchedMethodIsNoneMapper$MethodResolverBasedSqlProvider'.", e.getCause().getMessage()); } @@ -79,7 +88,7 @@ void shouldErrorWhenDefaultResolverMatchedMethodIsMultiple() { BuilderException e = Assertions.assertThrows(BuilderException.class, () -> sqlSessionFactory.getConfiguration().addMapper(DefaultProvideMethodResolverMatchedMethodIsMultipleMapper.class)); assertEquals( - "Cannot resolve the provide method because 'update' is found multiple in SqlProvider 'org.apache.ibatis.submitted.sqlprovider.ProviderMethodResolutionTest$DefaultProvideMethodResolverMatchedMethodIsMultipleMapper$MethodResolverBasedSqlProvider'.", + "Cannot resolve the provider method because 'update' is found multiple in SqlProvider 'org.apache.ibatis.submitted.sqlprovider.ProviderMethodResolutionTest$DefaultProvideMethodResolverMatchedMethodIsMultipleMapper$MethodResolverBasedSqlProvider'.", e.getCause().getMessage()); } @@ -187,7 +196,20 @@ default Method resolveMethod(ProviderContext context) { } } - interface DefaultProvideMethodResolverMatchedMethodIsNoneMapper { + interface DefaultProvideMethodResolverMethodNameMatchedMethodIsNoneMapper { + + @InsertProvider(type = MethodResolverBasedSqlProvider.class) + int insert(); + + class MethodResolverBasedSqlProvider implements ProviderMethodResolver { + public static String provideInsertSql() { + return "INSERT INTO foo (name) VALUES(#{name})"; + } + } + + } + + interface DefaultProvideMethodResolverReturnTypeMatchedMethodIsNoneMapper { @InsertProvider(type = MethodResolverBasedSqlProvider.class) int insert();