diff --git a/org.adempiere.base/src/org/compiere/model/Query.java b/org.adempiere.base/src/org/compiere/model/Query.java index 1cf0bcb392..9ad02eb43e 100644 --- a/org.adempiere.base/src/org/compiere/model/Query.java +++ b/org.adempiere.base/src/org/compiere/model/Query.java @@ -28,7 +28,12 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Properties; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; import java.util.logging.Level; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.adempiere.exceptions.DBException; import org.compiere.util.CLogger; @@ -645,51 +650,83 @@ public class Query } /** - * Return an Iterator implementation to fetch one PO at a time. The implementation first retrieve - * all IDS that match the query criteria and issue sql query to fetch the PO when caller want to - * fetch the next PO. This minimize memory usage but it is slower than the list method. - * @return Iterator + * Return an Iterable implementation that can be used in a for expression. Example: + *
{@code
+	 * Iterable query = new Query(...).iterable();
+	 *
+	 * for (MTable table : query) {
+	 *   // Do stuff with the element
+	 * }
+	 * 
+ * + * @return Iterable * @throws DBException */ - public Iterator iterate() throws DBException + public Iterable iterable() throws DBException { - String[] keys = table.getKeyColumns(); - StringBuilder sqlBuffer = new StringBuilder(" SELECT "); - for (int i = 0; i < keys.length; i++) { - if (i > 0) - sqlBuffer.append(", "); - if (!joinClauseList.isEmpty()) - sqlBuffer.append(table.getTableName()).append("."); - sqlBuffer.append(keys[i]); - } - sqlBuffer.append(" FROM ").append(table.getTableName()); - String sql = buildSQL(sqlBuffer, true); - + return () -> iterate(); + } + + /** + * Return an Stream implementation to fetch one PO at a time. This method will only create POs on-demand and + * they will become eligible for garbage collection once they have been consumed by the stream, so unlike + * {@link #list()} it doesn't have to hold a copy of all the POs in the result set in memory at one time. + * For situations where you need to iterate over a result set and operate on the results one-at-a-time rather + * than operate on the group as a whole, this method is likely to give better performance than list(). + * @return Stream + * @throws DBException + */ + public Stream stream() throws DBException + { + String sql = buildSQL(null, true); + PreparedStatement pstmt = null; ResultSet rs = null; - List idList = new ArrayList(); - try - { + try { pstmt = DB.prepareStatement (sql, trxName); + final PreparedStatement finalPstmt = pstmt; rs = createResultSet(pstmt); - while (rs.next ()) - { - Object[] ids = new Object[keys.length]; - for (int i = 0; i < ids.length; i++) { - ids[i] = rs.getObject(i+1); - } - idList.add(ids); - } + final ResultSet finalRS = rs; + + return StreamSupport.stream(new Spliterators.AbstractSpliterator( + Long.MAX_VALUE,Spliterator.ORDERED) { + @Override + public boolean tryAdvance(Consumer action) { + try { + if(!finalRS.next()) return false; + @SuppressWarnings("unchecked") + final T newRec = (T)table.getPO(finalRS, trxName); + action.accept(newRec); + return true; + } catch(SQLException ex) { + log.log(Level.SEVERE, sql, ex); + throw new DBException(ex, sql); + } + } + }, false).onClose(() -> DB.close(finalRS, finalPstmt)); } catch (SQLException e) { + DB.close(rs, pstmt); log.log(Level.SEVERE, sql, e); throw new DBException(e, sql); - } finally { - DB.close(rs, pstmt); - rs = null; pstmt = null; } - return new POIterator(table, idList, trxName); + } + + /** + * Return an Iterator implementation to fetch one PO at a time. This implementation is equivalent to + * stream().iterator() and has similar performance benefits compared with {@link #list()}. + * Useful if the downstream API requires an Iterator; otherwise the additional + * of the {@link #stream()} interface is likely to be more useful. + * + * @return Iterator + * @throws DBException + * @see #stream() + */ + @SuppressWarnings("unchecked") + public Iterator iterate() throws DBException + { + return (Iterator) stream().iterator(); } /** diff --git a/org.idempiere.p2.targetplatform/maven.locations.xml b/org.idempiere.p2.targetplatform/maven.locations.xml index b2c4e902df..70ae96d341 100644 --- a/org.idempiere.p2.targetplatform/maven.locations.xml +++ b/org.idempiere.p2.targetplatform/maven.locations.xml @@ -1355,4 +1355,11 @@ Export-Package: *;version="${version}";-noimport:=true 4.8.138 jar + + + org.assertj + assertj-core + 3.22.0 + jar + diff --git a/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.target b/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.target index 2d424c21c7..ac3f9d354b 100644 --- a/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.target +++ b/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.target @@ -1,7 +1,7 @@ - + @@ -1492,5 +1492,12 @@ Export-Package: *;version="${version}";-noimport:=true 4.8.138 jar + + + org.assertj + assertj-core + 3.22.0 + jar + diff --git a/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.tpd b/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.tpd index 94ec1659ca..47cc0f08b2 100644 --- a/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.tpd +++ b/org.idempiere.p2.targetplatform/org.idempiere.p2.targetplatform.tpd @@ -1,5 +1,5 @@ -target "idempiere-220124" +target "idempiere-220428" with source configurePhase allEnvironments diff --git a/org.idempiere.test/META-INF/MANIFEST.MF b/org.idempiere.test/META-INF/MANIFEST.MF index 7ddadbdea2..b30c31ff45 100644 --- a/org.idempiere.test/META-INF/MANIFEST.MF +++ b/org.idempiere.test/META-INF/MANIFEST.MF @@ -5,7 +5,9 @@ Bundle-SymbolicName: org.idempiere.test Bundle-Version: 10.0.0.qualifier Bundle-Vendor: iDempiere Automatic-Module-Name: org.idempiere.test -Import-Package: org.junit.jupiter.api;version="5.6.0", +Import-Package: org.assertj.core.api;version="3.22.0", + org.assertj.core.api.junit.jupiter;version="3.22.0", + org.junit.jupiter.api;version="5.6.0", org.junit.jupiter.api.condition;version="5.6.0", org.junit.jupiter.api.extension;version="5.6.0", org.junit.jupiter.api.extension.support;version="5.6.0", diff --git a/org.idempiere.test/idempiere.unit.test.launch b/org.idempiere.test/idempiere.unit.test.launch index a897d78cab..268b266535 100644 --- a/org.idempiere.test/idempiere.unit.test.launch +++ b/org.idempiere.test/idempiere.unit.test.launch @@ -66,6 +66,7 @@ + diff --git a/org.idempiere.test/src/org/idempiere/test/base/QueryTest.java b/org.idempiere.test/src/org/idempiere/test/base/QueryTest.java index 0da1a0cc3d..2ad7ae37af 100644 --- a/org.idempiere.test/src/org/idempiere/test/base/QueryTest.java +++ b/org.idempiere.test/src/org/idempiere/test/base/QueryTest.java @@ -36,9 +36,13 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.stream.Stream; import org.adempiere.exceptions.DBException; import org.adempiere.model.POWrapper; +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.api.junit.jupiter.InjectSoftAssertions; +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; import org.compiere.model.I_Test; import org.compiere.model.MPInstance; import org.compiere.model.MProcess; @@ -53,14 +57,18 @@ import org.compiere.util.Env; import org.compiere.util.KeyNamePair; import org.idempiere.test.AbstractTestCase; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; /** * @author hengsin * */ - +@ExtendWith(SoftAssertionsExtension.class) public class QueryTest extends AbstractTestCase { + @InjectSoftAssertions + SoftAssertions softly; + /** * */ @@ -86,6 +94,41 @@ public class QueryTest extends AbstractTestCase { assertEquals(list.get(1).getTableName(), "M_InOut", "Invalid object 2"); } + @Test + public void testStream() throws Exception + { + Stream stream = new Query(Env.getCtx(), "AD_Table", "TableName IN (?,?)", getTrxName()) + .setParameters("C_Invoice", "M_InOut") + .setOrderBy("TableName") + .stream(); + softly.assertThat(stream.map(MTable::getTableName)).containsExactly("C_Invoice", "M_InOut"); + } + + @Test + public void testIterable() throws Exception { + Iterable query = new Query(Env.getCtx(), "AD_Table", "TableName IN (?,?)", getTrxName()) + .setParameters("C_Invoice", "M_InOut") + .setOrderBy("TableName") + .iterable(); + int i = 0; + for (MTable t : query) { + if (i == 0) + { + softly.assertThat(t.getTableName()).as("element 0").isEqualTo("C_Invoice"); + } + else if (i == 1) + { + softly.assertThat(t.getTableName()).as("element 1").isEqualTo("M_InOut"); + } + else + { + softly.fail("More objects retrieved than expected: " + t.get_TableName()); + break; + } + i++; + } + } + @Test public void testScroll() throws Exception { @@ -109,7 +152,7 @@ public class QueryTest extends AbstractTestCase { } else { - fail("More objects retrived than expected"); + fail("More objects retrieved than expected"); } i++; } @@ -142,7 +185,7 @@ public class QueryTest extends AbstractTestCase { } else { - fail("More objects retrived than expected"); + fail("More objects retrieved than expected"); } i++; }