package io.ebeaninternal.server.persist;

import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.server.core.PersistRequest;
import io.ebeaninternal.server.core.PersistRequestBean;
import io.ebeaninternal.server.core.PersistRequestUpdateSql;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

Controls the batch ordering of persist requests.

Persist requests include bean inserts updates deletes and UpdateSql and CallableSql requests.

This object queues up the requests into appropriate entries according to the 'depth' and the 'type' of the requests. The depth relates to how saves and deletes cascade following the associations of a bean. For saving Associated One cascades reduce the depth (-1) and associated many's increase the depth. The initial depth of a request is 0.

/** * Controls the batch ordering of persist requests. * <p> * Persist requests include bean inserts updates deletes and UpdateSql and * CallableSql requests. * </p> * <p> * This object queues up the requests into appropriate entries according to the * 'depth' and the 'type' of the requests. The depth relates to how saves and * deletes cascade following the associations of a bean. For saving Associated * One cascades reduce the depth (-1) and associated many's increase the depth. * The initial depth of a request is 0. * </p> */
public final class BatchControl {
Used to sort queue entries by depth.
/** * Used to sort queue entries by depth. */
private static final BatchDepthComparator depthComparator = new BatchDepthComparator();
Controls batching of the PreparedStatements. This should be flushed after each 'depth'.
/** * Controls batching of the PreparedStatements. This should be flushed after * each 'depth'. */
private final BatchedPstmtHolder pstmtHolder = new BatchedPstmtHolder();
Map of the BatchedBeanHolder objects. They each have a depth and are later sorted by their depth to get the execution order.
/** * Map of the BatchedBeanHolder objects. They each have a depth and are later * sorted by their depth to get the execution order. */
private final HashMap<String, BatchedBeanHolder> beanHoldMap = new HashMap<>(); private final SpiTransaction transaction;
The size at which the batch queue will flush. This should be close to the number of statements that are batched into a single PreparedStatement. This size relates to the size of a list in a BatchQueueEntry and not the total number of request which could be more than that.
/** * The size at which the batch queue will flush. This should be close to the * number of statements that are batched into a single PreparedStatement. This * size relates to the size of a list in a BatchQueueEntry and not the total * number of request which could be more than that. */
private int batchSize;
If true try to get generated keys from inserts.
/** * If true try to get generated keys from inserts. */
private boolean getGeneratedKeys; private boolean batchFlushOnMixed = true; private int maxDepth;
Size of the largest buffer.
/** * Size of the largest buffer. */
private int bufferMax; private int topCounter; private Queue earlyQueue; private Queue lateQueue;
Create for a given transaction, PersistExecute, default size and getGeneratedKeys.
/** * Create for a given transaction, PersistExecute, default size and getGeneratedKeys. */
public BatchControl(SpiTransaction t, int batchSize, boolean getGenKeys) { this.transaction = t; this.batchSize = batchSize; this.getGeneratedKeys = getGenKeys; transaction.setBatchControl(this); }
Set this flag to false to allow batching of a mix of Beans and UpdateSql (or CallableSql). Normally if you mix the two this will result in an automatic flush.

Note that UpdateSql and CallableSql will ALWAYS flush first. This is due to it already having been bound to a PreparedStatement where as the Beans go through a 2 step process when they are flushed (delayed binding).

/** * Set this flag to false to allow batching of a mix of Beans and UpdateSql * (or CallableSql). Normally if you mix the two this will result in an * automatic flush. * <p> * Note that UpdateSql and CallableSql will ALWAYS flush first. This is due to * it already having been bound to a PreparedStatement where as the Beans go * through a 2 step process when they are flushed (delayed binding). * </p> */
public void setBatchFlushOnMixed(boolean flushBatchOnMixed) { this.batchFlushOnMixed = flushBatchOnMixed; }
Return the batchSize.
/** * Return the batchSize. */
public int getBatchSize() { return batchSize; }
Set the size of batch execution.

The user can set this via the Transaction.

/** * Set the size of batch execution. * <p> * The user can set this via the Transaction. * </p> */
public void setBatchSize(int batchSize) { if (batchSize > 1) { this.batchSize = batchSize; } }
Set whether or not to use getGeneratedKeys for this batch execution.

The user can set this via the transaction

/** * Set whether or not to use getGeneratedKeys for this batch execution. * <p> * The user can set this via the transaction * </p> */
public void setGetGeneratedKeys(Boolean getGeneratedKeys) { if (getGeneratedKeys != null) { this.getGeneratedKeys = getGeneratedKeys; } }
Execute a Orm Update, SqlUpdate or CallableSql.

These all go straight to jdbc and use addBatch(). Entity beans goto a queue and wait there so that the jdbc is executed in the correct order according to the depth.

/** * Execute a Orm Update, SqlUpdate or CallableSql. * <p> * These all go straight to jdbc and use addBatch(). Entity beans goto a queue * and wait there so that the jdbc is executed in the correct order according * to the depth. * </p> */
public int executeStatementOrBatch(PersistRequest request, boolean batch) throws BatchedSqlException { if (!batch || (batchFlushOnMixed && !isBeansEmpty())) { // flush when mixing beans and updateSql flush(); } if (!batch) { // execute the request immediately without batching return request.executeNow(); } if (pstmtHolder.getMaxSize() >= batchSize) { flush(); } // for OrmUpdate, SqlUpdate, CallableSql there is no queue... // so straight to jdbc prepared statement and use addBatch(). // aka executeNow() may use addBatch(). request.executeNow(); return -1; }
Entity Bean insert, update or delete. This will either execute the request immediately or queue it for batch processing later. The queue is flushedIntercept according to the depth (object graph depth).
/** * Entity Bean insert, update or delete. This will either execute the request * immediately or queue it for batch processing later. The queue is flushedIntercept * according to the depth (object graph depth). */
public int executeOrQueue(PersistRequestBean<?> request, boolean batch) throws BatchedSqlException { if (!batch || (batchFlushOnMixed && !pstmtHolder.isEmpty())) { // flush when mixing beans and updateSql flush(); } if (!batch) { return request.executeNow(); } if (addToBatch(request)) { // flush as the top level has hit the batch size flush(); } return -1; }
Add the request to the batch and return true if we should flush.
/** * Add the request to the batch and return true if we should flush. */
private boolean addToBatch(PersistRequestBean<?> request) throws BatchedSqlException { BatchedBeanHolder beanHolder = getBeanHolder(request); int bufferSize = beanHolder.append(request); bufferMax = Math.max(bufferMax, bufferSize); // flush if any buffer hits 10 times batch size return (bufferMax >= batchSize * 10); }
Return the actual batch of PreparedStatements.
/** * Return the actual batch of PreparedStatements. */
public BatchedPstmtHolder getPstmtHolder() { return pstmtHolder; }
Return true if the queue is empty.
/** * Return true if the queue is empty. */
public boolean isEmpty() { return (isBeansEmpty() && pstmtHolder.isEmpty()); }
Flush any batched PreparedStatements.
/** * Flush any batched PreparedStatements. */
private void flushPstmtHolder() throws BatchedSqlException { pstmtHolder.flush(getGeneratedKeys); }
Execute all the requests contained in the list.
/** * Execute all the requests contained in the list. */
void executeNow(ArrayList<PersistRequest> list) throws BatchedSqlException { for (int i = 0; i < list.size(); i++) { if (i % batchSize == 0) { // hit the batch size so flush flushPstmtHolder(); } list.get(i).executeNow(); } flushPstmtHolder(); }
Flush without resetting the topOrder (maintains the depth info).
/** * Flush without resetting the topOrder (maintains the depth info). */
public void flush() throws BatchedSqlException { flushBuffer(false); }
Flush with a reset the topOrder (fully empty the batch).
/** * Flush with a reset the topOrder (fully empty the batch). */
public void flushReset() throws BatchedSqlException { flushBuffer(true); }
Clears the batch, discarding all batched statements.
/** * Clears the batch, discarding all batched statements. */
public void clear() { pstmtHolder.clear(); beanHoldMap.clear(); maxDepth = 0; topCounter = 0; } private void flushBuffer(boolean resetTop) throws BatchedSqlException { flushInternal(resetTop); flushQueue(earlyQueue); flushQueue(lateQueue); } private void flushQueue(Queue queue) throws BatchedSqlException { if (queue != null && queue.flush() && !pstmtHolder.isEmpty()) { flushPstmtHolder(); } }
execute all the requests currently queued or batched.
/** * execute all the requests currently queued or batched. */
private void flushInternal(boolean resetTop) throws BatchedSqlException { try { bufferMax = 0; if (!pstmtHolder.isEmpty()) { // Flush existing pstmts (updateSql or callableSql) flushPstmtHolder(); } if (isEmpty()) { // Nothing in queue to flush return; } // convert entry map to array for sorting BatchedBeanHolder[] bsArray = getBeanHolderArray(); // sort the entries by depth Arrays.sort(bsArray, depthComparator); if (transaction.isLogSummary()) { transaction.logSummary("BatchControl flush " + Arrays.toString(bsArray)); } for (BatchedBeanHolder beanHolder : bsArray) { beanHolder.executeNow(); } if (resetTop) { beanHoldMap.clear(); maxDepth = 0; } } catch (BatchedSqlException e) { // clear the batch on error in case we want to // catch, rollback and continue processing clear(); throw e; } }
Return an entry for the given type description. The type description is typically the bean class name (or table name for MapBeans).
/** * Return an entry for the given type description. The type description is * typically the bean class name (or table name for MapBeans). */
private BatchedBeanHolder getBeanHolder(PersistRequestBean<?> request) throws BatchedSqlException { BeanDescriptor<?> beanDescriptor = request.getBeanDescriptor(); BatchedBeanHolder batchBeanHolder = beanHoldMap.get(beanDescriptor.rootName()); if (batchBeanHolder == null) { int relativeDepth = transaction.depth(); int beanDepth = 100 + relativeDepth; if (relativeDepth == 0 && !beanHoldMap.isEmpty()) { // could be non-cascading or uni-directional relationship // so see look for a 'parent' in the beanHoldMap int maybe = relativeToParentDepth(beanDescriptor); if (maybe != -1) { beanDepth = maybe; } else { // additional "top level" bean type ordered by save() order beanDepth += ++topCounter; } } maxDepth = Math.max(maxDepth, beanDepth); batchBeanHolder = new BatchedBeanHolder(this, beanDescriptor, beanDepth); beanHoldMap.put(beanDescriptor.rootName(), batchBeanHolder); } return batchBeanHolder; }
Find a depth based on imported relationships (to a parent that is already in the buffer).
/** * Find a depth based on imported relationships (to a parent that is already in the buffer). */
private int relativeToParentDepth(BeanDescriptor<?> beanDescriptor) { BeanPropertyAssocOne<?>[] imported = beanDescriptor.propertiesOneImported(); if (imported.length == 0) { // a top level type so just maintain the order relative to the current depth return maxDepth + 1; } int parentMaxDepth = -1; for (BeanPropertyAssocOne<?> parent : imported) { BatchedBeanHolder parentBatch = beanHoldMap.get(parent.getTargetDescriptor().rootName()); if (parentBatch != null) { // deeper that the parent parentMaxDepth = Math.max(parentMaxDepth, parentBatch.getOrder() + 1); } } return parentMaxDepth; }
Return true if this holds no persist requests.
/** * Return true if this holds no persist requests. */
private boolean isBeansEmpty() { if (beanHoldMap.isEmpty()) { return true; } for (BatchedBeanHolder beanHolder : beanHoldMap.values()) { if (!beanHolder.isEmpty()) { return false; } } return true; }
Return the BatchedBeanHolder's ready for sorting and executing.
/** * Return the BatchedBeanHolder's ready for sorting and executing. */
private BatchedBeanHolder[] getBeanHolderArray() { return beanHoldMap.values().toArray(new BatchedBeanHolder[beanHoldMap.size()]); }
Execute a batched statement.
/** * Execute a batched statement. */
public int[] execute(String key, boolean getGeneratedKeys) throws SQLException { return pstmtHolder.execute(key, getGeneratedKeys); }
Add a SqlUpdate request to execute after flush.
/** * Add a SqlUpdate request to execute after flush. */
public void addToFlushQueue(PersistRequestUpdateSql request, boolean early) { if (early) { // add it to the early queue if (earlyQueue == null) { earlyQueue = new Queue(); } earlyQueue.add(request); } else { // add it to the late queue if (lateQueue == null) { lateQueue = new Queue(); } lateQueue.add(request); } } private static class Queue { private final List<PersistRequestUpdateSql> queue = new ArrayList<>(); boolean flush() { if (queue.isEmpty()) { return false; } for (PersistRequestUpdateSql request : queue) { request.executeAddBatch(); } queue.clear(); return true; } void add(PersistRequestUpdateSql request) { queue.add(request); } } }