package com.revolsys.jdbc.io; import java.sql.BatchUpdateException; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import javax.annotation.PreDestroy; import javax.sql.DataSource; import org.apache.log4j.Logger; import org.springframework.dao.DataAccessException; import com.revolsys.io.AbstractRecordWriter; import com.revolsys.io.FileUtil; import com.revolsys.io.PathName; import com.revolsys.jdbc.JdbcConnection; import com.revolsys.jdbc.JdbcUtils; import com.revolsys.jdbc.field.JdbcFieldDefinition; import com.revolsys.record.Record; import com.revolsys.record.RecordState; import com.revolsys.record.property.GlobalIdProperty; import com.revolsys.record.schema.FieldDefinition; import com.revolsys.record.schema.RecordDefinition; import com.revolsys.record.schema.RecordStore; import com.revolsys.transaction.Transaction; import com.revolsys.util.count.CategoryLabelCountMap; public class JdbcWriterImpl extends AbstractRecordWriter { private static final Logger LOG = Logger.getLogger(JdbcWriterImpl.class); private int batchSize = 1; private JdbcConnection connection; private boolean flushBetweenTypes = false; private String hints = null; private String label; private RecordDefinition lastRecordDefinition; private boolean quoteColumnNames = true; private JdbcRecordStore recordStore; private String sqlPrefix; private String sqlSuffix; private CategoryLabelCountMap statistics; private boolean throwExceptions = false; private final Map<PathName, Integer> typeCountMap = new LinkedHashMap<>(); private Map<PathName, Integer> typeDeleteBatchCountMap = new LinkedHashMap<>(); private Map<PathName, String> typeDeleteSqlMap = new LinkedHashMap<>(); private Map<PathName, PreparedStatement> typeDeleteStatementMap = new LinkedHashMap<>(); private Map<PathName, Integer> typeInsertBatchCountMap = new LinkedHashMap<>(); private Map<PathName, Integer> typeInsertSequenceBatchCountMap = new LinkedHashMap<>(); private Map<PathName, String> typeInsertSequenceSqlMap = new LinkedHashMap<>(); private Map<PathName, PreparedStatement> typeInsertSequenceStatementMap = new LinkedHashMap<>(); private Map<PathName, String> typeInsertSqlMap = new LinkedHashMap<>(); private Map<PathName, PreparedStatement> typeInsertStatementMap = new LinkedHashMap<>(); private Map<PathName, Integer> typeUpdateBatchCountMap = new LinkedHashMap<>(); private Map<PathName, String> typeUpdateSqlMap = new LinkedHashMap<>(); private Map<PathName, PreparedStatement> typeUpdateStatementMap = new LinkedHashMap<>(); public JdbcWriterImpl(final JdbcRecordStore recordStore) { this(recordStore, recordStore.getStatistics()); } public JdbcWriterImpl(final JdbcRecordStore recordStore, final CategoryLabelCountMap statistics) { this.recordStore = recordStore; this.statistics = statistics; this.connection = recordStore.getJdbcConnection(); final DataSource dataSource = this.connection.getDataSource(); if (dataSource != null) { try { this.connection.setAutoCommit(false); } catch (final SQLException e) { throw new RuntimeException("Unable to create connection", e); } } statistics.connect(); } private void addSqlColumEqualsPlaceholder(final StringBuilder sqlBuffer, final JdbcFieldDefinition attribute) { final String fieldName = attribute.getName(); if (this.quoteColumnNames) { sqlBuffer.append('"').append(fieldName).append('"'); } else { sqlBuffer.append(fieldName); } sqlBuffer.append(" = "); attribute.addInsertStatementPlaceHolder(sqlBuffer, false); } @Override @PreDestroy public void close() { flush(); closeDo(); } private void close(final Map<PathName, String> sqlMap, final Map<PathName, PreparedStatement> statementMap, final Map<PathName, Integer> batchCountMap) { for (final Entry<PathName, PreparedStatement> entry : statementMap.entrySet()) { final PathName typePath = entry.getKey(); final PreparedStatement statement = entry.getValue(); final String sql = sqlMap.get(typePath); try { processCurrentBatch(typePath, sql, statement, batchCountMap); } catch (final DataAccessException e) { if (this.throwExceptions) { throw e; } else { LOG.error("Error commiting records", e); } } JdbcUtils.close(statement); } } protected synchronized void closeDo() { if (this.recordStore != null) { try { close(this.typeInsertSqlMap, this.typeInsertStatementMap, this.typeInsertBatchCountMap); close(this.typeInsertSequenceSqlMap, this.typeInsertSequenceStatementMap, this.typeInsertSequenceBatchCountMap); close(this.typeUpdateSqlMap, this.typeUpdateStatementMap, this.typeUpdateBatchCountMap); close(this.typeDeleteSqlMap, this.typeDeleteStatementMap, this.typeDeleteBatchCountMap); if (this.statistics != null) { this.statistics.disconnect(); this.statistics = null; } } finally { this.typeInsertSqlMap = null; this.typeInsertStatementMap = null; this.typeInsertBatchCountMap = null; this.typeInsertSequenceSqlMap = null; this.typeInsertSequenceStatementMap = null; this.typeInsertSequenceBatchCountMap = null; this.typeUpdateBatchCountMap = null; this.typeUpdateSqlMap = null; this.typeUpdateStatementMap = null; this.typeDeleteBatchCountMap = null; this.typeDeleteSqlMap = null; this.typeDeleteStatementMap = null; this.recordStore = null; if (this.connection != null) { final DataSource dataSource = this.connection.getDataSource(); try { if (dataSource != null && !Transaction.isHasCurrentTransaction()) { this.connection.commit(); } } catch (final SQLException e) { throw new RuntimeException("Failed to commit data:", e); } finally { FileUtil.closeSilent(this.connection); this.connection = null; } } } } } public synchronized void commit() { flush(); JdbcUtils.commit(this.connection); } private void delete(final Record object) throws SQLException { final RecordDefinition objectType = object.getRecordDefinition(); final PathName typePath = objectType.getPathName(); final RecordDefinition recordDefinition = getRecordDefinition(typePath); flushIfRequired(recordDefinition); PreparedStatement statement = this.typeDeleteStatementMap.get(typePath); if (statement == null) { final String sql = getDeleteSql(recordDefinition); try { statement = this.connection.prepareStatement(sql); this.typeDeleteStatementMap.put(typePath, statement); } catch (final SQLException e) { LOG.error(sql, e); } } int parameterIndex = 1; final JdbcFieldDefinition idField = (JdbcFieldDefinition)recordDefinition.getIdField(); parameterIndex = idField.setInsertPreparedStatementValue(statement, parameterIndex, object); statement.addBatch(); Integer batchCount = this.typeDeleteBatchCountMap.get(typePath); if (batchCount == null) { batchCount = 1; this.typeDeleteBatchCountMap.put(typePath, 1); } else { batchCount += 1; this.typeDeleteBatchCountMap.put(typePath, batchCount); } this.recordStore.addStatistic("Delete", object); // TODO this locks code tables which prevents insert // if (batchCount >= batchSize) { // final String sql = getDeleteSql(recordDefinition); // processCurrentBatch(typePath, sql, statement, typeDeleteBatchCountMap, // getDeleteStatistics()); // } } @Override public synchronized void flush() { flush(this.typeInsertSqlMap, this.typeInsertStatementMap, this.typeInsertBatchCountMap); flush(this.typeInsertSequenceSqlMap, this.typeInsertSequenceStatementMap, this.typeInsertSequenceBatchCountMap); flush(this.typeUpdateSqlMap, this.typeUpdateStatementMap, this.typeUpdateBatchCountMap); flush(this.typeDeleteSqlMap, this.typeDeleteStatementMap, this.typeDeleteBatchCountMap); } private void flush(final Map<PathName, String> sqlMap, final Map<PathName, PreparedStatement> statementMap, final Map<PathName, Integer> batchCountMap) { if (statementMap != null) { for (final Entry<PathName, PreparedStatement> entry : statementMap.entrySet()) { final PathName typePath = entry.getKey(); final PreparedStatement statement = entry.getValue(); final String sql = sqlMap.get(typePath); try { processCurrentBatch(typePath, sql, statement, batchCountMap); } catch (final DataAccessException e) { if (this.throwExceptions) { throw e; } else { LOG.error("Error writing to database", e); } } } } } private void flushIfRequired(final RecordDefinition recordDefinition) { if (this.flushBetweenTypes && recordDefinition != this.lastRecordDefinition) { flush(); this.lastRecordDefinition = recordDefinition; } } public int getBatchSize() { return this.batchSize; } private String getDeleteSql(final RecordDefinition type) { final PathName typePath = type.getPathName(); final String tableName = this.recordStore.getDatabaseQualifiedTableName(typePath); String sql = this.typeDeleteSqlMap.get(typePath); if (sql == null) { final StringBuilder sqlBuffer = new StringBuilder(); if (this.sqlPrefix != null) { sqlBuffer.append(this.sqlPrefix); } sqlBuffer.append("delete "); if (this.hints != null) { sqlBuffer.append(this.hints); } sqlBuffer.append(" from "); sqlBuffer.append(tableName); sqlBuffer.append(" where "); final JdbcFieldDefinition idField = (JdbcFieldDefinition)type.getIdField(); if (idField == null) { throw new RuntimeException("No primary key found for " + type); } addSqlColumEqualsPlaceholder(sqlBuffer, idField); sqlBuffer.append(" "); if (this.sqlSuffix != null) { sqlBuffer.append(this.sqlSuffix); } sql = sqlBuffer.toString(); this.typeDeleteSqlMap.put(typePath, sql); } return sql; } private String getGeneratePrimaryKeySql(final RecordDefinition recordDefinition) { return this.recordStore.getGeneratePrimaryKeySql(recordDefinition); } /** * @return the hints */ public String getHints() { return this.hints; } private String getInsertSql(final RecordDefinition type, final boolean generatePrimaryKey) { final PathName typePath = type.getPathName(); final String tableName = this.recordStore.getDatabaseQualifiedTableName(typePath); String sql; if (generatePrimaryKey) { sql = this.typeInsertSequenceSqlMap.get(typePath); } else { sql = this.typeInsertSqlMap.get(typePath); } if (sql == null) { final StringBuilder sqlBuffer = new StringBuilder(); if (this.sqlPrefix != null) { sqlBuffer.append(this.sqlPrefix); } sqlBuffer.append("insert "); if (this.hints != null) { sqlBuffer.append(this.hints); } sqlBuffer.append(" into "); sqlBuffer.append(tableName); sqlBuffer.append(" ("); if (generatePrimaryKey) { final String idFieldName = type.getIdFieldName(); if (this.quoteColumnNames) { sqlBuffer.append('"').append(idFieldName).append('"'); } else { sqlBuffer.append(idFieldName); } sqlBuffer.append(","); } for (int i = 0; i < type.getFieldCount(); i++) { if (!generatePrimaryKey || i != type.getIdFieldIndex()) { final String fieldName = type.getFieldName(i); if (this.quoteColumnNames) { sqlBuffer.append('"').append(fieldName).append('"'); } else { sqlBuffer.append(fieldName); } if (i < type.getFieldCount() - 1) { sqlBuffer.append(", "); } } } sqlBuffer.append(") VALUES ("); if (generatePrimaryKey) { sqlBuffer.append(getGeneratePrimaryKeySql(type)); sqlBuffer.append(","); } for (int i = 0; i < type.getFieldCount(); i++) { if (!generatePrimaryKey || i != type.getIdFieldIndex()) { final JdbcFieldDefinition attribute = (JdbcFieldDefinition)type.getField(i); attribute.addInsertStatementPlaceHolder(sqlBuffer, generatePrimaryKey); if (i < type.getFieldCount() - 1) { sqlBuffer.append(", "); } } } sqlBuffer.append(")"); if (this.sqlSuffix != null) { sqlBuffer.append(this.sqlSuffix); } sql = sqlBuffer.toString(); if (generatePrimaryKey) { this.typeInsertSequenceSqlMap.put(typePath, sql); } else { this.typeInsertSqlMap.put(typePath, sql); } } return sql; } public String getLabel() { return this.label; } private RecordDefinition getRecordDefinition(final PathName typePath) { if (this.recordStore == null) { return null; } else { final RecordDefinition recordDefinition = this.recordStore.getRecordDefinition(typePath); return recordDefinition; } } public String getSqlPrefix() { return this.sqlPrefix; } public String getSqlSuffix() { return this.sqlSuffix; } private String getUpdateSql(final RecordDefinition type) { final PathName typePath = type.getPathName(); final String tableName = this.recordStore.getDatabaseQualifiedTableName(typePath); String sql = this.typeUpdateSqlMap.get(typePath); if (sql == null) { final StringBuilder sqlBuffer = new StringBuilder(); if (this.sqlPrefix != null) { sqlBuffer.append(this.sqlPrefix); } sqlBuffer.append("update "); if (this.hints != null) { sqlBuffer.append(this.hints); } sqlBuffer.append(tableName); sqlBuffer.append(" set "); final List<FieldDefinition> idFields = type.getIdFields(); boolean first = true; for (final FieldDefinition attribute : type.getFields()) { if (!idFields.contains(attribute)) { final JdbcFieldDefinition jdbcAttribute = (JdbcFieldDefinition)attribute; if (first) { first = false; } else { sqlBuffer.append(", "); } addSqlColumEqualsPlaceholder(sqlBuffer, jdbcAttribute); } } sqlBuffer.append(" where "); first = true; for (final FieldDefinition idField : idFields) { if (first) { first = false; } else { sqlBuffer.append(" AND "); } final JdbcFieldDefinition idJdbcAttribute = (JdbcFieldDefinition)idField; addSqlColumEqualsPlaceholder(sqlBuffer, idJdbcAttribute); } sqlBuffer.append(" "); if (this.sqlSuffix != null) { sqlBuffer.append(this.sqlSuffix); } sql = sqlBuffer.toString(); this.typeUpdateSqlMap.put(typePath, sql); } return sql; } private void insert(final Record record) throws SQLException { final PathName typePath = record.getPathName(); final RecordDefinition recordDefinition = getRecordDefinition(typePath); flushIfRequired(recordDefinition); final String idFieldName = recordDefinition.getIdFieldName(); final boolean hasId = idFieldName != null; final GlobalIdProperty globalIdProperty = GlobalIdProperty.getProperty(record); if (globalIdProperty != null) { if (record.getValue(globalIdProperty.getFieldName()) == null) { record.setValue(globalIdProperty.getFieldName(), UUID.randomUUID().toString()); } } final boolean hasIdValue = hasId && record.getValue(idFieldName) != null; if (!hasId || hasIdValue) { insert(record, typePath, recordDefinition); } else { insertSequence(record, typePath, recordDefinition); } record.setState(RecordState.PERSISTED); this.recordStore.addStatistic("Insert", record); } private void insert(final Record object, final PathName typePath, final RecordDefinition recordDefinition) throws SQLException { PreparedStatement statement = this.typeInsertStatementMap.get(typePath); if (statement == null) { final String sql = getInsertSql(recordDefinition, false); try { statement = this.connection.prepareStatement(sql); this.typeInsertStatementMap.put(typePath, statement); } catch (final SQLException e) { LOG.error(sql, e); } } int parameterIndex = 1; for (final FieldDefinition fieldDefinition : recordDefinition.getFields()) { final JdbcFieldDefinition jdbcFieldDefinition = (JdbcFieldDefinition)fieldDefinition; parameterIndex = jdbcFieldDefinition.setInsertPreparedStatementValue(statement, parameterIndex, object); } statement.addBatch(); Integer batchCount = this.typeInsertBatchCountMap.get(typePath); if (batchCount == null) { batchCount = 1; this.typeInsertBatchCountMap.put(typePath, 1); } else { batchCount += 1; this.typeInsertBatchCountMap.put(typePath, batchCount); } if (batchCount >= this.batchSize) { final String sql = getInsertSql(recordDefinition, false); processCurrentBatch(typePath, sql, statement, this.typeInsertBatchCountMap); } } private void insertSequence(final Record object, final PathName typePath, final RecordDefinition recordDefinition) throws SQLException { PreparedStatement statement = this.typeInsertSequenceStatementMap.get(typePath); if (statement == null) { final String sql = getInsertSql(recordDefinition, true); try { statement = this.connection.prepareStatement(sql); this.typeInsertSequenceStatementMap.put(typePath, statement); } catch (final SQLException e) { LOG.error(sql, e); } } int parameterIndex = 1; final FieldDefinition idField = recordDefinition.getIdField(); for (final FieldDefinition field : recordDefinition.getFields()) { if (field != idField) { final JdbcFieldDefinition jdbcAttribute = (JdbcFieldDefinition)field; parameterIndex = jdbcAttribute.setInsertPreparedStatementValue(statement, parameterIndex, object); } } statement.addBatch(); Integer batchCount = this.typeInsertSequenceBatchCountMap.get(typePath); if (batchCount == null) { batchCount = 1; this.typeInsertSequenceBatchCountMap.put(typePath, 1); } else { batchCount += 1; this.typeInsertSequenceBatchCountMap.put(typePath, batchCount); } if (batchCount >= this.batchSize) { final String sql = getInsertSql(recordDefinition, true); processCurrentBatch(typePath, sql, statement, this.typeInsertSequenceBatchCountMap); } } public boolean isFlushBetweenTypes() { return this.flushBetweenTypes; } public boolean isQuoteColumnNames() { return this.quoteColumnNames; } public boolean isThrowExceptions() { return this.throwExceptions; } private void processCurrentBatch(final PathName typePath, final String sql, final PreparedStatement statement, final Map<PathName, Integer> batchCountMap) { Integer batchCount = batchCountMap.get(typePath); if (batchCount == null) { batchCount = 0; } try { Integer typeCount = this.typeCountMap.get(typePath); if (typeCount == null) { typeCount = batchCount; } else { typeCount += batchCount; } this.typeCountMap.put(typePath, typeCount); statement.executeBatch(); } catch (final SQLException e) { throw this.connection.getException("Process Batch", sql, e); } catch (final RuntimeException e) { LOG.error(sql, e); throw e; } finally { batchCountMap.put(typePath, 0); } } public void setBatchSize(final int batchSize) { this.batchSize = batchSize; } public void setFlushBetweenTypes(final boolean flushBetweenTypes) { this.flushBetweenTypes = flushBetweenTypes; } /** * @param hints the hints to set */ public void setHints(final String hints) { this.hints = hints; } public void setLabel(final String label) { this.label = label; } public void setQuoteColumnNames(final boolean quoteColumnNames) { this.quoteColumnNames = quoteColumnNames; } public void setSqlPrefix(final String sqlPrefix) { this.sqlPrefix = sqlPrefix; } public void setSqlSuffix(final String sqlSuffix) { this.sqlSuffix = sqlSuffix; } public void setThrowExceptions(final boolean throwExceptions) { this.throwExceptions = throwExceptions; } @Override public String toString() { if (this.recordStore == null) { return super.toString(); } else { return this.recordStore.toString() + " writer"; } } private void update(final Record object) throws SQLException { final RecordDefinition objectType = object.getRecordDefinition(); final PathName typePath = objectType.getPathName(); final RecordDefinition recordDefinition = getRecordDefinition(typePath); flushIfRequired(recordDefinition); PreparedStatement statement = this.typeUpdateStatementMap.get(typePath); if (statement == null) { final String sql = getUpdateSql(recordDefinition); try { statement = this.connection.prepareStatement(sql); this.typeUpdateStatementMap.put(typePath, statement); } catch (final SQLException e) { LOG.error(sql, e); } } int parameterIndex = 1; final List<FieldDefinition> idFields = recordDefinition.getIdFields(); for (final FieldDefinition fieldDefinition : recordDefinition.getFields()) { if (!idFields.contains(fieldDefinition)) { final JdbcFieldDefinition jdbcFieldDefinition = (JdbcFieldDefinition)fieldDefinition; parameterIndex = jdbcFieldDefinition.setInsertPreparedStatementValue(statement, parameterIndex, object); } } for (final FieldDefinition idField : idFields) { final JdbcFieldDefinition jdbcFieldDefinition = (JdbcFieldDefinition)idField; parameterIndex = jdbcFieldDefinition.setInsertPreparedStatementValue(statement, parameterIndex, object); } statement.addBatch(); Integer batchCount = this.typeUpdateBatchCountMap.get(typePath); if (batchCount == null) { batchCount = 1; this.typeUpdateBatchCountMap.put(typePath, 1); } else { batchCount += 1; this.typeUpdateBatchCountMap.put(typePath, batchCount); } if (batchCount >= this.batchSize) { final String sql = getUpdateSql(recordDefinition); processCurrentBatch(typePath, sql, statement, this.typeUpdateBatchCountMap); } this.recordStore.addStatistic("Update", object); } @Override public synchronized void write(final Record record) { try { final RecordDefinition recordDefinition = record.getRecordDefinition(); final RecordStore recordStore = recordDefinition.getRecordStore(); final RecordState state = record.getState(); if (recordStore != this.recordStore) { if (state != RecordState.DELETED) { insert(record); } } else { switch (state) { case NEW: insert(record); break; case MODIFIED: update(record); break; case PERSISTED: // No action required break; case DELETED: delete(record); break; default: throw new IllegalStateException("State not known"); } } } catch (final RuntimeException e) { throw e; } catch (final Error e) { throw e; } catch (final BatchUpdateException e) { for (SQLException e1 = e.getNextException(); e1 != null; e1 = e1.getNextException()) { LOG.error("Unable to write", e1); } throw new RuntimeException("Unable to write", e); } catch (final Exception e) { throw new RuntimeException("Unable to write", e); } } }