IDEMPIERE-5014 Add stream interface to Query (#1298)

This commit is contained in:
Fr Jeremy Krieg 2022-04-29 11:57:26 +09:30 committed by GitHub
parent 5476274f59
commit 10db6177d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 135 additions and 38 deletions

View File

@ -28,7 +28,12 @@ import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Properties; 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.logging.Level;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.adempiere.exceptions.DBException; import org.adempiere.exceptions.DBException;
import org.compiere.util.CLogger; 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 * Return an Iterable implementation that can be used in a <tt>for</tt> expression. Example:
* all IDS that match the query criteria and issue sql query to fetch the PO when caller want to * <pre>{@code
* fetch the next PO. This minimize memory usage but it is slower than the list method. * Iterable<MTable> query = new Query(...).iterable();
* @return Iterator *
* for (MTable table : query) {
* // Do stuff with the element
* }
* </pre>
*
* @return Iterable
* @throws DBException * @throws DBException
*/ */
public <T extends PO> Iterator<T> iterate() throws DBException public <T extends PO> Iterable<T> iterable() throws DBException
{ {
String[] keys = table.getKeyColumns(); return () -> iterate();
StringBuilder sqlBuffer = new StringBuilder(" SELECT "); }
for (int i = 0; i < keys.length; i++) {
if (i > 0) /**
sqlBuffer.append(", "); * Return an Stream implementation to fetch one PO at a time. This method will only create POs on-demand and
if (!joinClauseList.isEmpty()) * they will become eligible for garbage collection once they have been consumed by the stream, so unlike
sqlBuffer.append(table.getTableName()).append("."); * {@link #list()} it doesn't have to hold a copy of all the POs in the result set in memory at one time.
sqlBuffer.append(keys[i]); * 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 <code>list()</code>.
sqlBuffer.append(" FROM ").append(table.getTableName()); * @return Stream
String sql = buildSQL(sqlBuffer, true); * @throws DBException
*/
public <T extends PO> Stream<T> stream() throws DBException
{
String sql = buildSQL(null, true);
PreparedStatement pstmt = null; PreparedStatement pstmt = null;
ResultSet rs = null; ResultSet rs = null;
List<Object[]> idList = new ArrayList<Object[]>(); try {
try
{
pstmt = DB.prepareStatement (sql, trxName); pstmt = DB.prepareStatement (sql, trxName);
final PreparedStatement finalPstmt = pstmt;
rs = createResultSet(pstmt); rs = createResultSet(pstmt);
while (rs.next ()) final ResultSet finalRS = rs;
{
Object[] ids = new Object[keys.length]; return StreamSupport.stream(new Spliterators.AbstractSpliterator<T>(
for (int i = 0; i < ids.length; i++) { Long.MAX_VALUE,Spliterator.ORDERED) {
ids[i] = rs.getObject(i+1); @Override
} public boolean tryAdvance(Consumer<? super T> action) {
idList.add(ids); 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) catch (SQLException e)
{ {
DB.close(rs, pstmt);
log.log(Level.SEVERE, sql, e); log.log(Level.SEVERE, sql, e);
throw new DBException(e, sql); throw new DBException(e, sql);
} finally {
DB.close(rs, pstmt);
rs = null; pstmt = null;
} }
return new POIterator<T>(table, idList, trxName); }
/**
* Return an Iterator implementation to fetch one PO at a time. This implementation is equivalent to
* <tt>stream().iterator()</tt> and has similar performance benefits compared with {@link #list()}.
* Useful if the downstream API requires an <code>Iterator</code>; otherwise the additional
* of the {@link #stream()} interface is likely to be more useful.
*
* @return Iterator
* @throws DBException
* @see #stream()
*/
@SuppressWarnings("unchecked")
public <T extends PO> Iterator<T> iterate() throws DBException
{
return (Iterator<T>) stream().iterator();
} }
/** /**

View File

@ -1355,4 +1355,11 @@ Export-Package: *;version="${version}";-noimport:=true
<version>4.8.138</version> <version>4.8.138</version>
<type>jar</type> <type>jar</type>
</location> </location>
<!-- AssertJ -->
<location includeSource="true" missingManifest="error" type="Maven">
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.22.0</version>
<type>jar</type>
</location>
</locations> </locations>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?pde?> <?pde?>
<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
<target name="idempiere-220124" sequenceNumber="1643026597"> <target name="idempiere-220428" sequenceNumber="1643026598">
<locations> <locations>
<location includeMode="slicer" includeAllPlatforms="true" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <location includeMode="slicer" includeAllPlatforms="true" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
<unit id="zcommon" version="9.6.1"/> <unit id="zcommon" version="9.6.1"/>
@ -1492,5 +1492,12 @@ Export-Package: *;version="${version}";-noimport:=true
<version>4.8.138</version> <version>4.8.138</version>
<type>jar</type> <type>jar</type>
</location> </location>
<!-- AssertJ -->
<location includeSource="true" missingManifest="error" type="Maven">
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.22.0</version>
<type>jar</type>
</location>
</locations> </locations>
</target> </target>

View File

@ -1,5 +1,5 @@
target "idempiere-220124" target "idempiere-220428"
with source configurePhase allEnvironments with source configurePhase allEnvironments

View File

@ -5,7 +5,9 @@ Bundle-SymbolicName: org.idempiere.test
Bundle-Version: 10.0.0.qualifier Bundle-Version: 10.0.0.qualifier
Bundle-Vendor: iDempiere Bundle-Vendor: iDempiere
Automatic-Module-Name: org.idempiere.test 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.condition;version="5.6.0",
org.junit.jupiter.api.extension;version="5.6.0", org.junit.jupiter.api.extension;version="5.6.0",
org.junit.jupiter.api.extension.support;version="5.6.0", org.junit.jupiter.api.extension.support;version="5.6.0",

View File

@ -66,6 +66,7 @@
<stringAttribute key="product" value="org.adempiere.server.server_product"/> <stringAttribute key="product" value="org.adempiere.server.server_product"/>
<booleanAttribute key="run_in_ui_thread" value="true"/> <booleanAttribute key="run_in_ui_thread" value="true"/>
<setAttribute key="selected_target_bundles"> <setAttribute key="selected_target_bundles">
<setEntry value="assertj-core@default:default"/>
<setEntry value="bcmail@default:default"/> <setEntry value="bcmail@default:default"/>
<setEntry value="bcpg*1.69.0@default:default"/> <setEntry value="bcpg*1.69.0@default:default"/>
<setEntry value="bcpkix@default:default"/> <setEntry value="bcpkix@default:default"/>

View File

@ -36,9 +36,13 @@ import java.sql.Timestamp;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import org.adempiere.exceptions.DBException; import org.adempiere.exceptions.DBException;
import org.adempiere.model.POWrapper; 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.I_Test;
import org.compiere.model.MPInstance; import org.compiere.model.MPInstance;
import org.compiere.model.MProcess; import org.compiere.model.MProcess;
@ -53,14 +57,18 @@ import org.compiere.util.Env;
import org.compiere.util.KeyNamePair; import org.compiere.util.KeyNamePair;
import org.idempiere.test.AbstractTestCase; import org.idempiere.test.AbstractTestCase;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
/** /**
* @author hengsin * @author hengsin
* *
*/ */
@ExtendWith(SoftAssertionsExtension.class)
public class QueryTest extends AbstractTestCase { 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"); assertEquals(list.get(1).getTableName(), "M_InOut", "Invalid object 2");
} }
@Test
public void testStream() throws Exception
{
Stream<MTable> 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<MTable> 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 @Test
public void testScroll() throws Exception public void testScroll() throws Exception
{ {
@ -109,7 +152,7 @@ public class QueryTest extends AbstractTestCase {
} }
else else
{ {
fail("More objects retrived than expected"); fail("More objects retrieved than expected");
} }
i++; i++;
} }
@ -142,7 +185,7 @@ public class QueryTest extends AbstractTestCase {
} }
else else
{ {
fail("More objects retrived than expected"); fail("More objects retrieved than expected");
} }
i++; i++;
} }