/**
* Copyright 2011-2017 Asakusa Framework Team.
*
* 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 com.asakusafw.windgate.jdbc;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.MessageFormat;
import java.util.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.asakusafw.windgate.core.WindGateLogger;
import com.asakusafw.windgate.core.resource.DrainDriver;
import com.asakusafw.windgate.core.vocabulary.JdbcProcess;
import com.asakusafw.windgate.core.vocabulary.DataModelJdbcSupport.DataModelPreparedStatement;
/**
* An implementation of {@link DrainDriver} using JDBC.
* @param <T> the type of data model object
* @since 0.2.2
* @version 0.9.0
*/
public class JdbcDrainDriver<T> implements DrainDriver<T> {
static final WindGateLogger WGLOG = new JdbcLogger(JdbcDrainDriver.class);
static final Logger LOG = LoggerFactory.getLogger(JdbcDrainDriver.class);
private final JdbcProfile profile;
private final JdbcScript<T> script;
private final Connection connection;
private final boolean truncateOnPrepare;
private final long batchPutUnit;
private long putLimitRest;
private long putCount;
private PreparedStatement statement;
private DataModelPreparedStatement<? super T> support;
private boolean sawError;
/**
* Creates a new instance.
* @param profile the profile of the target database
* @param script the script of this action
* @param connection the connection
* @param truncateOnPrepare {@code true} to truncate the target table on preparation
* @throws IllegalArgumentException if any parameter is {@code null}
*/
public JdbcDrainDriver(
JdbcProfile profile,
JdbcScript<T> script,
Connection connection,
boolean truncateOnPrepare) {
if (profile == null) {
throw new IllegalArgumentException("profile must not be null"); //$NON-NLS-1$
}
if (script == null) {
throw new IllegalArgumentException("script must not be null"); //$NON-NLS-1$
}
if (connection == null) {
throw new IllegalArgumentException("connection must not be null"); //$NON-NLS-1$
}
this.profile = profile;
this.script = script;
this.connection = connection;
this.batchPutUnit = profile.getBatchPutUnit();
this.truncateOnPrepare = truncateOnPrepare;
}
@Override
public void prepare() throws IOException {
LOG.debug("Preparing JDBC resource drain (resource={}, table={})",
profile.getResourceName(),
script.getTableName());
try {
if (truncateOnPrepare) {
truncate();
}
} catch (SQLException e) {
sawError = true;
for (SQLException ex = e; ex != null; ex = ex.getNextException()) {
WGLOG.error(ex, "E04001",
profile.getResourceName(),
script.getName(),
script.getTableName());
}
throw new IOException(MessageFormat.format(
"Failed to prepare JDBC drain (resource={0}, table={1}, columns={2})",
profile.getResourceName(),
script.getTableName(),
script.getColumnNames()), e);
}
try {
this.statement = prepareStatement();
} catch (SQLException e) {
sawError = true;
for (SQLException ex = e; ex != null; ex = ex.getNextException()) {
WGLOG.error(ex, "E04002",
profile.getResourceName(),
script.getName(),
script.getTableName(),
script.getColumnNames());
}
throw new IOException(MessageFormat.format(
"Failed to prepare JDBC drain (resource={0}, table={1}, columns={2})",
profile.getResourceName(),
script.getTableName(),
script.getColumnNames()), e);
}
LOG.debug("Creating PreparedStatement support {} for {}",
script.getSupport().getClass().getName(),
script.getColumnNames());
support = script.getSupport().createPreparedStatementSupport(statement, script.getColumnNames());
putLimitRest = batchPutUnit;
}
private void truncate() throws SQLException {
String sql;
if (script.getCustomTruncate() == null) {
sql = profile.getTruncateStatement(script.getTableName());
} else {
sql = script.getCustomTruncate();
}
try (Statement truncater = connection.createStatement()) {
WGLOG.info("I04001",
profile.getResourceName(),
script.getName(),
script.getTableName());
LOG.debug("Executing SQL: {}", sql);
truncater.execute(sql);
LOG.debug("Executed SQL: {}", sql);
connection.commit();
LOG.debug("Committed {}", sql);
}
}
private PreparedStatement prepareStatement() throws SQLException {
String sql = createSql();
LOG.debug("Preparing SQL: {}", sql);
return connection.prepareStatement(sql);
}
private String createSql() {
assert script.getColumnNames().isEmpty() == false;
assert script.getCondition() == null;
StringBuilder buf = new StringBuilder();
buf.append("INSERT ");
if (isOptimizationEnabled(JdbcProcess.OptionSymbols.ORACLE_DIRPATH)) {
buf.append("/*+APPEND_VALUES*/ ");
}
buf.append("INTO ");
buf.append(script.getTableName());
buf.append(" (");
buf.append(String.join(",", script.getColumnNames())); //$NON-NLS-1$
buf.append(") ");
buf.append("VALUES ");
buf.append("(");
buf.append(String.join(",", Collections.nCopies(script.getColumnNames().size(), "?"))); //$NON-NLS-1$ //$NON-NLS-2$
buf.append(")");
return buf.toString();
}
private boolean isOptimizationEnabled(String symbol) {
return profile.getOptimizations().contains(symbol) && script.getOptions().contains(symbol);
}
@Override
public void put(T object) throws IOException {
try {
support.setParameters(object);
statement.addBatch();
} catch (SQLException e) {
sawError = true;
for (SQLException ex = e; ex != null; ex = ex.getNextException()) {
WGLOG.error(ex, "E04003",
profile.getResourceName(),
script.getName(),
script.getTableName(),
script.getColumnNames());
}
throw new IOException(MessageFormat.format(
"Failed to put object to JDBC drain: {2} (resource={0}, table={1})",
profile.getResourceName(),
script.getTableName(),
object), e);
}
putLimitRest--;
if (putLimitRest == 0) {
flush();
}
assert putLimitRest > 0;
}
private void flush() throws IOException {
assert putLimitRest != batchPutUnit;
try {
LOG.debug("Flushing {} rows into {}",
batchPutUnit - putLimitRest,
script.getTableName());
statement.executeBatch();
connection.commit();
putCount += batchPutUnit - putLimitRest;
putLimitRest = batchPutUnit;
} catch (SQLException e) {
sawError = true;
for (SQLException ex = e; ex != null; ex = ex.getNextException()) {
WGLOG.error(ex, "E04004",
profile.getResourceName(),
script.getName(),
script.getTableName(),
script.getColumnNames());
}
throw new IOException(MessageFormat.format(
"Failed to flush table into JDBC drain (resource={0}, table={1})",
profile.getResourceName(),
script.getTableName()), e);
}
}
@Override
public void close() throws IOException {
LOG.debug("Closing JDBC resource drain (resource={}, table={})",
profile.getResourceName(),
script.getTableName());
IOException occurred = null;
if (statement != null) {
if (sawError == false && putLimitRest != batchPutUnit) {
try {
flush();
} catch (IOException e) {
occurred = e;
}
}
try {
statement.close();
} catch (SQLException e) {
for (SQLException ex = e; ex != null; ex = ex.getNextException()) {
WGLOG.warn(ex, "W04001",
profile.getResourceName(),
script.getName(),
script.getTableName(),
script.getColumnNames());
}
}
}
try {
connection.close();
} catch (SQLException e) {
for (SQLException ex = e; ex != null; ex = ex.getNextException()) {
WGLOG.warn(ex, "W02001",
profile.getResourceName(),
script.getName());
}
}
if (occurred != null) {
throw occurred;
}
}
}