From a95fdc6f8af6865fa93eb87fdf54cd539ca9b1ae Mon Sep 17 00:00:00 2001 From: hengsin Date: Fri, 26 Apr 2024 19:46:18 +0800 Subject: [PATCH] IDEMPIERE-6122 Query class to accept a list of columns to select (#2331) * IDEMPIERE-6122 Query class to accept a list of columns to select * IDEMPIERE-6122 Query class to accept a list of columns to select - improve efficiency --- .../src/org/compiere/model/MTable.java | 30 ++++++++++ .../src/org/compiere/model/PO.java | 11 +++- .../src/org/compiere/model/POInfo.java | 45 ++++++++++++++ .../src/org/compiere/model/POResultSet.java | 11 +++- .../src/org/compiere/model/Query.java | 59 +++++++++++++++---- .../org/idempiere/test/base/QueryTest.java | 58 ++++++++++++++++++ 6 files changed, 200 insertions(+), 14 deletions(-) diff --git a/org.adempiere.base/src/org/compiere/model/MTable.java b/org.adempiere.base/src/org/compiere/model/MTable.java index eb68485da4..942ff311b7 100644 --- a/org.adempiere.base/src/org/compiere/model/MTable.java +++ b/org.adempiere.base/src/org/compiere/model/MTable.java @@ -623,6 +623,36 @@ public PO getPO (int Record_ID, String trxName) return po; } // getPO + private static final ThreadLocal partialPOResultSetColumns = new ThreadLocal<>(); + + /** + * Get columns included in result set of {@link #getPO(int, String)} call.
+ * Use by {@link #getPartialPO(ResultSet, String[], String)}. + * @return columns included in result set of {@link #getPO(int, String)} call + */ + protected static final String[] getPartialPOResultSetColumns() { + return partialPOResultSetColumns.get(); + } + + /** + * Get PO Instance from result set that only include some of the columns of the PO model. + * @param rs result set + * @param selectColumns + * @param trxName transaction + * @return immutable PO instance + */ + public final PO getPartialPO (ResultSet rs, String[] selectColumns, String trxName) + { + try { + partialPOResultSetColumns.set(selectColumns); + PO po = getPO(rs, trxName); + po.makeImmutable(); + return po; + } finally { + partialPOResultSetColumns.remove(); + } + } + /** * Get PO Instance from result set * @param rs result set diff --git a/org.adempiere.base/src/org/compiere/model/PO.java b/org.adempiere.base/src/org/compiere/model/PO.java index 6726b4cd91..10a4db4167 100644 --- a/org.adempiere.base/src/org/compiere/model/PO.java +++ b/org.adempiere.base/src/org/compiere/model/PO.java @@ -36,6 +36,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.UUID; @@ -1627,7 +1628,7 @@ protected boolean load (ResultSet rs) int size = get_ColumnCount(); boolean success = true; int index = 0; - log.finest("(rs)"); + if (log.isLoggable(Level.FINEST)) log.finest("(rs)"); loadedVirtualColumns.clear(); // load column values for (index = 0; index < size; index++) @@ -1652,6 +1653,14 @@ protected boolean load (ResultSet rs) private boolean loadColumn(ResultSet rs, int index) { boolean success = true; String columnName = p_info.getColumnName(index); + String[] selectColumns = MTable.getPartialPOResultSetColumns(); + if (selectColumns != null && selectColumns.length > 0) { + Optional optional = Arrays.stream(selectColumns).filter(e -> e.equalsIgnoreCase(columnName)).findFirst(); + if (!optional.isPresent()) { + if (log.isLoggable(Level.FINER))log.log(Level.FINER, "Partial PO, Column not loaded: " + columnName); + return true; + } + } Class clazz = p_info.getColumnClass(index); int dt = p_info.getColumnDisplayType(index); try diff --git a/org.adempiere.base/src/org/compiere/model/POInfo.java b/org.adempiere.base/src/org/compiere/model/POInfo.java index 24abac1d38..4d494962f1 100644 --- a/org.adempiere.base/src/org/compiere/model/POInfo.java +++ b/org.adempiere.base/src/org/compiere/model/POInfo.java @@ -25,8 +25,10 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.logging.Level; @@ -880,6 +882,49 @@ public StringBuilder buildSelect(boolean fullyQualified, String ... virtualColum return sql; } + /** + * Build SQL SELECT statement for columns. + * @param fullyQualified prefix column names with the table name + * @return {@link StringBuilder} instance with the SQL statement. + */ + public StringBuilder buildSelectForColumns(boolean fullyQualified, String[] columns) + { + StringBuilder sql = new StringBuilder("SELECT "); + int size = getColumnCount(); + int count = 0; + String uuid = PO.getUUIDColumnName(m_TableName); + for (int i = 0; i < size; i++) + { + String columnName = getColumnName(i); + boolean virtual = isVirtualColumn(i); + boolean isKey = isKey(i); + boolean isUUID = columnName.equals(uuid); + //always include key, uuid and standard columns + if (!isKey && !isUUID && !columnName.equalsIgnoreCase("ad_client_id") && !columnName.equalsIgnoreCase("ad_org_id") + && !columnName.equalsIgnoreCase("isactive") && !columnName.equalsIgnoreCase("created") && !columnName.equalsIgnoreCase("createdby") + && !columnName.equalsIgnoreCase("updated") && !columnName.equalsIgnoreCase("updatedby")) + { + Optional optional = Arrays.stream(columns).filter(e -> e.equalsIgnoreCase(columnName)).findFirst(); + if (!optional.isPresent()) + continue; + } + + count++; + if (count > 1) + sql.append(","); + String columnSQL = getColumnSQL(i); + if (!virtual) + columnSQL = DB.getDatabase().quoteColumnName(columnSQL); + if (fullyQualified && !virtual) + sql.append(getTableName()).append("."); + sql.append(columnSQL); // Normal and Virtual Column + if (fullyQualified && !virtual) + sql.append(" AS ").append(m_columns[i].ColumnName); + } + sql.append(" FROM ").append(getTableName()); + return sql; + } + /** * Is save changes to change log table * @return if table save change log diff --git a/org.adempiere.base/src/org/compiere/model/POResultSet.java b/org.adempiere.base/src/org/compiere/model/POResultSet.java index a3c8308d66..438a05ed80 100644 --- a/org.adempiere.base/src/org/compiere/model/POResultSet.java +++ b/org.adempiere.base/src/org/compiere/model/POResultSet.java @@ -44,6 +44,7 @@ public class POResultSet implements AutoCloseable { private T currentPO = null; /** Should we close the statement and resultSet on any exception that occur ? */ private boolean closeOnError = true; + private String[] selectColumns; /** * Constructs the POResultSet.
@@ -87,7 +88,7 @@ public T next() throws DBException { } try { if ( resultSet.next() ) { - return (T) table.getPO(resultSet, trxName); + return (T) (selectColumns != null && selectColumns.length > 0 ? table.getPartialPO(resultSet, selectColumns, trxName) : table.getPO(resultSet, trxName)); } else { this.close(); // close it if there is no more data to read return null; @@ -135,4 +136,12 @@ public void close() { this.statement = null; currentPO = null; } + + /** + * Set columns for result set. Use for loading of partial PO. + * @param selectColumns + */ + public void setSelectColumns(String[] selectColumns) { + this.selectColumns = selectColumns; + } } diff --git a/org.adempiere.base/src/org/compiere/model/Query.java b/org.adempiere.base/src/org/compiere/model/Query.java index f5e5a99632..7c382f7526 100644 --- a/org.adempiere.base/src/org/compiere/model/Query.java +++ b/org.adempiere.base/src/org/compiere/model/Query.java @@ -110,6 +110,9 @@ public class Query * Number of records will be skipped on query run. */ private int recordsToSkip; + + /** list of columns to include in select statement (optional) */ + private String[] selectColumns; /** * @param table @@ -313,7 +316,6 @@ public void addTableDirectJoin(String foreignTableName) { * @return PO List * @throws DBException */ - @SuppressWarnings("unchecked") public List list() throws DBException { List list = new ArrayList(); @@ -327,7 +329,7 @@ public List list() throws DBException rs = createResultSet(pstmt); while (rs.next ()) { - T po = (T)table.getPO(rs, trxName); + T po = getPO(rs); list.add(po); } } @@ -346,8 +348,7 @@ public List list() throws DBException * Get first PO that match query criteria * @return first PO * @throws DBException - */ - @SuppressWarnings("unchecked") + */ public T first() throws DBException { T po = null; @@ -368,7 +369,7 @@ public T first() throws DBException rs = createResultSet(pstmt); if (rs.next ()) { - po = (T)table.getPO(rs, trxName); + po = getPO(rs); } } catch (SQLException e) @@ -382,6 +383,22 @@ public T first() throws DBException } return po; } + + /** + * Get partial or full PO + * @param + * @param rs + * @return partial or full PO. + */ + @SuppressWarnings("unchecked") + private T getPO(ResultSet rs) { + T po; + if (selectColumns != null && selectColumns.length > 0) + po = (T)table.getPartialPO(rs, selectColumns, trxName); + else + po = (T)table.getPO(rs, trxName); + return po; + } /** * Get first PO that match query criteria.
@@ -390,7 +407,6 @@ public T first() throws DBException * @throws DBException * @see {@link #first()} */ - @SuppressWarnings("unchecked") public T firstOnly() throws DBException { T po = null; @@ -411,7 +427,7 @@ public T firstOnly() throws DBException rs = createResultSet(pstmt); if (rs.next()) { - po = (T)table.getPO(rs, trxName); + po = getPO(rs); } if (rs.next()) { @@ -712,8 +728,7 @@ public Stream stream() throws DBException public boolean tryAdvance(Consumer action) { try { if(!finalRS.next()) return false; - @SuppressWarnings("unchecked") - final T newRec = (T)table.getPO(finalRS, trxName); + final T newRec = getPO(finalRS); action.accept(newRec); return true; } catch(SQLException ex) { @@ -801,6 +816,10 @@ public POResultSet scroll() throws DBException rs = createResultSet(pstmt); rsPO = new POResultSet(table, pstmt, rs, trxName); rsPO.setCloseOnError(true); + if (selectColumns != null && selectColumns.length > 0) + { + rsPO.setSelectColumns(selectColumns); + } return rsPO; } catch (SQLException e) @@ -834,10 +853,17 @@ private final String buildSQL(StringBuilder selectClause, boolean useOrderByClau throw new IllegalStateException("No POInfo found for AD_Table_ID="+table.getAD_Table_ID()); } boolean isFullyQualified = !joinClauseList.isEmpty(); - if(virtualColumns == null) - selectClause = info.buildSelect(isFullyQualified, noVirtualColumn); + if (selectColumns != null && selectColumns.length > 0) + { + selectClause = info.buildSelectForColumns(isFullyQualified, selectColumns); + } else - selectClause = info.buildSelect(isFullyQualified, virtualColumns); + { + if(virtualColumns == null) + selectClause = info.buildSelect(isFullyQualified, noVirtualColumn); + else + selectClause = info.buildSelect(isFullyQualified, virtualColumns); + } } if (!joinClauseList.isEmpty()) { @@ -1063,4 +1089,13 @@ public Query setVirtualColumns(String ... virtualColumns) { return this; } + /** + * Set the columns to include in select query.
+ * Note that this doesn't effect {@link #iterate()}. + * @param columns + */ + public Query selectColumns(String ...columns) { + this.selectColumns = columns; + return this; + } } 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 5cb76ba780..6db058975f 100644 --- a/org.idempiere.test/src/org/idempiere/test/base/QueryTest.java +++ b/org.idempiere.test/src/org/idempiere/test/base/QueryTest.java @@ -25,7 +25,9 @@ package org.idempiere.test.base; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -46,6 +48,7 @@ import org.compiere.model.I_Test; import org.compiere.model.MPInstance; import org.compiere.model.MProcess; +import org.compiere.model.MProduct; import org.compiere.model.MTable; import org.compiere.model.MTest; import org.compiere.model.MUser; @@ -434,4 +437,59 @@ public void testTableDirectJoin() { String sql = query.getSQL(); assertTrue(sql.toLowerCase().contains("inner join c_bpartner on (ad_user.c_bpartner_id=c_bpartner.c_bpartner_id)"), "Unexpected SQL clause generated from query"); } + + @Test + public void testPartialPO() { + Query query = new Query(Env.getCtx(), MProduct.Table_Name, MProduct.COLUMNNAME_M_Product_ID + "=?", getTrxName()); + MProduct product = query.setParameters(DictionaryIDs.M_Product.AZALEA_BUSH.id).first(); + assertNotNull(product.getName()); + assertNotNull(product.getValue()); + assertNotNull(product.getProductType()); + assertTrue(product.getM_Product_Category_ID() > 0); + assertFalse(product.is_Immutable()); + + product = query.selectColumns(MProduct.COLUMNNAME_Name, MProduct.COLUMNNAME_Value).setParameters(DictionaryIDs.M_Product.AZALEA_BUSH.id).first(); + assertNotNull(product.getName()); + assertNotNull(product.getValue()); + assertNull(product.getProductType()); + assertTrue(product.getM_Product_Category_ID() == 0); + assertTrue(product.is_Immutable()); + + product = query.selectColumns().setParameters(DictionaryIDs.M_Product.AZALEA_BUSH.id).first(); + assertNotNull(product.getName()); + assertNotNull(product.getValue()); + assertNotNull(product.getProductType()); + assertTrue(product.getM_Product_Category_ID() > 0); + assertFalse(product.is_Immutable()); + + List list = query.selectColumns(MProduct.COLUMNNAME_Name, MProduct.COLUMNNAME_Value).setParameters(DictionaryIDs.M_Product.AZALEA_BUSH.id).list(); + product = list.get(0); + assertNotNull(product.getName()); + assertNotNull(product.getValue()); + assertNull(product.getProductType()); + assertTrue(product.getM_Product_Category_ID() == 0); + assertTrue(product.is_Immutable()); + + product = query.selectColumns(MProduct.COLUMNNAME_Name, MProduct.COLUMNNAME_Value).setParameters(DictionaryIDs.M_Product.AZALEA_BUSH.id).firstOnly(); + assertNotNull(product.getName()); + assertNotNull(product.getValue()); + assertNull(product.getProductType()); + assertTrue(product.getM_Product_Category_ID() == 0); + assertTrue(product.is_Immutable()); + + product = (MProduct) query.selectColumns(MProduct.COLUMNNAME_Name, MProduct.COLUMNNAME_Value).setParameters(DictionaryIDs.M_Product.AZALEA_BUSH.id).scroll().next(); + assertNotNull(product.getName()); + assertNotNull(product.getValue()); + assertNull(product.getProductType()); + assertTrue(product.getM_Product_Category_ID() == 0); + assertTrue(product.is_Immutable()); + + Stream stream = query.selectColumns(MProduct.COLUMNNAME_Name, MProduct.COLUMNNAME_Value).setParameters(DictionaryIDs.M_Product.AZALEA_BUSH.id).stream(); + product = stream.findFirst().get(); + assertNotNull(product.getName()); + assertNotNull(product.getValue()); + assertNull(product.getProductType()); + assertTrue(product.getM_Product_Category_ID() == 0); + assertTrue(product.is_Immutable()); + } }