From 2b3f93223db4172ff06a4ca6982b4f1b950cec9d Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Sat, 10 Jun 2023 16:31:50 +0900 Subject: [PATCH 1/3] Add the @Options#timeoutString Support a new attribute that specify the query timeout using configuration's variables such as ${timeout.query1} --- .../apache/ibatis/annotations/Options.java | 17 +++++++ .../annotation/MapperAnnotationBuilder.java | 21 ++++++++- .../apache/ibatis/parsing/PropertyParser.java | 5 ++- .../builder/AnnotationMapperBuilderTest.java | 45 +++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apache/ibatis/annotations/Options.java b/src/main/java/org/apache/ibatis/annotations/Options.java index 12d80c20bf7..907c306e5ee 100644 --- a/src/main/java/org/apache/ibatis/annotations/Options.java +++ b/src/main/java/org/apache/ibatis/annotations/Options.java @@ -96,9 +96,26 @@ enum FlushCachePolicy { * Returns the statement timeout. * * @return the statement timeout + * + * @see #timeoutString() */ int timeout() default -1; + /** + * Returns the statement timeout string. + *

+ * Can specify configuration's variables such as {@code ${timeout.select}}. If not resolve variable value, fallback + * the {@link #timeout()} value. + *

+ * + * @return the statement timeout string + * + * @see #timeout() + * + * @since 3.5.14 + */ + String timeoutString() default ""; + /** * Returns whether use the generated keys feature supported by JDBC 3.0 * 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 61c62ad360d..07ef54967b8 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java @@ -33,6 +33,7 @@ import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -80,6 +81,7 @@ import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.mapping.StatementType; +import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.parsing.PropertyParser; import org.apache.ibatis.reflection.TypeParameterResolver; import org.apache.ibatis.scripting.LanguageDriver; @@ -101,6 +103,8 @@ public class MapperAnnotationBuilder { InsertProvider.class, DeleteProvider.class) .collect(Collectors.toSet()); + private static final GenericTokenParser TOKEN_PARSER = new GenericTokenParser("${", "}", t -> t); + private final Configuration configuration; private final MapperBuilderAssistant assistant; private final Class type; @@ -345,7 +349,8 @@ void parseStatement(Method method) { useCache = options.useCache(); // issue #348 fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; - timeout = options.timeout() > -1 ? options.timeout() : null; + Integer fallbackTimeout = options.timeout() > -1 ? options.timeout() : null; + timeout = parseStringValue(options.timeoutString(), fallbackTimeout, Integer::parseInt); statementType = options.statementType(); if (options.resultSetType() != ResultSetType.DEFAULT) { resultSetType = options.resultSetType(); @@ -372,6 +377,20 @@ null, parameterTypeClass, resultMapId, getReturnType(method, type), resultSetTyp }); } + private T parseStringValue(String valueString, T fallbackValue, Function valueTypeConverter) { + if (valueString.isEmpty()) { + return fallbackValue; + } else { + Properties defaults = new Properties(); + Optional.ofNullable(fallbackValue).map(String::valueOf) + .ifPresent(x -> defaults.setProperty(TOKEN_PARSER.parse(valueString), x)); + Properties variables = new Properties(defaults); + variables.putAll(configuration.getVariables()); + return Optional.ofNullable(PropertyParser.parse(valueString, variables)).map(valueTypeConverter) + .orElse(fallbackValue); + } + } + private LanguageDriver getLanguageDriver(Method method) { Lang lang = method.getAnnotation(Lang.class); Class langClass = null; diff --git a/src/main/java/org/apache/ibatis/parsing/PropertyParser.java b/src/main/java/org/apache/ibatis/parsing/PropertyParser.java index 57301667ea7..1f36d91714d 100644 --- a/src/main/java/org/apache/ibatis/parsing/PropertyParser.java +++ b/src/main/java/org/apache/ibatis/parsing/PropertyParser.java @@ -88,8 +88,9 @@ public String handleToken(String content) { return variables.getProperty(key, defaultValue); } } - if (variables.containsKey(key)) { - return variables.getProperty(key); + String value = variables.getProperty(key); + if (value != null) { + return value; } } return "${" + content + "}"; diff --git a/src/test/java/org/apache/ibatis/builder/AnnotationMapperBuilderTest.java b/src/test/java/org/apache/ibatis/builder/AnnotationMapperBuilderTest.java index e031f27f317..4dc89c3357f 100644 --- a/src/test/java/org/apache/ibatis/builder/AnnotationMapperBuilderTest.java +++ b/src/test/java/org/apache/ibatis/builder/AnnotationMapperBuilderTest.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Properties; + import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Select; @@ -25,6 +27,7 @@ import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ResultSetType; import org.apache.ibatis.mapping.StatementType; +import org.apache.ibatis.parsing.PropertyParser; import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.Test; @@ -93,6 +96,26 @@ void withoutOptionsWhenNotSpecifyDefaultValue() { assertThat(mappedStatement.getResultSetType()).isEqualTo(ResultSetType.DEFAULT); } + @Test + void timeout() { + Configuration configuration = new Configuration(); + Properties variables = new Properties(); + variables.setProperty(PropertyParser.KEY_ENABLE_DEFAULT_VALUE, "true"); + variables.setProperty("timeout.select1", "200"); + configuration.setVariables(variables); + MapperAnnotationBuilder builder = new MapperAnnotationBuilder(configuration, TimeoutMapper.class); + builder.parse(); + + assertThat(configuration.getMappedStatement("selectWithTimeoutStringByNumber").getTimeout()).isEqualTo(10); + assertThat(configuration.getMappedStatement("selectWithTimeoutStringByVariable").getTimeout()).isEqualTo(200); + assertThat(configuration.getMappedStatement("selectWithTimeoutStringByVariableDefaultValue").getTimeout()) + .isEqualTo(30); + assertThat(configuration.getMappedStatement("selectWithTimeoutAndTimeoutStringVariableFound").getTimeout()) + .isEqualTo(200); + assertThat(configuration.getMappedStatement("selectWithTimeoutAndTimeoutStringVariableNotFound").getTimeout()) + .isEqualTo(40); + } + interface Mapper { @Insert("insert into test (name) values(#{name})") @@ -112,4 +135,26 @@ interface Mapper { } + interface TimeoutMapper { + @Select("select * from test") + @Options(timeoutString = "10") + String selectWithTimeoutStringByNumber(Integer id); + + @Select("select * from test") + @Options(timeoutString = "${timeout.select1}") + String selectWithTimeoutStringByVariable(Integer id); + + @Select("select * from test") + @Options(timeoutString = "${timeout.select2:30}") + String selectWithTimeoutStringByVariableDefaultValue(Integer id); + + @Select("select * from test") + @Options(timeout = 20, timeoutString = "${timeout.select1}") + String selectWithTimeoutAndTimeoutStringVariableFound(Integer id); + + @Select("select * from test") + @Options(timeout = 40, timeoutString = "${timeout.select3}") + String selectWithTimeoutAndTimeoutStringVariableNotFound(Integer id); + } + } From c8ff596e6da0c9f8dee624994acb139f896459b2 Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Sun, 11 Jun 2023 06:51:28 +0900 Subject: [PATCH 2/3] Revert changes on PropertParser for applying review comment of gh-2886 --- .../ibatis/builder/annotation/MapperAnnotationBuilder.java | 5 ++--- src/main/java/org/apache/ibatis/parsing/PropertyParser.java | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) 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 07ef54967b8..8a612caf055 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java @@ -381,10 +381,9 @@ private T parseStringValue(String valueString, T fallbackValue, Function defaults.setProperty(TOKEN_PARSER.parse(valueString), x)); - Properties variables = new Properties(defaults); + .ifPresent(x -> variables.setProperty(TOKEN_PARSER.parse(valueString), x)); variables.putAll(configuration.getVariables()); return Optional.ofNullable(PropertyParser.parse(valueString, variables)).map(valueTypeConverter) .orElse(fallbackValue); diff --git a/src/main/java/org/apache/ibatis/parsing/PropertyParser.java b/src/main/java/org/apache/ibatis/parsing/PropertyParser.java index 1f36d91714d..57301667ea7 100644 --- a/src/main/java/org/apache/ibatis/parsing/PropertyParser.java +++ b/src/main/java/org/apache/ibatis/parsing/PropertyParser.java @@ -88,9 +88,8 @@ public String handleToken(String content) { return variables.getProperty(key, defaultValue); } } - String value = variables.getProperty(key); - if (value != null) { - return value; + if (variables.containsKey(key)) { + return variables.getProperty(key); } } return "${" + content + "}"; From 6dd9c12587bd02c7476afe8f949bb6ce3c375f54 Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Sun, 11 Jun 2023 12:50:25 +0900 Subject: [PATCH 3/3] Update docs for gh-2886 --- src/site/es/xdoc/java-api.xml | 2 +- src/site/ja/xdoc/java-api.xml | 2 +- src/site/ko/xdoc/java-api.xml | 1 + src/site/markdown/java-api.md | 42 +++++++++++++++++------------------ src/site/zh/xdoc/java-api.xml | 2 +- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/site/es/xdoc/java-api.xml b/src/site/es/xdoc/java-api.xml index cf5836a57a4..76fcdab8263 100644 --- a/src/site/es/xdoc/java-api.xml +++ b/src/site/es/xdoc/java-api.xml @@ -418,7 +418,7 @@ void rollback(boolean force) Esta anotación proporciona acceso a un gran conjunto de opciones de configuración que normalmente aparecen como atributos en los mapped statements. En lugar de complicar cada anotación existente la anotación Options proporciona una forma sencilla y concisa de acceder a estas opciones. - Atributos: useCache=true, flushCache=FlushCachePolicy.DEFAULT, resultSetType=DEFAULT, statementType=PREPARED, fetchSize=-1, timeout=-1, useGeneratedKeys=false, keyProperty=“”, keyColumn=“”, resultSets=“”, databaseId="". + Atributos: useCache=true, flushCache=FlushCachePolicy.DEFAULT, resultSetType=DEFAULT, statementType=PREPARED, fetchSize=-1, timeout=-1, timeoutString="", useGeneratedKeys=false, keyProperty=“”, keyColumn=“”, resultSets=“”, databaseId="". Es importante comprender que las anotaciones en Java no permiten indicar un valor nulo. Por lo tanto, cuando usas la anotación Options el statement usará todos los valores por defecto. Presta atención a estos valores pro defecto para evitar comportamientos inesperados. diff --git a/src/site/ja/xdoc/java-api.xml b/src/site/ja/xdoc/java-api.xml index e019d3bdfbb..91a9dc589af 100644 --- a/src/site/ja/xdoc/java-api.xml +++ b/src/site/ja/xdoc/java-api.xml @@ -430,7 +430,7 @@ void rollback(boolean force) マップドステートメントの属性 このアノテーションを使うと、通常マップドステートメントの属性として指定される多様なスイッチや設定オプションにアクセスすることができます。Options アノテーションによって、各ステートメントのアノテーションを複雑化することなく、一貫したクリーンな方法で設定にアクセスできるよう工夫されています。キー: Attributes: useCache=true, flushCache=FlushCachePolicy.DEFAULT, resultSetType=DEFAULT, - statementType=PREPARED, fetchSize=-1, timeout=-1, + statementType=PREPARED, fetchSize=-1, timeout=-1, timeoutString="", useGeneratedKeys=false, keyProperty="", keyColumn="", resultSets="", databaseId="". Java アノテーションを使う場合、値として null を指定することはできないという制限があります。これはどういうことかというと、Options アノテーションを付加したステートメントにはデフォルトのオプションが適用されるということです。予期しない動作を防ぐため、各オプションのデフォルト値を把握しておくようにしてください。 diff --git a/src/site/ko/xdoc/java-api.xml b/src/site/ko/xdoc/java-api.xml index 647c465c939..947028e23cb 100644 --- a/src/site/ko/xdoc/java-api.xml +++ b/src/site/ko/xdoc/java-api.xml @@ -541,6 +541,7 @@ void rollback(boolean force) statementType=PREPARED, fetchSize=-1, timeout=-1, + timeoutString="", useGeneratedKeys=false, keyProperty=“”, keyColumn=“”, diff --git a/src/site/markdown/java-api.md b/src/site/markdown/java-api.md index 87e01e7249b..b13099308ef 100644 --- a/src/site/markdown/java-api.md +++ b/src/site/markdown/java-api.md @@ -391,28 +391,28 @@ Since the very beginning, MyBatis has been an XML driven framework. The configur **The annotations are as follows:** -| Annotation | Target | XML equivalent | Description | -|-------------------------------------------------------------------------------------------------------------------|-------------|---------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `@CacheNamespace` | `Class` | `` | Configures the cache for the given namespace (i.e. class). Attributes: `implementation`, `eviction`, `flushInterval`, `size`, `readWrite`, `blocking`, `properties`. | -| `@Property` | N/A | `` | Specifies the property value or placeholder(can replace by configuration properties that defined at the `mybatis-config.xml`). Attributes: `name`, `value`. (Available on MyBatis 3.4.2+) | -| `@CacheNamespaceRef` | `Class` | `` | References the cache of another namespace to use. Note that caches declared in an XML mapper file are considered a separate namespace, even if they share the same FQCN. Attributes: `value` and `name`. If you use this annotation, you should be specified either `value` or `name` attribute. For the `value` attribute specify a java type indicating the namespace(the namespace name become a FQCN of specified java type), and for the `name` attribute(this attribute is available since 3.4.2) specify a name indicating the namespace. | -| `@ConstructorArgs` | `Method` | `` | Collects a group of results to be passed to a result object constructor. Attributes: `value`, which is an array of `Arg`s. | -| `@Arg` | N/A |
  • ``
  • ``
| A single constructor argument that is part of a ConstructorArgs collection. Attributes: `id`, `column`, `javaType`, `jdbcType`, `typeHandler`, `select`, `resultMap`. The id attribute is a boolean value that identifies the property to be used for comparisons, similar to the `` XML element. Since 3.5.4, it can be used as repeatable annotation. | -| `@TypeDiscriminator` | `Method` | `` | A group of value cases that can be used to determine the result mapping to perform. Attributes: `column`, `javaType`, `jdbcType`, `typeHandler`, `cases`. The cases attribute is an array of `Case`s. | -| `@Case` | N/A | `` | A single case of a value and its corresponding mappings. Attributes: `value`, `type`, `results`. The results attribute is an array of Results, thus this `Case` Annotation is similar to an actual `ResultMap`, specified by the `Results` annotation below. | -| `@Results` | `Method` | `` | A list of Result mappings that contain details of how a particular result column is mapped to a property or field. Attributes: `value`, `id`. The value attribute is an array of `Result` annotations. The id attribute is the name of the result mapping. | -| `@Result` | N/A |
  • ``
  • ``
| A single result mapping between a column and a property or field. Attributes: `id`, `column`, `property`, `javaType`, `jdbcType`, `typeHandler`, `one`, `many`. The id attribute is a boolean value that indicates that the property should be used for comparisons (similar to `` in the XML mappings). The one attribute is for single associations, similar to ``, and the many attribute is for collections, similar to ``. They are named as they are to avoid class naming conflicts. Since 3.5.4, it can be used as repeatable annotation. | -| `@One` | N/A | `` | A mapping to a single property value of a complex type. Attributes: `select`, which is the fully qualified name of a mapped statement (i.e. mapper method) that can load an instance of the appropriate type. `fetchType`, which supersedes the global configuration parameter `lazyLoadingEnabled` for this mapping. `resultMap`(available since 3.5.5), which is the fully qualified name of a result map that map to a single container object from select result. `columnPrefix`(available since 3.5.5), which is column prefix for grouping select columns at nested result map. NOTE You will notice that join mapping is not supported via the Annotations API. This is due to the limitation in Java Annotations that does not allow for circular references. | -| `@Many` | N/A | `` | A mapping to a collection property of a complex type. Attributes: `select`, which is the fully qualified name of a mapped statement (i.e. mapper method) that can load a collection of instances of the appropriate types. `fetchType`, which supersedes the global configuration parameter `lazyLoadingEnabled` for this mapping. `resultMap`(available since 3.5.5), which is the fully qualified name of a result map that map to collection object from select result. `columnPrefix`(available since 3.5.5), which is column prefix for grouping select columns at nested result map. NOTE You will notice that join mapping is not supported via the Annotations API. This is due to the limitation in Java Annotations that does not allow for circular references. | -| `@MapKey` | `Method` | | This is used on methods which return type is a Map. It is used to convert a List of result objects as a Map based on a property of those objects. Attributes: `value`, which is a property used as the key of the map. | -| `@Options` | `Method` | Attributes of mapped statements. | This annotation provides access to the wide range of switches and configuration options that are normally present on the mapped statement as attributes. Rather than complicate each statement annotation, the `Options` annotation provides a consistent and clear way to access these. Attributes: `useCache=true`, `flushCache=FlushCachePolicy.DEFAULT`, `resultSetType=DEFAULT`, `statementType=PREPARED`, `fetchSize=-1`, `timeout=-1`, `useGeneratedKeys=false`, `keyProperty=""`, `keyColumn=""`, `resultSets=""` and `databaseId=""`. It's important to understand that with Java Annotations, there is no way to specify `null` as a value. Therefore, once you engage the `Options` annotation, your statement is subject to all of the default values. Pay attention to what the default values are to avoid unexpected behavior. The `databaseId`(Available since 3.5.5), in case there is a configured `DatabaseIdProvider`, the MyBatis use the `Options` with no `databaseId` attribute or with a `databaseId` that matches the current one. If found with and without the `databaseId` the latter will be discarded.

Note that `keyColumn` is only required in certain databases (like Oracle and PostgreSQL). See the discussion about `keyColumn` and `keyProperty` above in the discussion of the insert statement for more information about allowable values in these attributes. | -|
  • `@Insert`
  • `@Update`
  • `@Delete`
  • `@Select`
| `Method` |
  • ``
  • ``
  • ``
  • ``
| Each of these annotations represents the actual SQL that is to be executed. They each take an array of strings (or a single string will do). If an array of strings is passed, they are concatenated with a single space between each to separate them. This helps avoid the "missing space" problem when building SQL in Java code. However, you're also welcome to concatenate together a single string if you like. Attributes: `value`, which is the array of Strings to form the single SQL statement. The `databaseId`(Available since 3.5.5), in case there is a configured `DatabaseIdProvider`, the MyBatis use a statement with no `databaseId` attribute or with a `databaseId` that matches the current one. If found with and without the `databaseId` the latter will be discarded. | |
  • `@InsertProvider`
  • `@UpdateProvider`
  • `@DeleteProvider`
  • `@SelectProvider`
| `Method` |
  • ``
  • ``
  • ``
  • `` element, then no other annotation is necessary. In other cases, use this annotation. For example, if a @Select annotated method will use a result handler, the return type must be void and this annotation (or @ResultMap) is required. This annotation is ignored unless the method return type is void. | -| `@Flush` | `Method` | N/A | If this annotation is used, it can be called the `SqlSession#flushStatements()` via method defined at a Mapper interface.(MyBatis 3.3 or above) | +| `@Param` | `Parameter` | N/A | If your mapper method takes multiple parameters, this annotation can be applied to a mapper method parameter to give each of them a name. Otherwise, multiple parameters will be named by their position prefixed with "param" (not including any `RowBounds` parameters). For example `#{param1}`, `#{param2}` etc. is the default. With `@Param("person")`, the parameter would be named `#{person}`. | +| `@SelectKey` | `Method` | `` | This annotation duplicates the `` functionality for methods annotated with `@Insert`, `@InsertProvider`, `@Update`, or `@UpdateProvider`. It is ignored for other methods. If you specify a `@SelectKey` annotation, then MyBatis will ignore any generated key properties set via the `@Options` annotation, or configuration properties. Attributes: `statement` an array of strings which is the SQL statement to execute, `keyProperty` which is the property of the parameter object that will be updated with the new value, `before` which must be either `true` or `false` to denote if the SQL statement should be executed before or after the insert, `resultType` which is the Java type of the `keyProperty`, and `statementType` is a type of the statement that is any one of `STATEMENT`, `PREPARED` or `CALLABLE` that is mapped to `Statement`, `PreparedStatement` and `CallableStatement` respectively. The default is `PREPARED`. The `databaseId`(Available since 3.5.5), in case there is a configured `DatabaseIdProvider`, the MyBatis will use a statement with no `databaseId` attribute or with a `databaseId` that matches the current one. If found with and without the `databaseId` the latter will be discarded. | +| `@ResultMap` | `Method` | N/A | This annotation is used to provide the id of a `` element in an XML mapper to a `@Select` or `@SelectProvider` annotation. This allows annotated selects to reuse resultmaps that are defined in XML. This annotation will override any `@Results` or `@ConstructorArgs` annotation if both are specified on an annotated select. | +| `@ResultType` | `Method` | N/A | This annotation is used when using a result handler. In that case, the return type is void so MyBatis must have a way to determine the type of object to construct for each row. If there is an XML result map, use the @ResultMap annotation. If the result type is specified in XML on the `