IDEMPIERE-5126 Virtual column lazy loading (#1095)

* IDEMPIERE-5126 Virtual column lazy loading

Virtual columns are now loaded only whenever their respective getters are invoked. This functionality works irrespective of the PO constructor used.

Query class now uses lazy loading by default as well.

* IDEMPIERE-5126 Virtual column lazy loading

Have the model generator ignore virtual columns with the @SQL prefix, as
their generated getters would always return null.

* IDEMPIERE-5126 Virtual column lazy loading

Added quoting to virtual column names

* IDEMPIERE-5126 Virtual column lazy loading

Reload virtual columns if the record was reloaded.

* IDEMPIERE-5126 Virtual column lazy loading

Fixed class cast exception in unit test.

* IDEMPIERE-5126 Virtual column lazy loading

Support for loading selected virtual columns along with regular table
columns, skipping lazy loading.

* IDEMPIERE-5126 Virtual column lazy loading

Fine-grained virtual column loading support for Query.

* IDEMPIERE-5126 Virtual column lazy loading

Support for loading selected virtual columns along with regular table
columns, skipping lazy loading.

* IDEMPIERE-5126 Virtual column lazy loading

Improved support for the constructor PO.PO(Properties, int, String,
String...).

* IDEMPIERE-5126 Virtual column lazy loading

Refactoring. Reduced size of messages sent and received from database
engine when lazy loading virtual columns. Other minor changes.

* IDEMPIERE-5126 Virtual column lazy loading

Added testing assertions.

* IDEMPIERE-5126 Virtual column lazy loading

Silenced Eclipse warnings.
This commit is contained in:
Saulo José Gil 2022-01-05 10:14:50 -03:00 committed by GitHub
parent 2fe6f5bba5
commit 0eb89932fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 416 additions and 160 deletions

View File

@ -0,0 +1,18 @@
SET SQLBLANKLINES ON
SET DEFINE OFF
-- Virtual Column Test
-- Dec 25, 2021, 3:25:02 PM ART
INSERT INTO AD_Element (AD_Element_ID,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,ColumnName,Name,Description,PrintName,EntityType,AD_Element_UU) VALUES (203552,0,0,'Y',TO_DATE('2021-12-25 15:25:01','YYYY-MM-DD HH24:MI:SS'),0,TO_DATE('2021-12-25 15:25:01','YYYY-MM-DD HH24:MI:SS'),0,'TestVirtualQty','Virtual Quantity','Used only for testing purposes','Test Virtual Column (Quantity)','D','c1b166c4-e265-47e1-b5f9-d9ce684df5aa')
;
-- Dec 25, 2021, 3:26:11 PM ART
INSERT INTO AD_Column (AD_Column_ID,Version,Name,Description,AD_Table_ID,ColumnName,FieldLength,IsKey,IsParent,IsMandatory,IsTranslated,IsIdentifier,SeqNo,IsEncrypted,AD_Reference_ID,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,AD_Element_ID,IsUpdateable,IsSelectionColumn,EntityType,IsSyncDatabase,IsAlwaysUpdateable,ColumnSQL,IsAutocomplete,IsAllowLogging,AD_Column_UU,IsAllowCopy,SeqNoSelection,IsToolbarButton,IsSecure,IsHtml) VALUES (214655,0,'Virtual Quantity','Used only for testing purposes',135,'TestVirtualQty',22,'N','N','N','N','N',0,'N',29,0,0,'Y',TO_DATE('2021-12-25 15:26:10','YYYY-MM-DD HH24:MI:SS'),0,TO_DATE('2021-12-25 15:26:10','YYYY-MM-DD HH24:MI:SS'),0,203552,'N','N','D','N','N','(SELECT 123.45)','N','Y','db8b1287-434e-43a4-91fe-748141eb8ffb','N',0,'N','N','N')
;
-- Dec 25, 2021, 5:14:12 PM ART
INSERT INTO AD_Field (AD_Field_ID,Name,Description,AD_Tab_ID,AD_Column_ID,IsDisplayed,DisplayLength,SeqNo,SortNo,IsSameLine,IsHeading,IsFieldOnly,IsEncrypted,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,IsReadOnly,IsCentrallyMaintained,AD_FieldGroup_ID,EntityType,AD_Field_UU,IsDisplayedGrid,SeqNoGrid,XPosition,ColumnSpan,NumLines,IsQuickEntry,IsDefaultFocus,IsAdvancedField,IsQuickForm) VALUES (206818,'Virtual Quantity','Used only for testing purposes',152,214655,'Y',0,260,0,'N','N','N','N',0,0,'Y',TO_DATE('2021-12-25 17:14:11','YYYY-MM-DD HH24:MI:SS'),0,TO_DATE('2021-12-25 17:14:11','YYYY-MM-DD HH24:MI:SS'),0,'N','Y',123,'D','240ab652-73b9-4dd1-b52f-0faa1d522817','Y',270,1,1,1,'N','N','N','N')
;
SELECT register_migration_script('202112251630_IDEMPIERE-5126.sql') FROM dual
;

View File

@ -0,0 +1,16 @@
-- Virtual Column Test
-- Dec 25, 2021, 3:25:02 PM ART
INSERT INTO AD_Element (AD_Element_ID,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,ColumnName,Name,Description,PrintName,EntityType,AD_Element_UU) VALUES (203552,0,0,'Y',TO_TIMESTAMP('2021-12-25 15:25:01','YYYY-MM-DD HH24:MI:SS'),0,TO_TIMESTAMP('2021-12-25 15:25:01','YYYY-MM-DD HH24:MI:SS'),0,'TestVirtualQty','Virtual Quantity','Used only for testing purposes','Test Virtual Column (Quantity)','D','c1b166c4-e265-47e1-b5f9-d9ce684df5aa')
;
-- Dec 25, 2021, 3:26:11 PM ART
INSERT INTO AD_Column (AD_Column_ID,Version,Name,Description,AD_Table_ID,ColumnName,FieldLength,IsKey,IsParent,IsMandatory,IsTranslated,IsIdentifier,SeqNo,IsEncrypted,AD_Reference_ID,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,AD_Element_ID,IsUpdateable,IsSelectionColumn,EntityType,IsSyncDatabase,IsAlwaysUpdateable,ColumnSQL,IsAutocomplete,IsAllowLogging,AD_Column_UU,IsAllowCopy,SeqNoSelection,IsToolbarButton,IsSecure,IsHtml) VALUES (214655,0,'Virtual Quantity','Used only for testing purposes',135,'TestVirtualQty',22,'N','N','N','N','N',0,'N',29,0,0,'Y',TO_TIMESTAMP('2021-12-25 15:26:10','YYYY-MM-DD HH24:MI:SS'),0,TO_TIMESTAMP('2021-12-25 15:26:10','YYYY-MM-DD HH24:MI:SS'),0,203552,'N','N','D','N','N','(SELECT 123.45)','N','Y','db8b1287-434e-43a4-91fe-748141eb8ffb','N',0,'N','N','N')
;
-- Dec 25, 2021, 5:14:12 PM ART
INSERT INTO AD_Field (AD_Field_ID,Name,Description,AD_Tab_ID,AD_Column_ID,IsDisplayed,DisplayLength,SeqNo,SortNo,IsSameLine,IsHeading,IsFieldOnly,IsEncrypted,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,IsReadOnly,IsCentrallyMaintained,AD_FieldGroup_ID,EntityType,AD_Field_UU,IsDisplayedGrid,SeqNoGrid,XPosition,ColumnSpan,NumLines,IsQuickEntry,IsDefaultFocus,IsAdvancedField,IsQuickForm) VALUES (206818,'Virtual Quantity','Used only for testing purposes',152,214655,'Y',0,260,0,'N','N','N','N',0,0,'Y',TO_TIMESTAMP('2021-12-25 17:14:11','YYYY-MM-DD HH24:MI:SS'),0,TO_TIMESTAMP('2021-12-25 17:14:11','YYYY-MM-DD HH24:MI:SS'),0,'N','Y',123,'D','240ab652-73b9-4dd1-b52f-0faa1d522817','Y',270,1,1,1,'N','N','N','N')
;
SELECT register_migration_script('202112251630_IDEMPIERE-5126.sql') FROM dual
;

View File

@ -54,20 +54,40 @@ public abstract class AbstractModelFactory implements IModelFactory {
boolean errorLogged = false;
try
{
Exception ce = null;
Object[] arguments = null;
Constructor<?> constructor = null;
try
{
constructor = clazz.getDeclaredConstructor(new Class[]{Properties.class, int.class, String.class});
arguments = new Object[] {Env.getCtx(), Integer.valueOf(Record_ID), trxName};
}
catch (Exception e)
{
String msg = e.getMessage();
ce = e;
}
if(constructor==null)
{
try
{
constructor = clazz.getDeclaredConstructor(new Class[]{Properties.class, int.class, String.class, String[].class});
arguments = new Object[] {Env.getCtx(), Integer.valueOf(Record_ID), trxName, null};
}
catch(Exception e)
{
ce = e;
}
}
if(constructor==null && ce!=null)
{
String msg = ce.getMessage();
if (msg == null)
msg = e.toString();
msg = ce.toString();
s_log.warning("No transaction Constructor for " + clazz + " (" + msg + ")");
}
PO po = constructor!=null ? (PO)constructor.newInstance(new Object[] {Env.getCtx(), Integer.valueOf(Record_ID), trxName}) : null;
PO po = constructor!=null ? (PO)constructor.newInstance(arguments) : null;
return po;
}
catch (Exception e)

View File

@ -53,7 +53,7 @@ public class GenericPO extends PO implements DocAction {
* @param ID
*/
public GenericPO(String tableName, Properties ctx, int ID) {
super(new PropertiesWrapper(ctx, tableName), ID, null, null);
super(new PropertiesWrapper(ctx, tableName), ID, null, (String[]) null);
}
/**
@ -72,7 +72,7 @@ public class GenericPO extends PO implements DocAction {
* @param trxName
*/
public GenericPO(String tableName, Properties ctx, int ID, String trxName) {
super(new PropertiesWrapper(ctx, tableName), ID, trxName, null);
super(new PropertiesWrapper(ctx, tableName), ID, trxName, (String[]) null);
}
/**

View File

@ -198,6 +198,19 @@ public class ModelClassGenerator
.append(" }").append(NL)
// Constructor End
// Standard Constructor + Virtual Columns
.append(NL)
.append(" /** Standard Constructor */").append(NL)
.append(" public ").append(className).append(" (Properties ctx, int ").append(keyColumn).append(", String trxName, String ... virtualColumns)").append(NL)
.append(" {").append(NL)
.append(" super (ctx, ").append(keyColumn).append(", trxName, virtualColumns);").append(NL)
.append(" /** if (").append(keyColumn).append(" == 0)").append(NL)
.append(" {").append(NL)
.append(mandatory)
.append(" } */").append(NL)
.append(" }").append(NL)
// Constructor End
// Load Constructor
.append(NL)
.append(" /** Load Constructor */").append(NL)
@ -267,7 +280,7 @@ public class ModelClassGenerator
+ "FROM AD_Column c "
+ "WHERE c.AD_Table_ID=?"
+ " AND c.ColumnName NOT IN ('AD_Client_ID', 'AD_Org_ID', 'IsActive', 'Created', 'CreatedBy', 'Updated', 'UpdatedBy')"
+ " AND c.IsActive='Y'"
+ " AND c.IsActive='Y' AND (c.ColumnSQL IS NULL OR c.ColumnSQL NOT LIKE '@SQL%') "
+ (!Util.isEmpty(entityTypeFilter) ? " AND c." + entityTypeFilter : "")
+ " ORDER BY c.ColumnName";
boolean isKeyNamePairCreated = false; // true if the method "getKeyNamePair" is already generated

View File

@ -248,7 +248,7 @@ public class ModelInterfaceGenerator
+ "FROM AD_Column c "
+ "WHERE c.AD_Table_ID=?"
+ " AND c.IsActive='Y'"
+ " AND c.IsActive='Y' AND (c.ColumnSQL IS NULL OR c.ColumnSQL NOT LIKE '@SQL%') "
+ (!Util.isEmpty(entityTypeFilter) ? " AND c." + entityTypeFilter : "")
+ " ORDER BY c.ColumnName";
PreparedStatement pstmt = null;

View File

@ -22,7 +22,7 @@ import org.compiere.util.KeyNamePair;
/** Generated Interface for M_ProductionLine
* @author iDempiere (generated)
* @version Release 9
* @version Release 10
*/
public interface I_M_ProductionLine
{
@ -266,32 +266,6 @@ public interface I_M_ProductionLine
*/
public boolean isProcessed();
/** Column name ProductType */
public static final String COLUMNNAME_ProductType = "ProductType";
/** Set Product Type.
* Type of product
*/
public void setProductType (String ProductType);
/** Get Product Type.
* Type of product
*/
public String getProductType();
/** Column name QtyAvailable */
public static final String COLUMNNAME_QtyAvailable = "QtyAvailable";
/** Set Available Quantity.
* Available Quantity (On Hand - Reserved)
*/
public void setQtyAvailable (BigDecimal QtyAvailable);
/** Get Available Quantity.
* Available Quantity (On Hand - Reserved)
*/
public BigDecimal getQtyAvailable();
/** Column name QtyUsed */
public static final String COLUMNNAME_QtyUsed = "QtyUsed";

View File

@ -348,6 +348,19 @@ public interface I_Test
/** Get Test_UU */
public String getTest_UU();
/** Column name TestVirtualQty */
public static final String COLUMNNAME_TestVirtualQty = "TestVirtualQty";
/** Set TestVirtualQty.
* Used only for testing purposes
*/
public void setTestVirtualQty (BigDecimal TestVirtualQty);
/** Get TestVirtualQty.
* Used only for testing purposes
*/
public BigDecimal getTestVirtualQty();
/** Column name T_Integer */
public static final String COLUMNNAME_T_Integer = "T_Integer";

View File

@ -87,6 +87,10 @@ public class MTest extends X_Test
} // MTest
public MTest(Properties ctx, int Test_ID, String trxName, String... virtualColumns) {
super(ctx, Test_ID, trxName, virtualColumns);
}
/**
* Before Delete
* @return true if it can be deleted

View File

@ -33,9 +33,11 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;
@ -147,20 +149,32 @@ public abstract class PO
*/
public PO (Properties ctx)
{
this (ctx, 0, null, null);
this (ctx, 0, null, null, (String[]) null);
} // PO
/**
* Create and Load existing Persistent Object
* @param ID The unique ID of the object
* @param ID The unique ID of the object
* @param ctx context
* @param trxName transaction name
*/
public PO (Properties ctx, int ID, String trxName)
{
this (ctx, ID, trxName, null);
this (ctx, ID, trxName, null, (String[]) null);
} // PO
/**
* Create and load existing Persistent Object
* @param ctx Context
* @param ID Unique ID of the object
* @param trxName Transaction name
* @param virtualColumns names of virtual columns to load along with the regular table columns
*/
public PO (Properties ctx, int ID, String trxName, String ... virtualColumns)
{
this (ctx, ID, trxName, null, virtualColumns);
}
/**
* Create and Load existing Persistent Object.
* @param ctx context
@ -190,8 +204,9 @@ public abstract class PO
* @param ID the ID if 0, the record defaults are applied - ignored if re exists
* @param trxName transaction name
* @param rs optional - load from current result set position (no navigation, not closed)
* @param virtualColumns optional - names of virtual columns to load along with the regular table columns
*/
public PO (Properties ctx, int ID, String trxName, ResultSet rs)
public PO (Properties ctx, int ID, String trxName, ResultSet rs, String ... virtualColumns)
{
p_ctx = ctx != null ? ctx : Env.getCtx();
m_trxName = trxName;
@ -207,9 +222,9 @@ public abstract class PO
m_setErrorsFilled = false;
if (rs != null)
load(rs); // will not have virtual columns
load(rs);
else
load(ID, trxName);
load(ID, trxName, virtualColumns);
checkCrossTenant(false);
} // PO
@ -223,7 +238,7 @@ public abstract class PO
*/
public PO (Properties ctx, PO source, int AD_Client_ID, int AD_Org_ID)
{
this (ctx, 0, null, null); // create new
this (ctx, 0, null, (String[]) null); // create new
//
if (source != null)
copyValues (source, this);
@ -296,6 +311,9 @@ public abstract class PO
private String[] m_optimisticLockingColumns = new String[] {"Updated"};
private Boolean m_useOptimisticLocking = null;
/** Indices of virtual columns that were already resolved */
private Set<Integer> loadedVirtualColumns = new HashSet<>();
/** Access Level S__ 100 4 System info */
public static final int ACCESSLEVEL_SYSTEM = 4;
/** Access Level _C_ 010 2 Client info */
@ -487,6 +505,8 @@ public abstract class PO
return null;
return m_newValues[index];
}
if(p_info.isVirtualColumn(index) && p_info.isVirtualDBColumn(index))
loadVirtualColumn(index);
return m_oldValues[index];
} // get_Value
@ -1325,8 +1345,9 @@ public abstract class PO
* Load record with ID
* @param ID ID
* @param trxName transaction name
* @param virtualColumns names of virtual columns to load along with the regular table columns
*/
protected void load (int ID, String trxName)
protected void load (int ID, String trxName, String ... virtualColumns)
{
checkImmutable();
@ -1335,7 +1356,7 @@ public abstract class PO
{
setKeyInfo();
m_IDs = new Object[] {Integer.valueOf(ID)};
load(trxName);
load(trxName, virtualColumns);
}
else // new
{
@ -1349,10 +1370,11 @@ public abstract class PO
/**
* Load record with UUID
*
* @param uuID UUID
* @param uuID universally unique identifier
* @param trxName transaction name
* @param virtualColumns names of virtual columns to load along with the regular table columns
*/
public void loadByUU(String uuID, String trxName)
public void loadByUU(String uuID, String trxName, String ... virtualColumns)
{
if (Util.isEmpty(uuID, true))
{
@ -1366,25 +1388,27 @@ public abstract class PO
if (log.isLoggable(Level.FINEST))
log.finest("uuID=" + uuID);
load(uuID,trxName);
load(uuID,trxName, virtualColumns);
} // loadByUU
/**
* (re)Load record with m_ID[*]
* @param trxName transaction
* @param virtualColumns names of virtual columns to load along with the regular table columns
* @return true if loaded
*/
public boolean load (String trxName) {
return load(null, trxName);
public boolean load (String trxName, String ... virtualColumns) {
return load(null, trxName, virtualColumns);
}
/**
* (re)Load record with uuID
* @param uuID RecrodUU
* @param trxName transaction
* @param virtualColumns names of virtual columns to load along with the regular table columns
* @return true if loaded
*/
protected boolean load (String uuID,String trxName)
protected boolean load (String uuID, String trxName, String ... virtualColumns)
{
m_trxName = trxName;
boolean success = true;
@ -1392,14 +1416,33 @@ public abstract class PO
int size = get_ColumnCount();
for (int i = 0; i < size; i++)
{
if (i != 0)
sql.append(",");
String columnSQL = p_info.getColumnSQL(i);
if (!p_info.isVirtualColumn(i))
if (p_info.isVirtualColumn(i))
{
boolean lazyLoad = true;
if(virtualColumns != null)
{
for(String virtualColumn : virtualColumns)
{
if(p_info.getColumnName(i).equalsIgnoreCase(virtualColumn))
{
lazyLoad = false;
break;
}
}
}
if(lazyLoad)
continue;
}
else
{
columnSQL = DB.getDatabase().quoteColumnName(columnSQL);
}
sql.append(columnSQL); // Normal and Virtual Column
if (i != 0)
sql.append(",");
sql.append(columnSQL);
}
sql.append(" FROM ").append(p_info.getTableName())
.append(" WHERE ")
@ -1480,61 +1523,12 @@ public abstract class PO
boolean success = true;
int index = 0;
log.finest("(rs)");
loadedVirtualColumns.clear();
// load column values
for (index = 0; index < size; index++)
{
String columnName = p_info.getColumnName(index);
Class<?> clazz = p_info.getColumnClass(index);
int dt = p_info.getColumnDisplayType(index);
try
{
if (clazz == Integer.class)
m_oldValues[index] = decrypt(index, Integer.valueOf(rs.getInt(columnName)));
else if (clazz == BigDecimal.class)
m_oldValues[index] = decrypt(index, rs.getBigDecimal(columnName));
else if (clazz == Boolean.class)
m_oldValues[index] = Boolean.valueOf("Y".equals(decrypt(index, rs.getString(columnName))));
else if (clazz == Timestamp.class)
m_oldValues[index] = decrypt(index, rs.getTimestamp(columnName));
else if (DisplayType.isLOB(dt))
m_oldValues[index] = get_LOB (rs.getObject(columnName));
else if (clazz == String.class)
{
String value = (String)decrypt(index, rs.getString(columnName));
if (value != null)
{
if (get_Table_ID() == I_AD_Column.Table_ID || get_Table_ID() == I_AD_Element.Table_ID
|| get_Table_ID() == I_AD_Field.Table_ID)
{
if ("Description".equals(columnName) || "Help".equals(columnName))
{
value = value.intern();
}
}
}
m_oldValues[index] = value;
}
else
m_oldValues[index] = loadSpecial(rs, index);
// NULL
if (rs.wasNull() && m_oldValues[index] != null)
m_oldValues[index] = null;
//
if (CLogMgt.isLevelAll())
log.finest(String.valueOf(index) + ": " + p_info.getColumnName(index)
+ "(" + p_info.getColumnClass(index) + ") = " + m_oldValues[index]);
}
catch (SQLException e)
{
if (p_info.isVirtualColumn(index)) { // if rs constructor used
if (log.isLoggable(Level.FINER))log.log(Level.FINER, "Virtual Column not loaded: " + columnName);
} else {
log.log(Level.SEVERE, "(rs) - " + String.valueOf(index)
+ ": " + p_info.getTableName() + "." + p_info.getColumnName(index)
+ " (" + p_info.getColumnClass(index) + ") - " + e);
success = false;
}
}
if(!loadColumn(rs, index) && success)
success = false;
}
m_createNew = false;
setKeyInfo();
@ -1542,6 +1536,103 @@ public abstract class PO
return success;
} // load
/**
* Load column value coming from a {@link ResultSet}.
* @param rs {@link ResultSet} with its position set according to the model class instance.
* @param index Column index. Might not coincide with the index of the column within the {@link ResultSet}.
* @return
* @see #m_oldValues
* @see POInfo#getColumnIndex(String)
*/
private boolean loadColumn(ResultSet rs, int index) {
boolean success = true;
String columnName = p_info.getColumnName(index);
Class<?> clazz = p_info.getColumnClass(index);
int dt = p_info.getColumnDisplayType(index);
try
{
if (clazz == Integer.class)
m_oldValues[index] = decrypt(index, Integer.valueOf(rs.getInt(columnName)));
else if (clazz == BigDecimal.class)
m_oldValues[index] = decrypt(index, rs.getBigDecimal(columnName));
else if (clazz == Boolean.class)
m_oldValues[index] = Boolean.valueOf("Y".equals(decrypt(index, rs.getString(columnName))));
else if (clazz == Timestamp.class)
m_oldValues[index] = decrypt(index, rs.getTimestamp(columnName));
else if (DisplayType.isLOB(dt))
m_oldValues[index] = get_LOB (rs.getObject(columnName));
else if (clazz == String.class)
{
String value = (String)decrypt(index, rs.getString(columnName));
if (value != null)
{
if (get_Table_ID() == I_AD_Column.Table_ID || get_Table_ID() == I_AD_Element.Table_ID
|| get_Table_ID() == I_AD_Field.Table_ID)
{
if ("Description".equals(columnName) || "Help".equals(columnName))
{
value = value.intern();
}
}
}
m_oldValues[index] = value;
}
else
m_oldValues[index] = loadSpecial(rs, index);
// NULL
if (rs.wasNull() && m_oldValues[index] != null)
m_oldValues[index] = null;
// flag virtual column as loaded
if(p_info.isVirtualColumn(index))
loadedVirtualColumns.add(index);
//
if (CLogMgt.isLevelAll())
log.finest(String.valueOf(index) + ": " + p_info.getColumnName(index)
+ "(" + p_info.getColumnClass(index) + ") = " + m_oldValues[index]);
}
catch (SQLException e)
{
if (p_info.isVirtualColumn(index)) {
if (log.isLoggable(Level.FINER))log.log(Level.FINER, "Virtual Column not loaded: " + columnName);
} else {
log.log(Level.SEVERE, "(rs) - " + String.valueOf(index)
+ ": " + p_info.getTableName() + "." + p_info.getColumnName(index)
+ " (" + p_info.getColumnClass(index) + ") - " + e);
success = false;
}
}
return success;
}
/**
* Load value for virtual column, only if it wasn't loaded previously.
* @param index Column index (see {@link POInfo#getColumnIndex(String)}).
*/
private void loadVirtualColumn(int index) {
if(!m_createNew && !loadedVirtualColumns.contains(index)) {
StringBuilder sql = new StringBuilder("SELECT ").append(p_info.getColumnSQL(index))
.append(" FROM ").append(p_info.getTableName()).append(" WHERE ")
.append(get_WhereClause(true, null));
ResultSet rs = null;
PreparedStatement pstmt = null;
try
{
pstmt = DB.prepareStatement(sql.toString(), m_trxName);
rs = pstmt.executeQuery();
if (rs.next())
loadColumn(rs, index);
loadedVirtualColumns.add(index);
}catch(Exception e){
log.log(Level.SEVERE, "(rs) - " + String.valueOf(index)
+ ": " + p_info.getTableName() + "." + p_info.getColumnName(index)
+ " (" + p_info.getColumnClass(index) + ") - " + e);
}finally {
DB.close(rs, pstmt);
}
}
}
/**
* Load from HashMap
* @param hmIn hash map
@ -1587,7 +1678,7 @@ public abstract class PO
}
catch (Exception e)
{
if (p_info.isVirtualColumn(index)) { // if rs constructor used
if (p_info.isVirtualColumn(index)) {
if (log.isLoggable(Level.FINER))log.log(Level.FINER, "Virtual Column not loaded: " + columnName);
} else {
log.log(Level.SEVERE, "(ht) - " + String.valueOf(index)

View File

@ -802,8 +802,8 @@ public class POInfo implements Serializable
} // validate
/**
* Build select clause
* @return stringbuilder
* Build SQL SELECT statement.
* @return {@link StringBuilder} instance with the SQL statement.
*/
public StringBuilder buildSelect()
{
@ -811,12 +811,24 @@ public class POInfo implements Serializable
}
/**
* Build select clause
* @param fullyQualified
* @param noVirtualColumn
* @return stringbuilder
* Build SQL SELECT statement.
* @param fullyQualified prefix column names with the table name
* @param noVirtualColumn Include (<code>false</code> value) all declared virtual columns at once
* or use lazy loading (<code>true</code> value).
* @return {@link StringBuilder} instance with the SQL statement.
*/
public StringBuilder buildSelect(boolean fullyQualified, boolean noVirtualColumn)
public StringBuilder buildSelect(boolean fullyQualified, boolean noVirtualColumn) {
return buildSelect(fullyQualified, noVirtualColumn ? new String[] {} : null);
}
/**
* Build SQL SELECT statement.
* @param fullyQualified prefix column names with the table name
* @param virtualColumns names of virtual columns to include along with the regular table columns -
* if no value (or <code>null</code>) is provided then all declared virtual columns will be included.
* @return {@link StringBuilder} instance with the SQL statement.
*/
public StringBuilder buildSelect(boolean fullyQualified, String ... virtualColumns)
{
StringBuilder sql = new StringBuilder("SELECT ");
int size = getColumnCount();
@ -824,8 +836,20 @@ public class POInfo implements Serializable
for (int i = 0; i < size; i++)
{
boolean virtual = isVirtualColumn(i);
if (virtual && noVirtualColumn)
continue;
if (virtual && virtualColumns != null)
{
boolean found = false;
for(String virtualColumn : virtualColumns)
{
if(m_columns[i].ColumnName.equalsIgnoreCase(virtualColumn))
{
found = true;
break;
}
}
if(!found)
continue;
}
count++;
if (count > 1)

View File

@ -72,6 +72,12 @@ public class Query
private String orderBy = null;
private String trxName = null;
private Object[] parameters = null;
/**
* Name of virtual columns to be included in the query
*/
private String[] virtualColumns = null;
private boolean applyAccessFilter = false;
private boolean applyAccessFilterRW = false;
private boolean applyAccessFilterFullyQualified = true;
@ -79,7 +85,13 @@ public class Query
private boolean onlyClient_ID = false;
private int onlySelection_ID = -1;
private boolean forUpdate = false;
private boolean noVirtualColumn = false;
/**
* Whether to load (<code>false</code> value) all declared virtual columns at once or use
* lazy loading (<code>true</code> value).
*/
private boolean noVirtualColumn = true;
private int queryTimeout = 0;
private List<String> joinClauseList = new ArrayList<String>();
@ -248,6 +260,13 @@ public class Query
return this;
}
/**
* Virtual columns are lazy loaded by default. In case lazy loading is not desired use this method with
* the <code>false</code> value.
* @param noVirtualColumn Whether to load (<code>false</code> value) all declared virtual columns at once or use lazy loading (<code>true</code> value).
* @return
* @see #setVirtualColumns(String...)
*/
public Query setNoVirtualColumn(boolean noVirtualColumn)
{
this.noVirtualColumn = noVirtualColumn;
@ -685,8 +704,8 @@ public class Query
}
/**
* Build SQL Clause
* @param selectClause optional; if null the select clause will be build according to POInfo
* Build SQL SELECT statement.
* @param selectClause optional; if null the select statement will be built by {@link POInfo}
* @return final SQL
*/
private final String buildSQL(StringBuilder selectClause, boolean useOrderByClause)
@ -698,7 +717,11 @@ public class Query
{
throw new IllegalStateException("No POInfo found for AD_Table_ID="+table.getAD_Table_ID());
}
selectClause = info.buildSelect(!joinClauseList.isEmpty(), noVirtualColumn);
boolean isFullyQualified = !joinClauseList.isEmpty();
if(virtualColumns == null)
selectClause = info.buildSelect(isFullyQualified, noVirtualColumn);
else
selectClause = info.buildSelect(isFullyQualified, virtualColumns);
}
if (!joinClauseList.isEmpty())
{
@ -926,4 +949,13 @@ public class Query
return retValue;
} // get_IDs
/**
* Virtual columns to be included in the query.
* @param virtualColumns virtual column names
*/
public Query setVirtualColumns(String ... virtualColumns) {
this.virtualColumns = virtualColumns;
return this;
}
}

View File

@ -25,7 +25,7 @@ import org.compiere.util.KeyNamePair;
/** Generated Model for M_ProductionLine
* @author iDempiere (generated)
* @version Release 9 - $Id$ */
* @version Release 10 - $Id$ */
@org.adempiere.base.Model(table="M_ProductionLine")
public class X_M_ProductionLine extends PO implements I_M_ProductionLine, I_Persistent
{
@ -33,7 +33,7 @@ public class X_M_ProductionLine extends PO implements I_M_ProductionLine, I_Pers
/**
*
*/
private static final long serialVersionUID = 20211224L;
private static final long serialVersionUID = 20211226L;
/** Standard Constructor */
public X_M_ProductionLine (Properties ctx, int M_ProductionLine_ID, String trxName)
@ -385,39 +385,6 @@ public class X_M_ProductionLine extends PO implements I_M_ProductionLine, I_Pers
return false;
}
/** Set Product Type.
@param ProductType Type of product
*/
public void setProductType (String ProductType)
{
throw new IllegalArgumentException ("ProductType is virtual column"); }
/** Get Product Type.
@return Type of product
*/
public String getProductType()
{
return (String)get_Value(COLUMNNAME_ProductType);
}
/** Set Available Quantity.
@param QtyAvailable Available Quantity (On Hand - Reserved)
*/
public void setQtyAvailable (BigDecimal QtyAvailable)
{
throw new IllegalArgumentException ("QtyAvailable is virtual column"); }
/** Get Available Quantity.
@return Available Quantity (On Hand - Reserved)
*/
public BigDecimal getQtyAvailable()
{
BigDecimal bd = (BigDecimal)get_Value(COLUMNNAME_QtyAvailable);
if (bd == null)
return Env.ZERO;
return bd;
}
/** Set Quantity Used.
@param QtyUsed Quantity Used
*/

View File

@ -26,7 +26,7 @@ import org.compiere.util.KeyNamePair;
/** Generated Model for Test
* @author iDempiere (generated)
* @version Release 9 - $Id$ */
* @version Release 10 - $Id$ */
@org.adempiere.base.Model(table="Test")
public class X_Test extends PO implements I_Test, I_Persistent
{
@ -34,7 +34,7 @@ public class X_Test extends PO implements I_Test, I_Persistent
/**
*
*/
private static final long serialVersionUID = 20211224L;
private static final long serialVersionUID = 20220102L;
/** Standard Constructor */
public X_Test (Properties ctx, int Test_ID, String trxName)
@ -47,6 +47,17 @@ public class X_Test extends PO implements I_Test, I_Persistent
} */
}
/** Standard Constructor */
public X_Test (Properties ctx, int Test_ID, String trxName, String ... virtualColumns)
{
super (ctx, Test_ID, trxName, virtualColumns);
/** if (Test_ID == 0)
{
setName (null);
setTest_ID (0);
} */
}
/** Load Constructor */
public X_Test (Properties ctx, ResultSet rs, String trxName)
{
@ -530,6 +541,24 @@ public class X_Test extends PO implements I_Test, I_Persistent
return (String)get_Value(COLUMNNAME_Test_UU);
}
/** Set Virtual Quantity.
@param TestVirtualQty Used only for testing purposes
*/
public void setTestVirtualQty (BigDecimal TestVirtualQty)
{
throw new IllegalArgumentException ("TestVirtualQty is virtual column"); }
/** Get Virtual Quantity.
@return Used only for testing purposes
*/
public BigDecimal getTestVirtualQty()
{
BigDecimal bd = (BigDecimal)get_Value(COLUMNNAME_TestVirtualQty);
if (bd == null)
return Env.ZERO;
return bd;
}
/** Set Integer.
@param T_Integer Integer
*/

View File

@ -32,6 +32,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Properties;
import org.adempiere.exceptions.DBException;
@ -449,4 +451,21 @@ public class POTest extends AbstractTestCase
msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName());
assertEquals(msg1.getMsgText(), msg2.getMsgText());
}
@Test
public void testVirtualColumnLoad() {
MTest testPo = new MTest(Env.getCtx(), getClass().getName(), 1);
testPo.save();
// asynchronous (default) virtual column loading
assertTrue(null == testPo.get_ValueOld(MTest.COLUMNNAME_TestVirtualQty));
BigDecimal expected = new BigDecimal(123.45d).setScale(2, RoundingMode.HALF_DOWN);
assertTrue(expected.compareTo(testPo.getTestVirtualQty()) == 0);
// synchronous virtual column loading
testPo = new MTest(Env.getCtx(), testPo.get_ID(), getTrxName(), MTest.COLUMNNAME_TestVirtualQty);
assertTrue(null != testPo.get_ValueOld(MTest.COLUMNNAME_TestVirtualQty));
assertTrue(expected.compareTo(testPo.getTestVirtualQty()) == 0);
}
}

View File

@ -30,15 +30,21 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.adempiere.exceptions.DBException;
import org.adempiere.model.POWrapper;
import org.compiere.model.I_Test;
import org.compiere.model.MPInstance;
import org.compiere.model.MProcess;
import org.compiere.model.MTable;
import org.compiere.model.MTest;
import org.compiere.model.PO;
import org.compiere.model.POResultSet;
import org.compiere.model.Query;
import org.compiere.model.X_AD_Element;
@ -333,4 +339,34 @@ public class QueryTest extends AbstractTestCase {
assertEquals(expected, ids[i], "Element "+i+" not equals");
}
}
@Test
public void testVirtualColumnLoad() {
// create bogus record
PO testPo = new MTest(Env.getCtx(), getClass().getName(), 1);
testPo.save();
BigDecimal expected = new BigDecimal(123.45d).setScale(2, RoundingMode.HALF_DOWN);
// virtual column lazy loading
Query query = new Query(Env.getCtx(), MTest.Table_Name, MTest.COLUMNNAME_Test_ID + "=?", getTrxName());
testPo = query.setParameters(testPo.get_ID()).first();
I_Test testRecord = POWrapper.create(testPo, I_Test.class);
assertTrue(null == testPo.get_ValueOld(MTest.COLUMNNAME_TestVirtualQty));
assertTrue(expected.compareTo(testRecord.getTestVirtualQty()) == 0);
// without virtual column lazy loading
testPo = query.setNoVirtualColumn(false).setParameters(testPo.get_ID()).first();
assertTrue(null != testPo.get_ValueOld(MTest.COLUMNNAME_TestVirtualQty));
testRecord = POWrapper.create(testPo, I_Test.class);
assertTrue(expected.compareTo(testRecord.getTestVirtualQty()) == 0);
// single virtual column without lazy loading
testPo = query.setVirtualColumns(I_Test.COLUMNNAME_TestVirtualQty)
.setParameters(testPo.get_ID()).first();
assertTrue(null != testPo.get_ValueOld(MTest.COLUMNNAME_TestVirtualQty));
testRecord = POWrapper.create(testPo, I_Test.class);
assertTrue(expected.compareTo(testRecord.getTestVirtualQty()) == 0);
}
}