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; boolean errorLogged = false;
try try
{ {
Exception ce = null;
Object[] arguments = null;
Constructor<?> constructor = null; Constructor<?> constructor = null;
try try
{ {
constructor = clazz.getDeclaredConstructor(new Class[]{Properties.class, int.class, String.class}); constructor = clazz.getDeclaredConstructor(new Class[]{Properties.class, int.class, String.class});
arguments = new Object[] {Env.getCtx(), Integer.valueOf(Record_ID), trxName};
} }
catch (Exception e) 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) if (msg == null)
msg = e.toString(); msg = ce.toString();
s_log.warning("No transaction Constructor for " + clazz + " (" + msg + ")"); 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; return po;
} }
catch (Exception e) catch (Exception e)

View File

@ -53,7 +53,7 @@ public class GenericPO extends PO implements DocAction {
* @param ID * @param ID
*/ */
public GenericPO(String tableName, Properties ctx, int 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 * @param trxName
*/ */
public GenericPO(String tableName, Properties ctx, int ID, String 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) .append(" }").append(NL)
// Constructor End // 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 // Load Constructor
.append(NL) .append(NL)
.append(" /** Load Constructor */").append(NL) .append(" /** Load Constructor */").append(NL)
@ -267,7 +280,7 @@ public class ModelClassGenerator
+ "FROM AD_Column c " + "FROM AD_Column c "
+ "WHERE c.AD_Table_ID=?" + "WHERE c.AD_Table_ID=?"
+ " AND c.ColumnName NOT IN ('AD_Client_ID', 'AD_Org_ID', 'IsActive', 'Created', 'CreatedBy', 'Updated', 'UpdatedBy')" + " 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 : "") + (!Util.isEmpty(entityTypeFilter) ? " AND c." + entityTypeFilter : "")
+ " ORDER BY c.ColumnName"; + " ORDER BY c.ColumnName";
boolean isKeyNamePairCreated = false; // true if the method "getKeyNamePair" is already generated boolean isKeyNamePairCreated = false; // true if the method "getKeyNamePair" is already generated

View File

@ -248,7 +248,7 @@ public class ModelInterfaceGenerator
+ "FROM AD_Column c " + "FROM AD_Column c "
+ "WHERE c.AD_Table_ID=?" + "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 : "") + (!Util.isEmpty(entityTypeFilter) ? " AND c." + entityTypeFilter : "")
+ " ORDER BY c.ColumnName"; + " ORDER BY c.ColumnName";
PreparedStatement pstmt = null; PreparedStatement pstmt = null;

View File

@ -22,7 +22,7 @@ import org.compiere.util.KeyNamePair;
/** Generated Interface for M_ProductionLine /** Generated Interface for M_ProductionLine
* @author iDempiere (generated) * @author iDempiere (generated)
* @version Release 9 * @version Release 10
*/ */
public interface I_M_ProductionLine public interface I_M_ProductionLine
{ {
@ -266,32 +266,6 @@ public interface I_M_ProductionLine
*/ */
public boolean isProcessed(); 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 */ /** Column name QtyUsed */
public static final String COLUMNNAME_QtyUsed = "QtyUsed"; public static final String COLUMNNAME_QtyUsed = "QtyUsed";

View File

@ -348,6 +348,19 @@ public interface I_Test
/** Get Test_UU */ /** Get Test_UU */
public String getTest_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 */ /** Column name T_Integer */
public static final String COLUMNNAME_T_Integer = "T_Integer"; public static final String COLUMNNAME_T_Integer = "T_Integer";

View File

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

View File

@ -33,9 +33,11 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
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.Set;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -147,20 +149,32 @@ public abstract class PO
*/ */
public PO (Properties ctx) public PO (Properties ctx)
{ {
this (ctx, 0, null, null); this (ctx, 0, null, null, (String[]) null);
} // PO } // PO
/** /**
* Create and Load existing Persistent Object * 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 ctx context
* @param trxName transaction name * @param trxName transaction name
*/ */
public PO (Properties ctx, int ID, String trxName) public PO (Properties ctx, int ID, String trxName)
{ {
this (ctx, ID, trxName, null); this (ctx, ID, trxName, null, (String[]) null);
} // PO } // 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. * Create and Load existing Persistent Object.
* @param ctx context * @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 ID the ID if 0, the record defaults are applied - ignored if re exists
* @param trxName transaction name * @param trxName transaction name
* @param rs optional - load from current result set position (no navigation, not closed) * @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(); p_ctx = ctx != null ? ctx : Env.getCtx();
m_trxName = trxName; m_trxName = trxName;
@ -207,9 +222,9 @@ public abstract class PO
m_setErrorsFilled = false; m_setErrorsFilled = false;
if (rs != null) if (rs != null)
load(rs); // will not have virtual columns load(rs);
else else
load(ID, trxName); load(ID, trxName, virtualColumns);
checkCrossTenant(false); checkCrossTenant(false);
} // PO } // PO
@ -223,7 +238,7 @@ public abstract class PO
*/ */
public PO (Properties ctx, PO source, int AD_Client_ID, int AD_Org_ID) 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) if (source != null)
copyValues (source, this); copyValues (source, this);
@ -296,6 +311,9 @@ public abstract class PO
private String[] m_optimisticLockingColumns = new String[] {"Updated"}; private String[] m_optimisticLockingColumns = new String[] {"Updated"};
private Boolean m_useOptimisticLocking = null; 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 */ /** Access Level S__ 100 4 System info */
public static final int ACCESSLEVEL_SYSTEM = 4; public static final int ACCESSLEVEL_SYSTEM = 4;
/** Access Level _C_ 010 2 Client info */ /** Access Level _C_ 010 2 Client info */
@ -487,6 +505,8 @@ public abstract class PO
return null; return null;
return m_newValues[index]; return m_newValues[index];
} }
if(p_info.isVirtualColumn(index) && p_info.isVirtualDBColumn(index))
loadVirtualColumn(index);
return m_oldValues[index]; return m_oldValues[index];
} // get_Value } // get_Value
@ -1325,8 +1345,9 @@ public abstract class PO
* Load record with ID * Load record with ID
* @param ID ID * @param ID ID
* @param trxName transaction name * @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(); checkImmutable();
@ -1335,7 +1356,7 @@ public abstract class PO
{ {
setKeyInfo(); setKeyInfo();
m_IDs = new Object[] {Integer.valueOf(ID)}; m_IDs = new Object[] {Integer.valueOf(ID)};
load(trxName); load(trxName, virtualColumns);
} }
else // new else // new
{ {
@ -1349,10 +1370,11 @@ public abstract class PO
/** /**
* Load record with UUID * Load record with UUID
* *
* @param uuID UUID * @param uuID universally unique identifier
* @param trxName transaction name * @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)) if (Util.isEmpty(uuID, true))
{ {
@ -1366,25 +1388,27 @@ public abstract class PO
if (log.isLoggable(Level.FINEST)) if (log.isLoggable(Level.FINEST))
log.finest("uuID=" + uuID); log.finest("uuID=" + uuID);
load(uuID,trxName); load(uuID,trxName, virtualColumns);
} // loadByUU } // loadByUU
/** /**
* (re)Load record with m_ID[*] * (re)Load record with m_ID[*]
* @param trxName transaction * @param trxName transaction
* @param virtualColumns names of virtual columns to load along with the regular table columns
* @return true if loaded * @return true if loaded
*/ */
public boolean load (String trxName) { public boolean load (String trxName, String ... virtualColumns) {
return load(null, trxName); return load(null, trxName, virtualColumns);
} }
/** /**
* (re)Load record with uuID * (re)Load record with uuID
* @param uuID RecrodUU * @param uuID RecrodUU
* @param trxName transaction * @param trxName transaction
* @param virtualColumns names of virtual columns to load along with the regular table columns
* @return true if loaded * @return true if loaded
*/ */
protected boolean load (String uuID,String trxName) protected boolean load (String uuID, String trxName, String ... virtualColumns)
{ {
m_trxName = trxName; m_trxName = trxName;
boolean success = true; boolean success = true;
@ -1392,14 +1416,33 @@ public abstract class PO
int size = get_ColumnCount(); int size = get_ColumnCount();
for (int i = 0; i < size; i++) for (int i = 0; i < size; i++)
{ {
if (i != 0)
sql.append(",");
String columnSQL = p_info.getColumnSQL(i); 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); 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()) sql.append(" FROM ").append(p_info.getTableName())
.append(" WHERE ") .append(" WHERE ")
@ -1480,61 +1523,12 @@ public abstract class PO
boolean success = true; boolean success = true;
int index = 0; int index = 0;
log.finest("(rs)"); log.finest("(rs)");
loadedVirtualColumns.clear();
// load column values // load column values
for (index = 0; index < size; index++) for (index = 0; index < size; index++)
{ {
String columnName = p_info.getColumnName(index); if(!loadColumn(rs, index) && success)
Class<?> clazz = p_info.getColumnClass(index); success = false;
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;
}
}
} }
m_createNew = false; m_createNew = false;
setKeyInfo(); setKeyInfo();
@ -1542,6 +1536,103 @@ public abstract class PO
return success; return success;
} // load } // 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 * Load from HashMap
* @param hmIn hash map * @param hmIn hash map
@ -1587,7 +1678,7 @@ public abstract class PO
} }
catch (Exception e) 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); if (log.isLoggable(Level.FINER))log.log(Level.FINER, "Virtual Column not loaded: " + columnName);
} else { } else {
log.log(Level.SEVERE, "(ht) - " + String.valueOf(index) log.log(Level.SEVERE, "(ht) - " + String.valueOf(index)

View File

@ -800,23 +800,35 @@ public class POInfo implements Serializable
} }
return null; return null;
} // validate } // validate
/** /**
* Build select clause * Build SQL SELECT statement.
* @return stringbuilder * @return {@link StringBuilder} instance with the SQL statement.
*/ */
public StringBuilder buildSelect() public StringBuilder buildSelect()
{ {
return buildSelect(false, false); return buildSelect(false, false);
} }
/** /**
* Build select clause * Build SQL SELECT statement.
* @param fullyQualified * @param fullyQualified prefix column names with the table name
* @param noVirtualColumn * @param noVirtualColumn Include (<code>false</code> value) all declared virtual columns at once
* @return stringbuilder * 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 "); StringBuilder sql = new StringBuilder("SELECT ");
int size = getColumnCount(); int size = getColumnCount();
@ -824,9 +836,21 @@ public class POInfo implements Serializable
for (int i = 0; i < size; i++) for (int i = 0; i < size; i++)
{ {
boolean virtual = isVirtualColumn(i); boolean virtual = isVirtualColumn(i);
if (virtual && noVirtualColumn) if (virtual && virtualColumns != null)
continue; {
boolean found = false;
for(String virtualColumn : virtualColumns)
{
if(m_columns[i].ColumnName.equalsIgnoreCase(virtualColumn))
{
found = true;
break;
}
}
if(!found)
continue;
}
count++; count++;
if (count > 1) if (count > 1)
sql.append(","); sql.append(",");

View File

@ -72,6 +72,12 @@ public class Query
private String orderBy = null; private String orderBy = null;
private String trxName = null; private String trxName = null;
private Object[] parameters = 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 applyAccessFilter = false;
private boolean applyAccessFilterRW = false; private boolean applyAccessFilterRW = false;
private boolean applyAccessFilterFullyQualified = true; private boolean applyAccessFilterFullyQualified = true;
@ -79,7 +85,13 @@ public class Query
private boolean onlyClient_ID = false; private boolean onlyClient_ID = false;
private int onlySelection_ID = -1; private int onlySelection_ID = -1;
private boolean forUpdate = false; 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 int queryTimeout = 0;
private List<String> joinClauseList = new ArrayList<String>(); private List<String> joinClauseList = new ArrayList<String>();
@ -247,7 +259,14 @@ public class Query
this.forUpdate = forUpdate; this.forUpdate = forUpdate;
return this; 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) public Query setNoVirtualColumn(boolean noVirtualColumn)
{ {
this.noVirtualColumn = noVirtualColumn; this.noVirtualColumn = noVirtualColumn;
@ -685,8 +704,8 @@ public class Query
} }
/** /**
* Build SQL Clause * Build SQL SELECT statement.
* @param selectClause optional; if null the select clause will be build according to POInfo * @param selectClause optional; if null the select statement will be built by {@link POInfo}
* @return final SQL * @return final SQL
*/ */
private final String buildSQL(StringBuilder selectClause, boolean useOrderByClause) 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()); 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()) if (!joinClauseList.isEmpty())
{ {
@ -926,4 +949,13 @@ public class Query
return retValue; return retValue;
} // get_IDs } // 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 /** Generated Model for M_ProductionLine
* @author iDempiere (generated) * @author iDempiere (generated)
* @version Release 9 - $Id$ */ * @version Release 10 - $Id$ */
@org.adempiere.base.Model(table="M_ProductionLine") @org.adempiere.base.Model(table="M_ProductionLine")
public class X_M_ProductionLine extends PO implements I_M_ProductionLine, I_Persistent 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 */ /** Standard Constructor */
public X_M_ProductionLine (Properties ctx, int M_ProductionLine_ID, String trxName) 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; 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. /** Set Quantity Used.
@param QtyUsed Quantity Used @param QtyUsed Quantity Used
*/ */

View File

@ -26,7 +26,7 @@ import org.compiere.util.KeyNamePair;
/** Generated Model for Test /** Generated Model for Test
* @author iDempiere (generated) * @author iDempiere (generated)
* @version Release 9 - $Id$ */ * @version Release 10 - $Id$ */
@org.adempiere.base.Model(table="Test") @org.adempiere.base.Model(table="Test")
public class X_Test extends PO implements I_Test, I_Persistent 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 */ /** Standard Constructor */
public X_Test (Properties ctx, int Test_ID, String trxName) 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 */ /** Load Constructor */
public X_Test (Properties ctx, ResultSet rs, String trxName) 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); 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. /** Set Integer.
@param T_Integer 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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Properties; import java.util.Properties;
import org.adempiere.exceptions.DBException; import org.adempiere.exceptions.DBException;
@ -449,4 +451,21 @@ public class POTest extends AbstractTestCase
msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName()); msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName());
assertEquals(msg1.getMsgText(), msg2.getMsgText()); 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.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp; 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 org.adempiere.exceptions.DBException; 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.MPInstance;
import org.compiere.model.MProcess; import org.compiere.model.MProcess;
import org.compiere.model.MTable; import org.compiere.model.MTable;
import org.compiere.model.MTest;
import org.compiere.model.PO;
import org.compiere.model.POResultSet; import org.compiere.model.POResultSet;
import org.compiere.model.Query; import org.compiere.model.Query;
import org.compiere.model.X_AD_Element; import org.compiere.model.X_AD_Element;
@ -333,4 +339,34 @@ public class QueryTest extends AbstractTestCase {
assertEquals(expected, ids[i], "Element "+i+" not equals"); 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);
}
} }