This is an automated email from the ASF dual-hosted git repository.

pgil pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new d7a1b40d1f Improved: Add delegator method to create in mass many 
entities (OFBIZ-13176) (#857)
d7a1b40d1f is described below

commit d7a1b40d1f5243cf38b2d755efcea12058e432f3
Author: Nicolas Malin <nicolas.ma...@nereide.fr>
AuthorDate: Fri Nov 15 14:43:03 2024 +0100

    Improved: Add delegator method to create in mass many entities 
(OFBIZ-13176) (#857)
    
    Improved: Add delegator method to create in mass many entities (OFBIZ-13176)
    
    When you need to load huge record on your database (like system 
synchronization, data import) OFBiz can have latency with the delegator.create.
    Incriminated unitary workflow and eeca control. You can disable eeca on use 
a dedicate delegator but sometime it's not enough.
    
    So for special case, we implement a new function 
delegator.createAllByBatchProcess that will use an insert with one transaction 
through the standard sql method INSERT TO ... VALUES ... .
    
    By the way, like the process isn't unitary, the eeca system can't run on 
this batch, so this function must therefore be used with care
    
    Improve test to validate multi entity creation by batch.
    
    Thanks to Néréide team for this contribution
    
    ---------
    
    Co-authored-by: Gil Portenseigne <gil.portensei...@nereide.fr>
---
 .../java/org/apache/ofbiz/entity/Delegator.java    | 28 ++++++++++++
 .../org/apache/ofbiz/entity/GenericDelegator.java  | 52 ++++++++++++++++++++++
 .../apache/ofbiz/entity/datasource/GenericDAO.java | 52 ++++++++++++++++++++++
 .../ofbiz/entity/datasource/GenericHelper.java     |  7 +++
 .../ofbiz/entity/datasource/GenericHelperDAO.java  | 14 ++++++
 .../ofbiz/entity/datasource/ReadOnlyHelperDAO.java |  8 ++++
 .../org/apache/ofbiz/entity/jdbc/SQLProcessor.java | 24 ++++++++++
 .../apache/ofbiz/entity/test/EntityTestSuite.java  | 19 ++++++++
 8 files changed, 204 insertions(+)

diff --git 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/Delegator.java 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/Delegator.java
index 73da59c8a7..bdd7a91252 100644
--- a/framework/entity/src/main/java/org/apache/ofbiz/entity/Delegator.java
+++ b/framework/entity/src/main/java/org/apache/ofbiz/entity/Delegator.java
@@ -197,6 +197,34 @@ public interface Delegator {
      */
     GenericValue createSingle(String entityName, Object singlePkValue) throws 
GenericEntityException;
 
+    /**
+     * <p>Create the Entities from the List GenericValue instances to the 
persistent
+     * store.</p>
+     * <p>This is different than the normal create method, because all creation
+     * will be done with one unique insert to go fast.</p>
+     * <p>For this reason eca can't be raised, so it's useful for process
+     * with huge data to inject into database</p>
+     * @param values
+     *            List of GenericValue instances containing the entities to 
create
+     */
+    void createAllByBatchProcess(List<GenericValue> values) throws 
GenericEntityException;
+
+    /**
+     * <p>Create the Entities from the List GenericValue instances to the 
persistent
+     * store.</p>
+     * <p>This is different than the normal create method, because all creation
+     * will be done with one unique insert to go fast.</p>
+     * <p>For this reason eca can't be raised, so it's useful for process
+     * with huge data to inject on database.</p>
+     * <p>As this is a huge process, we can specify whether we want to alert 
the ofbiz cluster
+     * if we need to clean their cache or just wait the normal expiration</p>
+     * @param values
+     *            List of GenericValue instances containing the entities to 
create
+     * @param distribute
+     *            set to true if we want to clean cache entity for ofbiz 
cluster
+     */
+    void createAllByBatchProcess(List<GenericValue> values, boolean 
distribute) throws GenericEntityException;
+
     Object decryptFieldValue(String entityName, ModelField.EncryptMethod 
encryptMethod, String encValue) throws EntityCryptoException;
 
     Object encryptFieldValue(String entityName, ModelField.EncryptMethod 
encryptMethod, Object fieldValue) throws EntityCryptoException;
diff --git 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/GenericDelegator.java 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/GenericDelegator.java
index ea2e46438d..2c0f34efde 100644
--- 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/GenericDelegator.java
+++ 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/GenericDelegator.java
@@ -39,6 +39,7 @@ import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
 
+import java.util.stream.Collectors;
 import javax.xml.parsers.ParserConfigurationException;
 
 import org.apache.ofbiz.base.concurrent.ConstantFuture;
@@ -922,6 +923,57 @@ public class GenericDelegator implements Delegator {
         }
     }
 
+    /* (non-Javadoc)
+     * @see 
org.apache.ofbiz.entity.Delegator#createAllByBatchProcess(org.apache.ofbiz.entity.Delegator)
+     */
+    @Override
+    public void createAllByBatchProcess(List<GenericValue> values, boolean 
distribute) throws GenericEntityException {
+        boolean beganTransaction = ALWAYS_USE_TRANS
+                ? TransactionUtil.begin()
+                : false;
+
+        if (UtilValidate.isEmpty(values)) {
+            throw new GenericEntityException("No value to store");
+        }
+        Map<String, List<GenericValue>> sortedValuesByEntityName = 
values.stream()
+                .collect(Collectors.groupingBy(GenericValue::getEntityName));
+        for (Map.Entry<String, List<GenericValue>> entry : 
sortedValuesByEntityName.entrySet()) {
+            List<GenericValue> entityValues = entry.getValue();
+            String entityName = entry.getKey();
+            try {
+                GenericHelper helper = getEntityHelper(entityName);
+
+                helper.createAll(entityValues);
+
+                if (testMode) {
+                    entityValues.forEach(v ->
+                            storeForTestRollback(new 
TestOperation(OperationType.INSERT, v)));
+                }
+                if (distribute) {
+                    this.clearCacheLine(entityName);
+                }
+
+                TransactionUtil.commit(beganTransaction);
+            } catch (IllegalStateException | GenericEntityException e) {
+                String errMsg = String.format(
+                        "Failure in create operation for list of entity on %s"
+                                + " for %s elements with error : %s. Rolling 
back transaction.",
+                        entityName, entityValues.size(), e);
+                Debug.logError(errMsg, MODULE);
+                TransactionUtil.rollback(beganTransaction, errMsg, e);
+                throw new GenericEntityException(e);
+            }
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see 
org.apache.ofbiz.entity.Delegator#createAllByBatchProcess(org.apache.ofbiz.entity.Delegator)
+     */
+    @Override
+    public void createAllByBatchProcess(List<GenericValue> values) throws 
GenericEntityException {
+        createAllByBatchProcess(values, true);
+    }
+
     /* (non-Javadoc)
      * @see 
org.apache.ofbiz.entity.Delegator#createOrStore(org.apache.ofbiz.entity.GenericValue)
      */
diff --git 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericDAO.java
 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericDAO.java
index 2348ae96cb..404bb82d55 100644
--- 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericDAO.java
+++ 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericDAO.java
@@ -122,6 +122,58 @@ public class GenericDAO {
         }
     }
 
+    /**
+     * Insert in database all GenericValue of the same entity
+     * @param entities
+     * @return number of entity inserted in database
+     * @throws GenericEntityException
+     */
+    public int insertAll(List<GenericValue> entities) throws 
GenericEntityException {
+        GenericEntity entity = entities.get(0);
+        ModelEntity modelEntity = entity.getModelEntity();
+
+        try (SQLProcessor sqlP = new SQLProcessor(entity.getDelegator(), 
helperInfo)) {
+            try {
+                return multiInsert(entities, modelEntity, 
modelEntity.getFieldsUnmodifiable(), sqlP);
+            } catch (GenericEntityException e) {
+                sqlP.rollback();
+                throw e;
+            }
+        }
+    }
+
+    private int multiInsert(List<GenericValue> entities, ModelEntity 
modelEntity, List<ModelField> fieldsToSave,
+                            SQLProcessor sqlP) throws GenericEntityException {
+        if (modelEntity instanceof ModelViewEntity) {
+            throw new GenericEntityException("Not Implemented");
+        }
+
+        StringBuilder sqlB = new StringBuilder("INSERT INTO ")
+                .append(modelEntity.getTableName(datasource))
+                .append(" (");
+
+        modelEntity.colNameString(fieldsToSave, sqlB, "");
+        sqlB.append(") VALUES (");
+        modelEntity.fieldsStringList(fieldsToSave, sqlB, "?", ", ");
+        sqlB.append(")");
+
+        try {
+            sqlP.prepareStatement(sqlB.toString());
+            for (GenericEntity ent : entities) {
+                SqlJdbcUtil.setValues(sqlP, fieldsToSave, ent, 
modelFieldTypeReader);
+                sqlP.addBatch();
+            }
+            int retVal = sqlP.executeBatch();
+
+            for (GenericEntity ent : entities) {
+                ent.synchronizedWithDatasource();
+            }
+            return retVal;
+        } catch (GenericEntityException | SQLException e) {
+            throw new GenericEntityException("Error while inserting: " + sqlB, 
e);
+        }
+    }
+
     private int singleInsert(GenericEntity entity, ModelEntity modelEntity, 
List<ModelField> fieldsToSave, SQLProcessor sqlP)
             throws GenericEntityException {
         if (modelEntity instanceof ModelViewEntity) {
diff --git 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericHelper.java
 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericHelper.java
index 33020dfd1e..bc64d50ade 100644
--- 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericHelper.java
+++ 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericHelper.java
@@ -53,6 +53,13 @@ public interface GenericHelper {
      */
     GenericValue create(GenericValue value) throws GenericEntityException;
 
+    /**
+     * Insert a given list of GenericValue to the database
+     * @param values
+     * @return The list of GenericValue created
+     */
+    List<GenericValue> createAll(List<GenericValue> values) throws 
GenericEntityException;
+
     /** Find a Generic Entity by its Primary Key
      *@param primaryKey The primary key to find by.
      *@return The GenericValue corresponding to the primaryKey
diff --git 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericHelperDAO.java
 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericHelperDAO.java
index 2cc1e333eb..8ea09c59a5 100644
--- 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericHelperDAO.java
+++ 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/GenericHelperDAO.java
@@ -72,6 +72,20 @@ public class GenericHelperDAO implements GenericHelper {
         return value;
     }
 
+    /** Insert a given list of GenericValue to the database
+     *@return List of GenericValue instance created
+     */
+    public List<GenericValue> createAll(List<GenericValue> values) throws 
GenericEntityException {
+        if (values.isEmpty()) {
+            return null;
+        }
+        int retVal = genericDAO.insertAll(values);
+        if (Debug.verboseOn()) {
+            Debug.logVerbose("Insert Return Value : " + retVal, MODULE);
+        }
+        return values;
+    }
+
     /** Find a Generic Entity by its Primary Key
      *@param primaryKey The primary key to find by.
      *@return The GenericValue corresponding to the primaryKey
diff --git 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/ReadOnlyHelperDAO.java
 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/ReadOnlyHelperDAO.java
index 0ab31dd337..0511b31292 100644
--- 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/ReadOnlyHelperDAO.java
+++ 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/datasource/ReadOnlyHelperDAO.java
@@ -64,6 +64,14 @@ public class ReadOnlyHelperDAO implements GenericHelper {
         return null;
     }
 
+    /** Read only, no creation realize on the database
+     *@return null
+     */
+    @Override
+    public List<GenericValue> createAll(List<GenericValue> value) throws 
GenericEntityException {
+        return null;
+    }
+
     /** Find a Generic Entity by its Primary Key
      *@param primaryKey The primary key to find by.
      *@return The GenericValue corresponding to the primaryKey
diff --git 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/jdbc/SQLProcessor.java 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/jdbc/SQLProcessor.java
index 30c2a7f2f1..d3afd8d326 100644
--- 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/jdbc/SQLProcessor.java
+++ 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/jdbc/SQLProcessor.java
@@ -33,6 +33,7 @@ import java.sql.SQLException;
 import java.sql.Statement;
 import java.sql.Types;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import org.apache.ofbiz.base.util.Debug;
@@ -858,4 +859,27 @@ public class SQLProcessor implements AutoCloseable {
             TransactionUtil.printAllThreadsTransactionBeginStacks();
         }
     }
+
+    /**
+     * Ask the processor to execute the batch and return the number of rows 
updated
+     * @return The number of rows updated
+     * @throws GenericDataSourceException
+     */
+    public int executeBatch() throws GenericDataSourceException {
+        try {
+            return Arrays.stream(ps.executeBatch()).sum();
+        } catch (SQLException sqle) {
+            this.checkLockWaitInfo(sqle);
+            throw new GenericDataSourceException("SQL Exception while 
executing the following:" + sql, sqle);
+        }
+    }
+
+    /**
+     * Add to the processor a batch treatment
+     * @throws SQLException
+     */
+    public void addBatch() throws SQLException {
+        this.ind = 1;
+        this.getPreparedStatement().addBatch();
+    }
 }
diff --git 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/test/EntityTestSuite.java
 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/test/EntityTestSuite.java
index b95fc8e85d..19e1664367 100644
--- 
a/framework/entity/src/main/java/org/apache/ofbiz/entity/test/EntityTestSuite.java
+++ 
b/framework/entity/src/main/java/org/apache/ofbiz/entity/test/EntityTestSuite.java
@@ -184,6 +184,25 @@ public class EntityTestSuite extends EntityTestCase {
         assertEquals("Finding removed value returns null", null, testValue);
     }
 
+    /**
+     * Test to load huge entity
+     * @throws Exception the exception
+     */
+    public void testCreateAllValues() throws Exception {
+        List<GenericValue> testValues = new ArrayList<>(100);
+        for (int i = 0; i < 100; i++) {
+            testValues.add(getDelegator().makeValue("TestingType",
+                    "testingTypeId", "TEST_CREATE_ALL" + i,
+                    "description", "Testing Type #CreateAll" + i));
+            testValues.add(getDelegator().makeValue("Testing", "testingId", 
"TEST_CREATE_DIST" + i));
+        }
+        getDelegator().createAllByBatchProcess(testValues);
+        assertEquals("create all insert 100 elements", 100, 
getDelegator().findCountByCondition("TestingType",
+                EntityCondition.makeCondition("testingTypeId", 
EntityOperator.LIKE, "TEST_CREATE_ALL%"), null, null));
+        assertEquals("create all insert 100 elements", 100, 
getDelegator().findCountByCondition("Testing",
+                EntityCondition.makeCondition("testingId", 
EntityOperator.LIKE, "TEST_CREATE_DIST%"), null, null));
+    }
+
     /**
      * Tests the entity cache
      * @throws Exception the exception

Reply via email to