diff --git a/org.adempiere.base/src/org/compiere/model/PO.java b/org.adempiere.base/src/org/compiere/model/PO.java index 66edb3921a..a8b6a0f660 100644 --- a/org.adempiere.base/src/org/compiere/model/PO.java +++ b/org.adempiere.base/src/org/compiere/model/PO.java @@ -112,11 +112,13 @@ public abstract class PO /** * */ - private static final long serialVersionUID = 571979727987834997L; + private static final long serialVersionUID = 3153695115162945843L; public static final String LOCAL_TRX_PREFIX = "POSave"; private static final String USE_TIMEOUT_FOR_UPDATE = "org.adempiere.po.useTimeoutForUpdate"; + + private static final String USE_OPTIMISTIC_LOCKING = "org.idempiere.po.useOptimisticLocking"; /** default timeout, 300 seconds **/ private static final int QUERY_TIME_OUT = 300; @@ -290,6 +292,9 @@ public abstract class PO /** Immutable flag **/ private boolean m_isImmutable = false; + + private String[] m_optimisticLockingColumns = new String[] {"Updated"}; + private Boolean m_useOptimisticLocking = null; /** Access Level S__ 100 4 System info */ public static final int ACCESSLEVEL_SYSTEM = 4; @@ -2548,6 +2553,14 @@ public abstract class PO List params = new ArrayList(); String where = get_WhereClause(true); + + List optimisticLockingParams = new ArrayList(); + if (is_UseOptimisticLocking() && m_optimisticLockingColumns != null && m_optimisticLockingColumns.length > 0) + { + StringBuilder builder = new StringBuilder(where); + addOptimisticLockingClause(optimisticLockingParams, builder); + where = builder.toString(); + } // boolean changes = false; StringBuilder sql = new StringBuilder ("UPDATE "); @@ -2785,9 +2798,12 @@ public abstract class PO } } sql.append(" WHERE ").append(where); - /** @todo status locking goes here */ if (log.isLoggable(Level.FINEST)) log.finest(sql.toString()); + + if (is_UseOptimisticLocking() && optimisticLockingParams.size() > 0) + params.addAll(optimisticLockingParams); + int no = 0; if (isUseTimeoutForUpdate()) no = withValues ? DB.executeUpdateEx(sql.toString(), m_trxName, QUERY_TIME_OUT) @@ -2827,7 +2843,95 @@ public abstract class PO return true; } } + + private void addOptimisticLockingClause(List optimisticLockingParams, StringBuilder where) { + for(String oc : m_optimisticLockingColumns) + { + int index = get_ColumnIndex(oc); + if (index >= 0) + { + Class c = p_info.getColumnClass(index); + int dt = p_info.getColumnDisplayType(index); + if (DisplayType.isLOB(dt)) + continue; + Object value = get_ValueOld(oc); + if (value == null) + { + where.append(" AND ").append(oc).append(" IS NULL "); + } + else if (value instanceof Timestamp) + { + if (dt == DisplayType.Date) + where.append(" AND ").append(oc).append(" = trunc(cast(? as date))"); + else + where.append(" AND ").append(oc).append(" = ? "); + optimisticLockingParams.add(value); + } + else if (c == Boolean.class) + { + where.append(" AND ").append(oc).append(" = ? "); + boolean bValue = false; + if (value instanceof Boolean) + bValue = ((Boolean)value).booleanValue(); + else + bValue = "Y".equals(value); + optimisticLockingParams.add(encrypt(index,bValue ? "Y" : "N")); + } + else if (c == String.class) + { + if (value.toString().length() == 0) { + where.append(" AND ").append(oc).append(" = '' "); + } else { + where.append(" AND ").append(oc).append(" = ? "); + optimisticLockingParams.add(encrypt(index,value)); + } + } + else + { + where.append(" AND ").append(oc).append(" = ? "); + optimisticLockingParams.add(value); + } + + } + } + } + /** + * + * @return true if optimistic locking is enable + */ + public boolean is_UseOptimisticLocking() { + if (m_useOptimisticLocking != null) + return m_useOptimisticLocking; + else + return "true".equalsIgnoreCase(System.getProperty(USE_OPTIMISTIC_LOCKING, "false")); + } + + /** + * enable/disable optimistic locking + * @param enable + */ + public void set_UseOptimisticLocking(boolean enable) { + m_useOptimisticLocking = enable; + } + + /** + * + * @return optimistic locking columns + */ + public String[] get_OptimisticLockingColumns() { + return m_optimisticLockingColumns; + } + + /** + * set columns use for optimistic locking (auto add to where clause for update + * and delete) + * @param columns + */ + public void set_OptimisticLockingColumns(String[] columns) { + m_optimisticLockingColumns = columns; + } + private boolean isUseTimeoutForUpdate() { return "true".equalsIgnoreCase(System.getProperty(USE_TIMEOUT_FOR_UPDATE, "false")) && DB.getDatabase().isQueryTimeoutSupported(); @@ -3469,15 +3573,27 @@ public abstract class PO } // The Delete Statement + String where = get_WhereClause(true); + List optimisticLockingParams = new ArrayList(); + if (is_UseOptimisticLocking() && m_optimisticLockingColumns != null && m_optimisticLockingColumns.length > 0) + { + StringBuilder builder = new StringBuilder(where); + addOptimisticLockingClause(optimisticLockingParams, builder); + where = builder.toString(); + } StringBuilder sql = new StringBuilder ("DELETE FROM ") //jz why no FROM?? .append(p_info.getTableName()) .append(" WHERE ") - .append(get_WhereClause(true)); + .append(where); int no = 0; if (isUseTimeoutForUpdate()) - no = DB.executeUpdateEx(sql.toString(), localTrxName, QUERY_TIME_OUT); + no = optimisticLockingParams.isEmpty() + ? DB.executeUpdateEx(sql.toString(), localTrxName, QUERY_TIME_OUT) + : DB.executeUpdateEx(sql.toString(), optimisticLockingParams.toArray(), localTrxName, QUERY_TIME_OUT); else - no = DB.executeUpdate(sql.toString(), localTrxName); + no = optimisticLockingParams.isEmpty() + ? DB.executeUpdate(sql.toString(), localTrxName) + : DB.executeUpdate(sql.toString(), optimisticLockingParams.toArray(), false, localTrxName); success = no == 1; } catch (Exception e) diff --git a/org.idempiere.test/src/org/idempiere/test/base/POTest.java b/org.idempiere.test/src/org/idempiere/test/base/POTest.java index 28035b62fe..6c8ba429e0 100644 --- a/org.idempiere.test/src/org/idempiere/test/base/POTest.java +++ b/org.idempiere.test/src/org/idempiere/test/base/POTest.java @@ -35,7 +35,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Properties; import org.adempiere.exceptions.DBException; +import org.compiere.model.MBPartner; import org.compiere.model.MClient; +import org.compiere.model.MMessage; import org.compiere.model.MTest; import org.compiere.model.POInfo; import org.compiere.util.DB; @@ -319,4 +321,132 @@ public class POTest extends AbstractTestCase trx3.close(); } } + + @Test + public void testOptimisticLocking() + { + int joeBlock = 118; + MBPartner bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + MBPartner bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + + //normal update without optimistic locking + bp1.setDescription("bp1"); + boolean updated = bp1.save(); + assertTrue(updated); + + bp2.setDescription("bp2"); + updated = bp2.save(); + assertTrue(updated); + + //last update ok, description=bp2 + bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + assertEquals("bp2", bp1.getDescription()); + assertEquals("bp2", bp2.getDescription()); + + //test update with default optimistic locking using updated timestamp + bp1.set_UseOptimisticLocking(true); + bp1.setDescription("bp1"); + updated = bp1.save(); + assertTrue(updated); + + bp2.set_UseOptimisticLocking(true); + bp2.setDescription("bp2.1"); + updated = bp2.save(); + assertFalse(updated); + + //last update fail, description=bp1 + bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + assertEquals("bp1", bp1.getDescription()); + assertEquals("bp1", bp2.getDescription()); + + //test update with custom optimistic locking columns + bp1.set_UseOptimisticLocking(true); + bp1.setDescription("bp1.1"); + updated = bp1.save(); + assertTrue(updated); + + bp2.set_UseOptimisticLocking(true); + bp2.set_OptimisticLockingColumns(new String[] {"Name"}); + bp2.setDescription("bp2"); + updated = bp2.save(); + assertTrue(updated); + + //last update ok, description=bp2 + bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + assertEquals("bp2", bp1.getDescription()); + assertEquals("bp2", bp2.getDescription()); + + //test update with custom multiple column optimistic locking + bp1.set_UseOptimisticLocking(true); + bp1.setDescription("bp1"); + updated = bp1.save(); + assertTrue(updated); + + bp2.set_UseOptimisticLocking(true); + bp2.set_OptimisticLockingColumns(new String[] {"Name","Description"}); + bp2.setDescription("bp2.1"); + updated = bp2.save(); + assertFalse(updated); + + //last update fail, description=bp1 + bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName()); + assertEquals("bp1", bp1.getDescription()); + assertEquals("bp1", bp2.getDescription()); + + MMessage msg1 = new MMessage(Env.getCtx(), 0, getTrxName()); + msg1.setValue("msg1 test"); + msg1.setMsgText("msg1 test"); + msg1.setMsgType(MMessage.MSGTYPE_Information); + msg1.saveEx(); + + //test normal delete + updated = msg1.delete(true); + assertTrue(updated); + + msg1 = new MMessage(Env.getCtx(), 0, getTrxName()); + msg1.setValue("msg1 test"); + msg1.setMsgText("msg1 test"); + msg1.setMsgType(MMessage.MSGTYPE_Information); + msg1.saveEx(); + + //test delete with default optimistic locking + MMessage msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName()); + msg1.setMsgText("msg 1.1 test"); + msg1.saveEx(); + + msg2.set_UseOptimisticLocking(true); + updated = msg2.delete(true); + assertFalse(updated); + + //test delete with custom optimistic locking columns + msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName()); + assertEquals(msg1.getMsgText(), msg2.getMsgText()); + msg1.setMsgText("msg1 test"); + msg1.saveEx(); + msg2.set_UseOptimisticLocking(true); + msg2.set_OptimisticLockingColumns(new String[] {"Value"}); + updated = msg2.delete(true); + assertTrue(updated); + + //test delete with multiple custom optimistic locking columns + msg1 = new MMessage(Env.getCtx(), 0, getTrxName()); + msg1.setValue("msg1 test"); + msg1.setMsgText("msg1 test"); + msg1.setMsgType(MMessage.MSGTYPE_Information); + msg1.saveEx(); + msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName()); + msg1.setMsgText("msg 1.1 test"); + msg1.saveEx(); + msg2.set_UseOptimisticLocking(true); + msg2.set_OptimisticLockingColumns(new String[] {"Value", "MsgText"}); + updated = msg2.delete(true); + assertFalse(updated); + + msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName()); + assertEquals(msg1.getMsgText(), msg2.getMsgText()); + } }