/* * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jkiss.dbeaver.model.impl.data; import org.jkiss.dbeaver.Log; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; import org.jkiss.dbeaver.model.data.DBDDataReceiver; import org.jkiss.dbeaver.model.data.DBDValueHandler; import org.jkiss.dbeaver.model.edit.DBEPersistAction; import org.jkiss.dbeaver.model.exec.*; import org.jkiss.dbeaver.model.impl.edit.SQLDatabasePersistAction; import org.jkiss.dbeaver.model.struct.DBSAttributeBase; import org.jkiss.dbeaver.model.struct.DBSDataManipulator; import org.jkiss.utils.ArrayUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Execute batch. * Can be used in JDBC or any other underlying DB APIs * */ public abstract class ExecuteBatchImpl implements DBSDataManipulator.ExecuteBatch { private static final Log log = Log.getLog(ExecuteBatchImpl.class); protected final DBSAttributeBase[] attributes; protected final List<Object[]> values = new ArrayList<>(); protected final DBDDataReceiver keysReceiver; protected final boolean reuseStatement; /** * Constructs new batch * @param attributes array of attributes used in batch * @param keysReceiver keys receiver (or null) * @param reuseStatement true if engine should reuse single prepared statement for each execution. */ protected ExecuteBatchImpl(@NotNull DBSAttributeBase[] attributes, @Nullable DBDDataReceiver keysReceiver, boolean reuseStatement) { this.attributes = attributes; this.keysReceiver = keysReceiver; this.reuseStatement = reuseStatement; } @Override public void add(@NotNull Object[] attributeValues) throws DBCException { if (!ArrayUtils.isEmpty(attributes) && ArrayUtils.isEmpty(attributeValues)) { throw new DBCException("Bad attribute values: " + Arrays.toString(attributeValues)); } values.add(attributeValues); } @NotNull @Override public DBCStatistics execute(@NotNull DBCSession session) throws DBCException { return processBatch(session, null); } @Override public void generatePersistActions(@NotNull DBCSession session, @NotNull List<DBEPersistAction> actions) throws DBCException { processBatch(session, actions); } /** * Execute batch OR generate batch script. * @param session session * @param actions script actions. If not null then no execution will be done * @return execution statistics * @throws DBCException */ @NotNull private DBCStatistics processBatch(@NotNull DBCSession session, @Nullable List<DBEPersistAction> actions) throws DBCException { DBDValueHandler[] handlers = new DBDValueHandler[attributes.length]; for (int i = 0; i < attributes.length; i++) { if (attributes[i] instanceof DBDAttributeBinding) { handlers[i] = ((DBDAttributeBinding)attributes[i]).getValueHandler(); } else { handlers[i] = DBUtils.findValueHandler(session, attributes[i]); } } boolean useBatch = session.getDataSource().getInfo().supportsBatchUpdates() && reuseStatement; if (values.size() <= 1) { useBatch = false; } DBCStatistics statistics = new DBCStatistics(); DBCStatement statement = null; try { // Here we'll try to reuse prepared statement. // It makes a great sense in case of data transfer where we need millions of inserts. // We must be aware of nulls because actual insert statements may differ depending on null values. // So if row nulls aren't the same as in previous row we need to prepare new statement and restart batch. // Quite complicated but works. boolean[] prevNulls = new boolean[attributes.length]; boolean[] nulls = new boolean[attributes.length]; int statementsInBatch = 0; for (Object[] rowValues : values) { boolean reuse = reuseStatement; if (reuse) { for (int i = 0; i < rowValues.length; i++) { nulls[i] = DBUtils.isNullValue(rowValues[i]); } if (!Arrays.equals(prevNulls, nulls) && statementsInBatch > 0) { reuse = false; } System.arraycopy(nulls, 0, prevNulls, 0, nulls.length); if (!reuse && statementsInBatch > 0) { // Flush batch if (actions == null) { flushBatch(statistics, statement); } statement.close(); statement = null; statementsInBatch = 0; reuse = true; } } if (statement == null || !reuse) { statement = prepareStatement(session, rowValues); statistics.setQueryText(statement.getQueryString()); statistics.addStatementsCount(); } try { bindStatement(handlers, statement, rowValues); if (actions == null) { if (useBatch) { statement.addToBatch(); statementsInBatch++; } else { // Execute each row separately long startTime = System.currentTimeMillis(); executeStatement(statement); statistics.addExecuteTime(System.currentTimeMillis() - startTime); long rowCount = statement.getUpdateRowCount(); if (rowCount > 0) { statistics.addRowsUpdated(rowCount); } // Read keys if (keysReceiver != null) { readKeys(statement.getSession(), statement, keysReceiver); } } } else { String queryString; if (statement instanceof DBCParameterizedStatement) { queryString = ((DBCParameterizedStatement)statement).getFormattedQuery(); } else { queryString = statement.getQueryString(); } actions.add( new SQLDatabasePersistAction( "Execute statement", queryString)); } } finally { if (!reuse) { statement.close(); } } } values.clear(); if (statementsInBatch > 0) { if (actions == null) { flushBatch(statistics, statement); } statement.close(); statement = null; } } finally { if (reuseStatement && statement != null) { statement.close(); } } return statistics; } private void flushBatch(DBCStatistics statistics, DBCStatement statement) throws DBCException { long startTime = System.currentTimeMillis(); int[] updatedRows = statement.executeStatementBatch(); statistics.addExecuteTime(System.currentTimeMillis() - startTime); if (!ArrayUtils.isEmpty(updatedRows)) { for (int rows : updatedRows) { statistics.addRowsUpdated(rows); } } } @Override public void close() { } private void readKeys(@NotNull DBCSession session, @NotNull DBCStatement dbStat, @NotNull DBDDataReceiver keysReceiver) throws DBCException { DBCResultSet dbResult; try { dbResult = dbStat.openGeneratedKeysResultSet(); } catch (Throwable e) { log.debug("Error obtaining generated keys", e); //$NON-NLS-1$ return; } if (dbResult == null) { return; } try { keysReceiver.fetchStart(session, dbResult, -1, -1); try { while (dbResult.nextRow()) { keysReceiver.fetchRow(session, dbResult); } } finally { keysReceiver.fetchEnd(session, dbResult); } } finally { dbResult.close(); keysReceiver.close(); } } @NotNull protected abstract DBCStatement prepareStatement(@NotNull DBCSession session, Object[] attributeValues) throws DBCException; protected abstract void bindStatement(@NotNull DBDValueHandler[] handlers, @NotNull DBCStatement statement, Object[] attributeValues) throws DBCException; protected void executeStatement(DBCStatement statement) throws DBCException { statement.executeStatement(); } }