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..c99ea625be9 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -532,7 +532,7 @@ private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, final List propertyMappings = resultMap.getPropertyResultMappings(); for (ResultMapping propertyMapping : propertyMappings) { String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); - if (propertyMapping.getNestedResultMapId() != null) { + if (propertyMapping.getNestedResultMapId() != null && !JdbcType.CURSOR.equals(propertyMapping.getJdbcType())) { // the user added a column attribute to a nested result map, ignore it column = null; } @@ -568,6 +568,11 @@ private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject if (propertyMapping.getNestedQueryId() != null) { return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix); } + if (JdbcType.CURSOR.equals(propertyMapping.getJdbcType())) { + List results = getNestedCursorValue(rs, propertyMapping, columnPrefix); + linkObjects(metaResultObject, propertyMapping, results.get(0), true); + return metaResultObject.getValue(propertyMapping.getProperty()); + } if (propertyMapping.getResultSet() != null) { addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK? return DEFERRED; @@ -578,6 +583,18 @@ private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject } } + private List getNestedCursorValue(ResultSet rs, ResultMapping propertyMapping, String parentColumnPrefix) + throws SQLException { + final String column = prependPrefix(propertyMapping.getColumn(), parentColumnPrefix); + ResultMap nestedResultMap = resolveDiscriminatedResultMap(rs, + configuration.getResultMap(propertyMapping.getNestedResultMapId()), + getColumnPrefix(parentColumnPrefix, propertyMapping)); + ResultSetWrapper rsw = new ResultSetWrapper(rs.getObject(column, ResultSet.class), configuration); + List results = new ArrayList<>(); + handleResultSet(rsw, nestedResultMap, results, null); + return results; + } + private List createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { final String mapKey = resultMap.getId() + ":" + columnPrefix; @@ -761,6 +778,15 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class resultType try { if (constructorMapping.getNestedQueryId() != null) { value = getNestedQueryConstructorValue(rsw.getResultSet(), constructorMapping, columnPrefix); + } else if (JdbcType.CURSOR.equals(constructorMapping.getJdbcType())) { + List result = (List) getNestedCursorValue(rsw.getResultSet(), constructorMapping, columnPrefix).get(0); + if (objectFactory.isCollection(parameterType)) { + MetaObject collection = configuration.newMetaObject(objectFactory.create(parameterType)); + collection.addAll((List) result); + value = collection.getOriginalObject(); + } else { + value = toSingleObj(result); + } } else if (constructorMapping.getNestedResultMapId() != null) { final String constructorColumnPrefix = getColumnPrefix(columnPrefix, constructorMapping); final ResultMap resultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), @@ -1527,10 +1553,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 +1578,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..413e9f6ddd1 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. diff --git a/src/main/java/org/apache/ibatis/mapping/ResultMap.java b/src/main/java/org/apache/ibatis/mapping/ResultMap.java index 145d053fcc4..7afaa098f7b 100644 --- a/src/main/java/org/apache/ibatis/mapping/ResultMap.java +++ b/src/main/java/org/apache/ibatis/mapping/ResultMap.java @@ -23,6 +23,7 @@ import java.util.Set; import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.type.JdbcType; /** * @author Clinton Begin @@ -85,9 +86,8 @@ public ResultMap build() { for (ResultMapping resultMapping : resultMap.resultMappings) { resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null; - resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps - || resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null; - + resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || resultMapping.getNestedResultMapId() != null + && resultMapping.getResultSet() == null && !JdbcType.CURSOR.equals(resultMapping.getJdbcType()); final String column = resultMapping.getColumn(); if (column != null) { resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH)); diff --git a/src/site/es/xdoc/sqlmap-xml.xml b/src/site/es/xdoc/sqlmap-xml.xml index 72716cd4493..1c84771857d 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 jdbcType attribute in the <association> element.
+Its value 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 jdbcType="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..350193a22b8 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> 要素の jdbcType 属性に CURSOR を指定している点です。
+これによって、author 列の値をネストされたカーソルとしてマッピングすることができます。

+ +

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

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

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

+ +

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

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

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

diff --git a/src/site/ko/xdoc/sqlmap-xml.xml b/src/site/ko/xdoc/sqlmap-xml.xml index 0a49cddf849..6da119fa809 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 jdbcType attribute in the <association> element.
+Its value 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 jdbcType="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..41d6d3ce052 100644 --- a/src/site/markdown/sqlmap-xml.md +++ b/src/site/markdown/sqlmap-xml.md @@ -939,6 +939,32 @@ 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 `jdbcType` attribute in the `` element. +Its value `CURSOR` indicates that the value of the column `author` is nested cursor. + + #### Multiple ResultSets for Association | Attribute | Description | @@ -984,6 +1010,7 @@ Now we can specify that the data to fill the "author" association comes in the " ``` + You've seen above how to deal with a "has one" type association. But what about "has many"? That's the subject of the next section. #### collection @@ -1091,6 +1118,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 `jdbcType="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..61df8d54922 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 jdbcType attribute in the <association> element.
+Its value 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 jdbcType="CURSOR" in the <collection> element.

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

集合的多结果集(ResultSet)

diff --git a/src/test/java/org/apache/ibatis/mapping/ResultMappingTest.java b/src/test/java/org/apache/ibatis/mapping/ResultMappingTest.java index 963b5176107..8b920cb41d7 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,7 +15,12 @@ */ 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.apache.ibatis.type.JdbcType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -30,15 +35,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 shouldFailWithAMissingColumnInNestedSelect() { - 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").jdbcType(JdbcType.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_cursor/BooksTypeHandler.java b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/BooksTypeHandler.java new file mode 100644 index 00000000000..42a107c0426 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/oracle_cursor/BooksTypeHandler.java @@ -0,0 +1,62 @@ +/* + * 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.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +public class BooksTypeHandler extends BaseTypeHandler> { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType) + throws SQLException { + // n/a + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + List list = new ArrayList<>(); + try (ResultSet nestedCursor = rs.getObject(columnName, ResultSet.class)) { + while (nestedCursor.next()) { + Integer id = nestedCursor.getInt("id"); + String name = nestedCursor.getString("name"); + list.add(new Book(id, name)); + } + } + return list; + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + // n/a + return null; + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + // n/a + return null; + } + +} 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 60% 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..ff05d3c2eaa 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,24 @@ public interface Mapper { List selectImplicitCursors_Callable(); + List selectNestedCursor_Statement(); + + List selectNestedCursor_Prepared(); + + List selectNestedCursor_Callable(); + + List selectNestedCursor_Automap(); + + List selectNestedCursorConstructorCollection(); + + List selectNestedCursorTypeHandler(); + + List selectNestedCursorTypeHandlerConstructor(); + + List selectNestedCursorOfStrings(); + + List selectNestedCursorAssociation(); + + List selectNestedCursorConstructorAssociation(); + } 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 53% 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..da3d2237048 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,46 @@ 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); + } + + @Test + void nestedCursors_Automap() { + doTest(Mapper::selectNestedCursor_Automap); + } + + @Test + void nestedCursorsConstructorCollection() { + doTest(Mapper::selectNestedCursorConstructorCollection); + } + + @Test + void nestedCursorsTypeHandler() { + doTest(Mapper::selectNestedCursorTypeHandler); + } + + @Test + void nestedCursorsTypeHandlerConstructor() { + doTest(Mapper::selectNestedCursorTypeHandlerConstructor); + } + 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 +115,37 @@ 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() { + doTestBook2(Mapper::selectNestedCursorAssociation); + } + + @Test + void nestedCursorConstructorAssociation() { + doTestBook2(Mapper::selectNestedCursorConstructorAssociation); + } + + private void doTestBook2(Function> query) { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + List books = query.apply(mapper); + 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..cfd81175282 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/oracle_cursor/Mapper.xml @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - -