From 571af93350bf473bdf3f7ec6fe6cf9a5243e18d3 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Mon, 6 Jan 2025 05:31:09 +0900 Subject: [PATCH] Support nested cursor By specifying a special name NESTED_CURSOR to `resultSet` attribute, Should fix #566 --- .../resultset/DefaultResultSetHandler.java | 35 +++- .../ibatis/mapping/MappedStatement.java | 8 +- .../apache/ibatis/mapping/ResultMapping.java | 10 +- src/site/es/xdoc/sqlmap-xml.xml | 48 ++++++ src/site/ja/xdoc/sqlmap-xml.xml | 48 ++++++ src/site/ko/xdoc/sqlmap-xml.xml | 150 ++++++++++++++++++ src/site/markdown/sqlmap-xml.md | 48 ++++++ src/site/zh_CN/xdoc/sqlmap-xml.xml | 48 ++++++ .../ibatis/mapping/MappedStatementTest.java | 58 +++++++ .../ibatis/mapping/ResultMappingTest.java | 24 ++- .../Author.java | 8 +- .../submitted/oracle_cursor/Author2.java | 84 ++++++++++ .../Book.java | 2 +- .../ibatis/submitted/oracle_cursor/Book2.java | 82 ++++++++++ .../Mapper.java | 12 +- .../OracleCursorTest.java} | 50 +++++- .../CreateDB.sql | 0 .../ibatis/submitted/oracle_cursor/Mapper.xml | 130 +++++++++++++++ .../oracle_implicit_cursor/Mapper.xml | 67 -------- 19 files changed, 829 insertions(+), 83 deletions(-) create mode 100644 src/test/java/org/apache/ibatis/mapping/MappedStatementTest.java rename src/test/java/org/apache/ibatis/submitted/{oracle_implicit_cursor => oracle_cursor}/Author.java (92%) create mode 100644 src/test/java/org/apache/ibatis/submitted/oracle_cursor/Author2.java rename src/test/java/org/apache/ibatis/submitted/{oracle_implicit_cursor => oracle_cursor}/Book.java (96%) create mode 100644 src/test/java/org/apache/ibatis/submitted/oracle_cursor/Book2.java rename src/test/java/org/apache/ibatis/submitted/{oracle_implicit_cursor => oracle_cursor}/Mapper.java (74%) rename src/test/java/org/apache/ibatis/submitted/{oracle_implicit_cursor/OracleImplicitCursorTest.java => oracle_cursor/OracleCursorTest.java} (61%) rename src/test/resources/org/apache/ibatis/submitted/{oracle_implicit_cursor => oracle_cursor}/CreateDB.sql (100%) create mode 100644 src/test/resources/org/apache/ibatis/submitted/oracle_cursor/Mapper.xml delete mode 100644 src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.xml diff --git a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java index 803ddfaca96..9b89d42de8c 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -569,8 +569,20 @@ private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix); } if (propertyMapping.getResultSet() != null) { - addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK? - return DEFERRED; + if (ResultMapping.NESTED_CURSOR.equals(propertyMapping.getResultSet())) { + final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); + ResultMap nestedResultMap = resolveDiscriminatedResultMap(rs, + configuration.getResultMap(propertyMapping.getNestedResultMapId()), + getColumnPrefix(columnPrefix, propertyMapping)); + ResultSetWrapper rsw = new ResultSetWrapper(rs.getObject(column, ResultSet.class), configuration); + List results = new ArrayList<>(); + handleResultSet(rsw, nestedResultMap, results, null); + linkObjects(metaResultObject, propertyMapping, results.get(0), true); + return metaResultObject.getValue(propertyMapping.getProperty()); + } else { + addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK? + return DEFERRED; + } } else { final TypeHandler typeHandler = propertyMapping.getTypeHandler(); final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); @@ -1527,10 +1539,19 @@ private void createRowKeyForMap(ResultSetWrapper rsw, CacheKey cacheKey) throws } private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) { + linkObjects(metaObject, resultMapping, rowValue, false); + } + + private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue, + boolean isNestedCursorResult) { final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); if (collectionProperty != null) { final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty); - targetMetaObject.add(rowValue); + if (isNestedCursorResult) { + targetMetaObject.addAll((List) rowValue); + } else { + targetMetaObject.add(rowValue); + } // it is possible for pending creations to get set via property mappings, // keep track of these, so we can rebuild them. @@ -1543,10 +1564,16 @@ private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Obj pendingPccRelations.put(originalObject, pendingRelation); } } else { - metaObject.setValue(resultMapping.getProperty(), rowValue); + metaObject.setValue(resultMapping.getProperty(), + isNestedCursorResult ? toSingleObj((List) rowValue) : rowValue); } } + private Object toSingleObj(List list) { + // Even if there are multiple elements, silently returns the first one. + return list.isEmpty() ? null : list.get(0); + } + private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) { final String propertyName = resultMapping.getProperty(); Object propertyValue = metaObject.getValue(propertyName); diff --git a/src/main/java/org/apache/ibatis/mapping/MappedStatement.java b/src/main/java/org/apache/ibatis/mapping/MappedStatement.java index 9b78717249a..6762669fff1 100644 --- a/src/main/java/org/apache/ibatis/mapping/MappedStatement.java +++ b/src/main/java/org/apache/ibatis/mapping/MappedStatement.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 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. @@ -16,6 +16,7 @@ package org.apache.ibatis.mapping; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -203,6 +204,11 @@ public MappedStatement build() { assert mappedStatement.id != null; assert mappedStatement.sqlSource != null; assert mappedStatement.lang != null; + if (mappedStatement.resultSets != null + && Arrays.asList(mappedStatement.resultSets).contains(ResultMapping.NESTED_CURSOR)) { + throw new IllegalStateException( + "Result set name '" + ResultMapping.NESTED_CURSOR + "' is reserved, please assign another name."); + } mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps); return mappedStatement; } diff --git a/src/main/java/org/apache/ibatis/mapping/ResultMapping.java b/src/main/java/org/apache/ibatis/mapping/ResultMapping.java index 9ec710679ca..7b1db7d22ae 100644 --- a/src/main/java/org/apache/ibatis/mapping/ResultMapping.java +++ b/src/main/java/org/apache/ibatis/mapping/ResultMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 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,6 +30,11 @@ */ public class ResultMapping { + /** + * Reserved result set name indicating a nested cursor is mapped to this property. + */ + public static final String NESTED_CURSOR = "NESTED_CURSOR"; + private Configuration configuration; private String property; private String column; @@ -166,7 +171,8 @@ private void validate() { if (resultMapping.foreignColumn != null) { numForeignColumns = resultMapping.foreignColumn.split(",").length; } - if (numColumns != numForeignColumns) { + if (numColumns != numForeignColumns && !NESTED_CURSOR.equals(resultMapping.resultSet)) { + // Nested cursor does not use 'foreignKey' throw new IllegalStateException( "There should be the same number of columns and foreignColumns in property " + resultMapping.property); } diff --git a/src/site/es/xdoc/sqlmap-xml.xml b/src/site/es/xdoc/sqlmap-xml.xml index 72716cd4493..83d75083545 100644 --- a/src/site/es/xdoc/sqlmap-xml.xml +++ b/src/site/es/xdoc/sqlmap-xml.xml @@ -1428,6 +1428,31 @@ When using this functionality, it is preferable for the entire mapping hierarchy columnPrefix="co_" /> ]]> + +

Nested Cursor for Association

+ +

Some databases can return java.sql.ResultSet as a column value.
+Here is the statement and result map.

+ + + + + + + + + + +]]> + +

Compared to the examples in the previous section, the key difference is the resultSet attribute in the <association> element.
+Its value NESTED_CURSOR indicates that the value of the column author is nested cursor.

+ +

ResultSets múltiples en Association

@@ -1601,6 +1626,29 @@ SELECT * FROM AUTHOR WHERE ID = #{id}]]> ]]> + +

Nested Cursor for Collection

+ +

It might be obvious, but nested cursor can return multiple rows.
+Just like <association>, you just need to specify resultSet="NESTED_CURSOR" in the <collection> element.

+ + + + + + + + + + + +]]> + +

ResultSets múltiples en Collection

diff --git a/src/site/ja/xdoc/sqlmap-xml.xml b/src/site/ja/xdoc/sqlmap-xml.xml index 268f6354973..ad29df732c4 100644 --- a/src/site/ja/xdoc/sqlmap-xml.xml +++ b/src/site/ja/xdoc/sqlmap-xml.xml @@ -1608,6 +1608,31 @@ User{username=Peter, roles=[Users, Maintainers, Approvers]} columnPrefix="co_" /> ]]> + +

ネストされたカーソルを association にマッピングする

+ +

データベースによっては列の値として java.sql.ResultSet 返すことができます。
+このような結果をマッピングする例を説明します。

+ + + + + + + + + + +]]> + +

上の章の例との重要な違いは、<association> 要素の resultSet 属性に特別な値 NESTED_CURSOR を指定している点です。
+これによって、author 列の値をネストされたカーソルとしてマッピングすることができます。

+ +

複数の ResultSet を association にマッピングする

@@ -1789,6 +1814,29 @@ SELECT * FROM AUTHOR WHERE ID = #{id}]]> ]]> + +

ネストされたカーソルを collection にマッピングする

+ +

当然ですが、ネストされたカーソルが複数の値を返す場合もあります。
+先に説明した <association> の場合と同様、 <collection> 要素に resultSet="NESTED_CURSOR" を指定してください。

+ + + + + + + + + + + +]]> + +

複数の ResultSets を collection にマッピングする

diff --git a/src/site/ko/xdoc/sqlmap-xml.xml b/src/site/ko/xdoc/sqlmap-xml.xml index 0a49cddf849..b5aa79cd8c7 100644 --- a/src/site/ko/xdoc/sqlmap-xml.xml +++ b/src/site/ko/xdoc/sqlmap-xml.xml @@ -1455,6 +1455,102 @@ When using this functionality, it is preferable for the entire mapping hierarchy columnPrefix="co_" /> ]]> + +

Nested Cursor for Association

+ +

Some databases can return java.sql.ResultSet as a column value.
+Here is the statement and result map.

+ + + + + + + + + + +]]> + +

Compared to the examples in the previous section, the key difference is the resultSet attribute in the <association> element.
+Its value NESTED_CURSOR indicates that the value of the column author is nested cursor.

+ + +

Multiple ResultSets for Association

+ +
+ + + + + + + + + + + + + + + + + + + + +
AttributeDescription
column + When using multiple resultset this attribute specifies the columns (separated by commas) that will be correlated + with the foreignColumn to identify the parent and the child of a relationship. +
foreignColumn + Identifies the name of the columns that contains the foreign keys which values will be matched against the + values of the columns specified in the column attibute of the parent type. +
resultSet + Identifies the name of the result set where this complex type will be loaded from. +
+ +

Starting from version 3.2.3 MyBatis provides yet another way to solve the N+1 problem.

+ +

Some databases allow stored procedures to return more than one resultset or execute more than one statement + at once and return a resultset per each one. This can be used to hit the database just once + and return related data without using a join.

+ +

In the example, the stored procedure executes the following queries and returns two result sets. + The first will contain Blogs and the second Authors.

+ + + +

A name must be given to each result set by adding a + resultSets attribute to the mapped statement with a list of names separated by commas.

+ + + {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})} + +]]> + +

+ Now we can specify that the data to fill the "author" association comes in the "authors" result set: +

+ + + + + + + + + + + +]]> + +

지금까지 “has one” 관계를 다루는 방법을 보았다. 하지만 “has many” 는 어떻게 처리할까? 그건 다음 섹션에서 다루어보자.

@@ -1562,6 +1658,60 @@ When using this functionality, it is preferable for the entire mapping hierarchy ]]> + +

Nested Cursor for Collection

+ +

It might be obvious, but nested cursor can return multiple rows.
+Just like <association>, you just need to specify resultSet="NESTED_CURSOR" in the <collection> element.

+ + + + + + + + + + + +]]> + + +

Multiple ResultSets for Collection

+ +

+ As we did for the association, we can call a stored procedure that executes two queries and returns two result sets, one with Blogs + and another with Posts: +

+ + + +

A name must be given to each result set by adding a + resultSets attribute to the mapped statement with a list of names separated by commas.

+ + + {call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})} +]]> + +

We specify that the "posts" collection will be filled out of data contained in the result set named "posts":

+ + + + + + + + + +]]> + +

참고 associations과 collections에서 내포의 단계 혹은 조합에는 제한이 없다. 매핑할때는 성능을 생각해야 한다. 단위테스트와 성능테스트는 애플리케이션에서 가장 좋은 방법을 찾도록 지속해야 한다. diff --git a/src/site/markdown/sqlmap-xml.md b/src/site/markdown/sqlmap-xml.md index cf496a8fcdb..a9c0bc426d0 100644 --- a/src/site/markdown/sqlmap-xml.md +++ b/src/site/markdown/sqlmap-xml.md @@ -939,6 +939,31 @@ Because the column names in the results differ from the columns defined in the r ``` +#### Nested Cursor for Association + +Some databases can return `java.sql.ResultSet` as a column value. +Here is the statement and result map. + +```xml + + + + + + + + + + +``` + +Compared to the examples in the previous section, the key difference is the `resultSet` attribute in the `` element. +Its value `NESTED_CURSOR` indicates that the value of the column `author` is nested cursor. + #### Multiple ResultSets for Association | Attribute | Description | @@ -1091,6 +1116,29 @@ Also, if you prefer the longer form that allows for more reusability of your res ``` +#### Nested Cursor for Collection + +It might be obvious, but nested cursor can return multiple rows. +Just like ``, you just need to specify `resultSet="NESTED_CURSOR"` in the `` element. + +```xml + + + + + + + + + + + +``` + #### Multiple ResultSets for Collection As we did for the association, we can call a stored procedure that executes two queries and returns two result sets, one with Blogs and another with Posts: diff --git a/src/site/zh_CN/xdoc/sqlmap-xml.xml b/src/site/zh_CN/xdoc/sqlmap-xml.xml index 6eec172cc19..584ab62d1cb 100644 --- a/src/site/zh_CN/xdoc/sqlmap-xml.xml +++ b/src/site/zh_CN/xdoc/sqlmap-xml.xml @@ -1660,6 +1660,31 @@ When using this functionality, it is preferable for the entire mapping hierarchy columnPrefix="co_" /> ]]> + +

Nested Cursor for Association

+ +

Some databases can return java.sql.ResultSet as a column value.
+Here is the statement and result map.

+ + + + + + + + + + +]]> + +

Compared to the examples in the previous section, the key difference is the resultSet attribute in the <association> element.
+Its value NESTED_CURSOR indicates that the value of the column author is nested cursor.

+ +

关联的多结果集(ResultSet)

@@ -1850,6 +1875,29 @@ SELECT * FROM AUTHOR WHERE ID = #{id}]]> ]]> + +

Nested Cursor for Collection

+ +

It might be obvious, but nested cursor can return multiple rows.
+Just like <association>, you just need to specify resultSet="NESTED_CURSOR" in the <collection> element.

+ + + + + + + + + + + +]]> + +

集合的多结果集(ResultSet)

diff --git a/src/test/java/org/apache/ibatis/mapping/MappedStatementTest.java b/src/test/java/org/apache/ibatis/mapping/MappedStatementTest.java new file mode 100644 index 00000000000..1a864887afb --- /dev/null +++ b/src/test/java/org/apache/ibatis/mapping/MappedStatementTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2009-2025 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 + * + * https://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.mapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.HashMap; +import java.util.List; + +import org.apache.ibatis.builder.StaticSqlSource; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.type.TypeHandlerRegistry; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class MappedStatementTest { + + @CsvSource(value = { "aRS|true", "nested_cursor|true", "aRS," + ResultMapping.NESTED_CURSOR + "|false", + ResultMapping.NESTED_CURSOR + ",aRS|false" }, delimiter = '|') + @ParameterizedTest + void shouldRejectReservedResultSetName(String resultSets, boolean shouldPass) { + final Configuration config = new Configuration(); + final TypeHandlerRegistry registry = config.getTypeHandlerRegistry(); + + try { + new MappedStatement.Builder(config, "select", new StaticSqlSource(config, "select 1"), SqlCommandType.SELECT) + .resultMaps(List.of(new ResultMap.Builder(config, "authorRM", HashMap.class, + List.of(new ResultMapping.Builder(config, "id", "id", registry.getTypeHandler(Integer.class)).build())) + .build())) + .resultSets(resultSets).build(); + if (!shouldPass) { + fail("Reserved result set name should be rejected."); + } + } catch (IllegalStateException e) { + if (shouldPass) { + fail("Non-reserved result set names should not be rejected."); + } + assertEquals("Result set name '" + ResultMapping.NESTED_CURSOR + "' is reserved, please assign another name.", + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/apache/ibatis/mapping/ResultMappingTest.java b/src/test/java/org/apache/ibatis/mapping/ResultMappingTest.java index 7243989146b..08fca9b258e 100644 --- a/src/test/java/org/apache/ibatis/mapping/ResultMappingTest.java +++ b/src/test/java/org/apache/ibatis/mapping/ResultMappingTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 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,10 @@ */ package org.apache.ibatis.mapping; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -30,15 +34,29 @@ class ResultMappingTest { // Issue 697: Association with both a resultMap and a select attribute should throw exception @Test void shouldThrowErrorWhenBothResultMapAndNestedSelectAreSet() { - Assertions.assertThrows(IllegalStateException.class, () -> new ResultMapping.Builder(configuration, "prop") + assertThrows(IllegalStateException.class, () -> new ResultMapping.Builder(configuration, "prop") .nestedQueryId("nested query ID").nestedResultMapId("nested resultMap").build()); } // Issue 4: column is mandatory on nested queries @Test void shouldFailWithAMissingColumnInNetstedSelect() { - Assertions.assertThrows(IllegalStateException.class, + assertThrows(IllegalStateException.class, () -> new ResultMapping.Builder(configuration, "prop").nestedQueryId("nested query ID").build()); } + @Test + void shouldFailIfSizeOfColumnsAndForeignColumnsDontMatch() { + IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, + () -> new ResultMapping.Builder(configuration, "books").resultSet("bookRS").column("id,x") + .foreignColumn("author_id").nestedResultMapId("bookRM").build()); + assertEquals("There should be the same number of columns and foreignColumns in property books", ex.getMessage()); + } + + @Test + void shouldNestedCursorNotRequireForeignColumns() { + assertNotNull(new ResultMapping.Builder(configuration, "books").resultSet(ResultMapping.NESTED_CURSOR) + .nestedResultMapId("bookRM").column("books").build()); + } + } diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Author.java b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Author.java similarity index 92% rename from src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Author.java rename to src/test/java/org/apache/ibatis/submitted/oracle_cursor/Author.java index ccb73472c5b..ceab3fc8d82 100644 --- a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Author.java +++ b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Author.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.apache.ibatis.submitted.oracle_implicit_cursor; +package org.apache.ibatis.submitted.oracle_cursor; import java.util.List; import java.util.Objects; @@ -28,6 +28,12 @@ public Author() { super(); } + public Author(Integer id, String name) { + super(); + this.id = id; + this.name = name; + } + public Author(Integer id, String name, List books) { super(); this.id = id; diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Author2.java b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Author2.java new file mode 100644 index 00000000000..ee842fd88a1 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Author2.java @@ -0,0 +1,84 @@ +/* + * Copyright 2009-2025 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 + * + * https://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.oracle_cursor; + +import java.util.List; +import java.util.Objects; + +public class Author2 { + private Integer id; + private String name; + private List bookNames; + + public Author2() { + super(); + } + + public Author2(Integer id, String name, List bookNames) { + super(); + this.id = id; + this.name = name; + this.bookNames = bookNames; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getBookNames() { + return bookNames; + } + + public void setBookNames(List bookNames) { + this.bookNames = bookNames; + } + + @Override + public int hashCode() { + return Objects.hash(bookNames, id, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Author2)) { + return false; + } + Author2 other = (Author2) obj; + return Objects.equals(bookNames, other.bookNames) && Objects.equals(id, other.id) + && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Author2 [id=" + id + ", name=" + name + ", bookNames=" + bookNames + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Book.java b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Book.java similarity index 96% rename from src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Book.java rename to src/test/java/org/apache/ibatis/submitted/oracle_cursor/Book.java index b031054602a..43b19b01e02 100644 --- a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Book.java +++ b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Book.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.apache.ibatis.submitted.oracle_implicit_cursor; +package org.apache.ibatis.submitted.oracle_cursor; import java.util.Objects; diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Book2.java b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Book2.java new file mode 100644 index 00000000000..7dae3cf2f91 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Book2.java @@ -0,0 +1,82 @@ +/* + * Copyright 2009-2025 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 + * + * https://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.oracle_cursor; + +import java.util.Objects; + +public class Book2 { + private Integer id; + private String name; + private Author author; + + public Book2() { + super(); + } + + public Book2(Integer id, String name, Author author) { + super(); + this.id = id; + this.name = name; + this.author = author; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Author getAuthor() { + return author; + } + + public void setAuthor(Author author) { + this.author = author; + } + + @Override + public int hashCode() { + return Objects.hash(author, id, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Book2)) { + return false; + } + Book2 other = (Book2) obj; + return Objects.equals(author, other.author) && Objects.equals(id, other.id) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Book2 [id=" + id + ", name=" + name + ", author=" + author + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.java b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Mapper.java similarity index 74% rename from src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.java rename to src/test/java/org/apache/ibatis/submitted/oracle_cursor/Mapper.java index db860a95c7d..3abf30c5bd4 100644 --- a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.java +++ b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/Mapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.ibatis.submitted.oracle_implicit_cursor; +package org.apache.ibatis.submitted.oracle_cursor; import java.util.List; @@ -25,4 +25,14 @@ public interface Mapper { List selectImplicitCursors_Callable(); + List selectNestedCursor_Statement(); + + List selectNestedCursor_Prepared(); + + List selectNestedCursor_Callable(); + + List selectNestedCursorOfStrings(); + + List selectNestedCursorAssociation(); + } diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/OracleImplicitCursorTest.java b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/OracleCursorTest.java similarity index 61% rename from src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/OracleImplicitCursorTest.java rename to src/test/java/org/apache/ibatis/submitted/oracle_cursor/OracleCursorTest.java index 5459a93033d..8292b250d59 100644 --- a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/OracleImplicitCursorTest.java +++ b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/OracleCursorTest.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.ibatis.submitted.oracle_implicit_cursor; +package org.apache.ibatis.submitted.oracle_cursor; import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import java.util.function.Function; @@ -33,7 +36,7 @@ import org.junit.jupiter.api.Test; @Tag("TestcontainersTests") -class OracleImplicitCursorTest { +class OracleCursorTest { private static SqlSessionFactory sqlSessionFactory; @@ -47,7 +50,7 @@ static void setUp() throws Exception { sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), - "org/apache/ibatis/submitted/oracle_implicit_cursor/CreateDB.sql"); + "org/apache/ibatis/submitted/oracle_cursor/CreateDB.sql"); } @Test @@ -65,7 +68,26 @@ void shouldImplicitCursors_Callable() { doTest(Mapper::selectImplicitCursors_Callable); } + @Test + void nestedCursors_Statement() { + doTest(Mapper::selectNestedCursor_Statement, LinkedList.class); + } + + @Test + void nestedCursors_Prepared() { + doTest(Mapper::selectNestedCursor_Prepared, LinkedList.class); + } + + @Test + void nestedCursors_Callable() { + doTest(Mapper::selectNestedCursor_Callable, LinkedList.class); + } + private void doTest(Function> query) { + doTest(query, ArrayList.class); + } + + private void doTest(Function> query, Class expectedBooksType) { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Mapper mapper = sqlSession.getMapper(Mapper.class); List authors = query.apply(mapper); @@ -73,6 +95,28 @@ private void doTest(Function> query) { List.of(new Author(1, "John", List.of(new Book(1, "C#"), new Book(2, "Python"), new Book(5, "Ruby"))), new Author(2, "Jane", List.of(new Book(3, "SQL"), new Book(4, "Java")))), authors); + assertSame(expectedBooksType, authors.get(0).getBooks().getClass()); + } + } + + @Test + void nestedCursorOfStrings() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + List authors = mapper.selectNestedCursorOfStrings(); + assertIterableEquals(List.of(new Author2(1, "John", List.of("C#", "Python", "Ruby")), + new Author2(2, "Jane", List.of("SQL", "Java"))), authors); + } + } + + @Test + void nestedCursorAssociation() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + List books = mapper.selectNestedCursorAssociation(); + assertIterableEquals(List.of(new Book2(1, "C#", new Author(1, "John")), + new Book2(2, "Python", new Author(1, "John")), new Book2(3, "SQL", new Author(2, "Jane")), + new Book2(4, "Java", new Author(2, "Jane")), new Book2(5, "Ruby", new Author(1, "John"))), books); } } } diff --git a/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/CreateDB.sql b/src/test/resources/org/apache/ibatis/submitted/oracle_cursor/CreateDB.sql similarity index 100% rename from src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/CreateDB.sql rename to src/test/resources/org/apache/ibatis/submitted/oracle_cursor/CreateDB.sql diff --git a/src/test/resources/org/apache/ibatis/submitted/oracle_cursor/Mapper.xml b/src/test/resources/org/apache/ibatis/submitted/oracle_cursor/Mapper.xml new file mode 100644 index 00000000000..b4a39ae8ce2 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/oracle_cursor/Mapper.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.xml b/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.xml deleted file mode 100644 index f17ef518414..00000000000 --- a/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - -