
com.feedzai.commons.sql.abstraction.engine.AbstractDatabaseEngine Maven / Gradle / Ivy
/*
* Copyright 2014 Feedzai
*
* 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.feedzai.commons.sql.abstraction.engine;
import com.feedzai.commons.sql.abstraction.batch.AbstractBatch;
import com.feedzai.commons.sql.abstraction.batch.DefaultBatch;
import com.feedzai.commons.sql.abstraction.ddl.*;
import com.feedzai.commons.sql.abstraction.dml.Expression;
import com.feedzai.commons.sql.abstraction.dml.dialect.Dialect;
import com.feedzai.commons.sql.abstraction.dml.result.ResultColumn;
import com.feedzai.commons.sql.abstraction.dml.result.ResultIterator;
import com.feedzai.commons.sql.abstraction.engine.configuration.PdbProperties;
import com.feedzai.commons.sql.abstraction.engine.handler.ExceptionHandler;
import com.feedzai.commons.sql.abstraction.engine.handler.OperationFault;
import com.feedzai.commons.sql.abstraction.entry.EntityEntry;
import com.feedzai.commons.sql.abstraction.util.AESHelper;
import com.feedzai.commons.sql.abstraction.util.InitiallyReusableByteArrayOutputStream;
import com.feedzai.commons.sql.abstraction.util.PreparedStatementCapsule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import java.io.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static com.feedzai.commons.sql.abstraction.engine.configuration.PdbProperties.*;
import static com.feedzai.commons.sql.abstraction.util.StringUtils.quotize;
import static com.feedzai.commons.sql.abstraction.util.StringUtils.readString;
/**
* Provides a set of functions to interact with the database.
*
* This Engine already provides thread safeness to all public exposed methods
*
* @author Rui Vilao ([email protected])
* @since 2.0.0
*/
public abstract class AbstractDatabaseEngine implements DatabaseEngine {
/**
* The Guice injector.
*/
@Inject
protected Injector injector;
/**
* The translator in place.
*/
@Inject
protected AbstractTranslator translator;
/**
* The default fetch size for iterators.
*/
private static final int DEFAULT_FETCH_SIZE = 1000;
/**
* The database connection.
*/
protected Connection conn;
/**
* The dev Marker.
*/
protected final static Marker dev = MarkerFactory.getMarker("DEV");
/**
* Maximum number of tries.
*/
private final int maximumNumberOfTries;
/**
* Retry interval.
*/
private final long retryInterval;
/**
* The logger that will be used.
*/
protected Logger logger;
/**
* The notification logger for administration.
*/
protected Logger notificationLogger;
/**
* Map of entities.
*/
protected final Map entities = new HashMap<>();
/**
* Map of prepared statements.
*/
protected final Map stmts = new HashMap();
/**
* The configuration.
*/
protected final PdbProperties properties;
/**
* The dialect.
*/
protected final Dialect diaclect;
/**
* The reusable initial byte buffer for blobs.
*/
private byte[] reusableByteBuffer;
/**
* The exception handler to control the flow when defining new entities.
*/
protected ExceptionHandler eh;
/**
* Creates a new instance of {@link AbstractDatabaseEngine}.
*
* @param driver The driver to connect to the database.
* @param properties The properties of the connection.
* @param dialect The dialect in use.
* @throws DatabaseEngineException When errors occur trying to set up the database connection.
*/
public AbstractDatabaseEngine(String driver, final PdbProperties properties, final Dialect dialect) throws DatabaseEngineException {
this.eh = ExceptionHandler.DEFAULT;
this.properties = properties;
// Check if the driver is to override.
if (properties.isDriverSet()) {
driver = properties.getDriver();
}
logger = LoggerFactory.getLogger(this.getClass());
notificationLogger = LoggerFactory.getLogger("admin-notifications");
this.diaclect = dialect;
final String jdbc = this.properties.getJdbc();
if (StringUtils.isBlank(jdbc)) {
throw new DatabaseEngineException(String.format("%s property is mandatory", JDBC));
}
maximumNumberOfTries = this.properties.getMaxRetries();
retryInterval = this.properties.getRetryInterval();
try {
Class.forName(driver);
connect();
} catch (ClassNotFoundException ex) {
throw new DatabaseEngineException("Driver not found", ex);
} catch (SQLException ex) {
throw new DatabaseEngineException("Unable to connect to database", ex);
} catch (DatabaseEngineException ex) {
throw ex;
} catch (Exception ex) {
throw new DatabaseEngineException("An unknown error occurred while establishing a connection to the database.", ex);
}
}
/**
* Reads the private key from the secret location.
*
* @return A string with the private key.
* @throws Exception If something occurs while reading.
*/
protected String getPrivateKey() throws Exception {
String location = this.properties.getProperty(SECRET_LOCATION);
if (StringUtils.isBlank(location)) {
throw new DatabaseEngineException("Encryption was specified but there's no location specified for the private key.");
}
File f = new File(location);
if (!f.canRead()) {
throw new DatabaseEngineException("Specified file '" + location + "' does not exist or the application does not have read permissions over it.");
}
return readString(new FileInputStream(f)).trim();
}
/**
* Connects to the database.
*
* @throws Exception If connection is not possible, or failed to decrypt username/password if encryption was provided.
*/
protected void connect() throws Exception {
String username = this.properties.getProperty(USERNAME);
String password = this.properties.getProperty(PASSWORD);
if (this.properties.isEncryptedPassword() || this.properties.isEncryptedUsername()) {
String privateKey = getPrivateKey();
if (this.properties.isEncryptedUsername()) {
final String decryptedUsername = AESHelper.decrypt(this.properties.getProperty(ENCRYPTED_USERNAME), privateKey);
if (StringUtils.isEmpty(decryptedUsername)) {
throw new DatabaseEngineException("The encrypted username could not be decrypted.");
}
username = decryptedUsername;
}
if (this.properties.isEncryptedPassword()) {
final String decryptedPassword = AESHelper.decrypt(this.properties.getProperty(ENCRYPTED_PASSWORD), privateKey);
if (StringUtils.isEmpty(decryptedPassword)) {
throw new DatabaseEngineException("The encrypted password could not be decrypted.");
}
password = decryptedPassword;
}
}
String jdbc = getFinalJdbcConnection(this.properties.getProperty(JDBC));
conn = DriverManager.getConnection(
jdbc,
username,
password);
setTransactionIsolation();
logger.debug("Connection successful.");
}
/**
* Gets the final JDBC connection.
*
* Implementations might override this method in order to change the JDBC connection.
*
* @param jdbc The current JDBC connection.
* @return The final JDBC connection.
*/
protected String getFinalJdbcConnection(String jdbc) {
return jdbc;
}
/**
* Sets the transaction isolation level.
*
* @throws SQLException If a database access error occurs.
*/
protected void setTransactionIsolation() throws SQLException {
conn.setTransactionIsolation(properties.getIsolationLevel());
}
/**
* Gets the class that translates SQL bound to this engine.
*
* @return The class that translates SQL bound to this engine.
*/
public abstract Class extends AbstractTranslator> getTranslatorClass();
/**
* Checks if the connection is available and returns it. If the connection is not available, it tries to reconnect (the number of times defined in the
* properties with the delay there specified).
*
* @return The connection.
* @throws RetryLimitExceededException If the retry limit is exceeded.
* @throws InterruptedException If the thread is interrupted during reconnection.
*/
@Override
public synchronized Connection getConnection() throws RetryLimitExceededException, InterruptedException, RecoveryException {
if (!properties.isReconnectOnLost()) {
return conn;
}
int retries = 1;
if (checkConnection(conn)) {
return conn;
}
logger.debug("Connection is down.");
// reconnect.
while (true) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
try {
if (maximumNumberOfTries > 0) {
if (retries == (maximumNumberOfTries / 2) || retries == (maximumNumberOfTries - 1)) {
logger.error("The connection to the database was lost. Remaining retries: {}", (maximumNumberOfTries - retries));
notificationLogger.error("The connection to the database was lost. Remaining retries: {}", (maximumNumberOfTries - retries));
} else {
logger.debug("Retrying ({}/{}) in {} seconds...", new Object[]{retries, maximumNumberOfTries, TimeUnit.MILLISECONDS.toSeconds(retryInterval)});
}
} else {
logger.debug("Retry number {} in {} seconds...", retries, TimeUnit.MILLISECONDS.toSeconds(retryInterval));
if (retries % 10 == 0) {
notificationLogger.error("The connection to the database was lost. Retry number {} in {}", retries, TimeUnit.MILLISECONDS.toSeconds(retryInterval));
}
}
Thread.sleep(retryInterval);
connect(); // this sets the new object.
// recover state.
try {
recover();
} catch (Exception e) {
throw new RecoveryException("Error recovering from lost connection.", e);
}
// return it.
return conn;
} catch (SQLException ex) {
logger.debug("Connection failed.");
if (maximumNumberOfTries > 0 && retries > maximumNumberOfTries) {
throw new RetryLimitExceededException("Maximum number of retries for a connection exceeded.", ex);
}
retries++;
} catch (Exception e) {
logger.error("An unexpected error occurred.", e);
}
}
}
private synchronized void recover() throws DatabaseEngineException, NameAlreadyExistsException {
// Recover entities.
final Map niw = new HashMap(entities);
// clear the entities
entities.clear();
for (MappedEntity me : niw.values()) {
loadEntity(me.getEntity());
}
// Recover prepared statements.
final Map niwStmts = new HashMap(stmts);
for (Map.Entry e : niwStmts.entrySet()) {
createPreparedStatement(e.getKey(), e.getValue().query, e.getValue().timeout, true);
}
logger.debug("State recovered.");
}
/**
* Closes the connection to the database.
*/
@Override
public synchronized void close() {
try {
if (properties.isSchemaPolicyCreateDrop()) {
for (Map.Entry me : entities.entrySet()) {
try {
// Flush first
final PreparedStatement insert = me.getValue().getInsert();
final PreparedStatement insertReturning = me.getValue().getInsertReturning();
try {
insert.executeBatch();
if (insertReturning != null) {
insertReturning.executeBatch();
}
} catch (SQLException ex) {
logger.debug(String.format("Failed to flush before dropping entity '%s'", me.getValue().getEntity().getName()), ex);
} finally {
if (insert != null) {
try {
insert.close();
} catch (Exception e) {
logger.trace("Could not close prepared statement.", e);
}
}
if (insertReturning != null) {
try {
insertReturning.close();
} catch (Exception e) {
logger.trace("Could not close prepared statement.", e);
}
}
}
dropEntity(me.getValue().getEntity());
} catch (DatabaseEngineException ex) {
logger.debug(String.format("Failed to drop entity '%s'", me.getValue().getEntity().getName()), ex);
}
}
}
conn.close();
logger.debug("Connection to database closed");
} catch (SQLException ex) {
logger.warn("Unable to close connection");
}
}
/**
* Starts a transaction. Doing this will set auto commit to false ({@link Connection#getAutoCommit()}).
*
* @throws DatabaseEngineException If something goes wrong while persisting data.
*/
@Override
public synchronized void beginTransaction() throws DatabaseEngineRuntimeException {
/*
* It makes sense trying to reconnect since it's the beginning of a transaction.
*/
try {
getConnection();
if (!conn.getAutoCommit()) { //I.E. Manual control
logger.debug("There's already one transaction active");
return;
}
conn.setAutoCommit(false);
} catch (Exception ex) {
throw new DatabaseEngineRuntimeException("Error occurred while starting transaction", ex);
}
}
/**
* Adds an entity to the engine. It will create tables and everything necessary so persistence can work.
*
* @param entity The entity to add.
* @throws DatabaseEngineException If something goes wrong while creating the structures.
*/
@Override
public synchronized void addEntity(DbEntity entity) throws DatabaseEngineException {
addEntity(entity, false);
}
@Override
public synchronized void loadEntity(DbEntity entity) throws DatabaseEngineException {
if (!entities.containsKey(entity.getName())) {
validateEntity(entity);
MappedEntity me = createPreparedStatementForInserts(entity).setEntity(entity);
entities.put(entity.getName(), me);
logger.trace("Entity '{}' loaded", entity.getName());
}
}
/**
* Adds an entity to the engine. It will create tables and everything necessary so persistence can work.
*
* @param entity The entity to add.
* @param recovering True if entities are being add due to recovery.
* @throws DatabaseEngineException If something goes wrong while creating the structures.
*/
private void addEntity(DbEntity entity, boolean recovering) throws DatabaseEngineException {
if (!recovering) {
try {
getConnection();
} catch (Exception e) {
throw new DatabaseEngineException("Could not add entity", e);
}
validateEntity(entity);
if (entities.containsKey(entity.getName())) {
throw new DatabaseEngineException(String.format("Entity '%s' is already defined", entity.getName()));
}
if (this.properties.isSchemaPolicyDropCreate()) {
dropEntity(entity);
}
}
if (!this.properties.isSchemaPolicyNone()) {
createTable(entity);
addPrimaryKey(entity);
addFks(entity);
addIndexes(entity);
addSequences(entity);
}
loadEntity(entity);
}
/**
*
* Updates an entity in the engine.
*
*
* If the entity does not exist in the instance, the method {@link #addEntity(com.feedzai.commons.sql.abstraction.ddl.DbEntity)} will be invoked.
*
*
* The engine will compare the entity with the {@link #getMetadata(String)} information and update the schema of the table.
*
*
* ATTENTION: This method will only add new columns or drop removed columns in the database table. It will also
* drop and create foreign keys.
* Primary Keys, Indexes and column types changes will not be updated.
*
*
* @param entity The entity to update.
* @throws com.feedzai.commons.sql.abstraction.engine.DatabaseEngineException
* @since 12.1.0
*/
@Override
public synchronized void updateEntity(DbEntity entity) throws DatabaseEngineException {
// Only mutate the schema in the DB (i.e. add schema, drop/add columns, if the schema policy allows it.
if (!properties.isSchemaPolicyNone()) {
final Map tableMetadata = getMetadata(entity.getName());
if (tableMetadata.size() == 0) { // the table does not exist
addEntity(entity);
} else if (this.properties.isSchemaPolicyDropCreate()) {
dropEntity(entity);
addEntity(entity);
} else {
validateEntity(entity);
dropFks(entity.getName());
List toRemove = new ArrayList();
for (String dbColumn : tableMetadata.keySet()) {
if (!entity.containsColumn(dbColumn)) {
toRemove.add(dbColumn);
}
}
if (!toRemove.isEmpty()) {
if (properties.allowColumnDrop()) {
dropColumn(entity, toRemove.toArray(new String[toRemove.size()]));
} else {
logger.warn("Need to remove {} columns to update {} entity, but property allowColumnDrop is set to false.", StringUtils.join(toRemove, ","), entity.getName());
}
}
List columns = new ArrayList();
for (DbColumn localColumn : entity.getColumns()) {
if (!tableMetadata.containsKey(localColumn.getName())) {
columns.add(localColumn);
}
}
if (!columns.isEmpty()) {
addColumn(entity, columns.toArray(new DbColumn[columns.size()]));
}
addFks(entity);
}
}
// We still want to create prepared statements for the entity, regardless the schema policy
MappedEntity me = createPreparedStatementForInserts(entity);
me = me.setEntity(entity);
entities.put(entity.getName(), me);
logger.trace("Entity '{}' updated", entity.getName());
}
@Override
public synchronized DbEntity removeEntity(final String name) {
final MappedEntity toRemove = entities.get(name);
if (toRemove == null) {
return null;
}
try {
toRemove.getInsert().executeBatch();
} catch (SQLException ex) {
logger.debug("Could not flush before remove '{}'", name, ex);
}
if (properties.isSchemaPolicyCreateDrop()) {
try {
dropEntity(toRemove.getEntity());
} catch (DatabaseEngineException ex) {
logger.debug(String.format("Something went wrong while dropping entity '%s'", name), ex);
}
}
entities.remove(name);
return toRemove.getEntity();
}
@Override
public synchronized boolean containsEntity(String name) {
return entities.containsKey(name);
}
/**
* Drops an entity.
*
* @param entity The entity name.
* @throws com.feedzai.commons.sql.abstraction.engine.DatabaseEngineException
*/
@Override
public synchronized void dropEntity(String entity) throws DatabaseEngineException {
if (!containsEntity(entity)) {
return;
}
dropEntity(entities.get(entity).getEntity());
}
/**
* Drops everything that belongs to the entity.
*
* @param entity The entity.
* @throws DatabaseEngineException If something goes wrong while dropping the structures.
*/
public synchronized void dropEntity(final DbEntity entity) throws DatabaseEngineException {
dropSequences(entity);
dropTable(entity);
entities.remove(entity.getName());
logger.trace("Entity {} dropped", entity.getName());
}
/**
* Persists a given entry. Persisting a query implies executing the statement.
If you are inside of an explicit transaction, changes will only be
* visible upon explicit commit, otherwise a commit will immediately take place.
*
* @param name The entity name.
* @param entry The entry to persist.
* @return The ID of the auto generated value, null if there's no auto generated value.
* @throws DatabaseEngineException If something goes wrong while persisting data.
*/
@Override
public abstract Long persist(final String name, final EntityEntry entry) throws DatabaseEngineException;
/**
*
* Persists a given entry. Persisting a query implies executing the statement.
* If define useAutoInc as false, PDB will disable the auto increments for the current insert and advance the sequences if needed.
*
*
* If you are inside of an explicit transaction, changes will only be visible upon explicit commit, otherwise a
* commit will immediately take place.
*
*
* @param name The entity name.
* @param entry The entry to persist.
* @param useAutoInc Use or not the autoinc.
* @return The ID of the auto generated value, null if there's no auto generated value.
* @throws DatabaseEngineException If something goes wrong while persisting data.
*/
@Override
public abstract Long persist(final String name, final EntityEntry entry, final boolean useAutoInc) throws DatabaseEngineException;
/**
* Flushes the batches for all the registered entities.
*
* @throws DatabaseEngineException If something goes wrong while persisting data.
*/
@Override
public synchronized void flush() throws DatabaseEngineException {
/*
* Reconnect on this method does not make sense since a new connection will have nothing to flush.
*/
try {
for (MappedEntity me : entities.values()) {
me.getInsert().executeBatch();
}
} catch (Exception ex) {
throw new DatabaseEngineException("Something went wrong while flushing", ex);
}
}
/**
* Commits the current transaction. You should only call this method if you've previously called {@link AbstractDatabaseEngine#beginTransaction()}.
*
* @throws DatabaseEngineException If something goes wrong while persisting data.
*/
@Override
public synchronized void commit() throws DatabaseEngineRuntimeException {
/*
* Reconnect on this method does not make sense since a new connection will have nothing to commit.
*/
try {
conn.commit();
conn.setAutoCommit(true);
} catch (SQLException ex) {
throw new DatabaseEngineRuntimeException("Something went wrong while committing transaction", ex);
}
}
/**
* Rolls back a transaction. You should only call this method if you've previously called {@link AbstractDatabaseEngine#beginTransaction()}.
*
* @throws DatabaseEngineRuntimeException If the rollback fails.
*/
@Override
public synchronized void rollback() throws DatabaseEngineRuntimeException {
/*
* Reconnect on this method does not make sense since a new connection will have nothing to rollback.
*/
try {
conn.rollback();
conn.setAutoCommit(true);
} catch (SQLException ex) {
throw new DatabaseEngineRuntimeException("Something went wrong while rolling back the transaction", ex);
}
}
/**
* @return True if the transaction is active.
* @throws DatabaseEngineRuntimeException If the access to the database fails.
*/
@Override
public synchronized boolean isTransactionActive() throws DatabaseEngineRuntimeException {
/*
* This method is used to know if a rollback is needed like: beginTransaction(); try { // Do something commit(); } finally { if (isTransactionActive())
* { rollback(); } }
*
* So reconnection in this case does not make sense either.
*/
try {
return !conn.getAutoCommit();
} catch (SQLException ex) {
throw new DatabaseEngineRuntimeException("Something went wrong while rolling back the transaction", ex);
}
}
/**
* Executes a native query.
*
* @param query The query to execute.
* @return The number of rows updated.
* @throws DatabaseEngineException If something goes wrong executing the native query.
*/
@Override
public synchronized int executeUpdate(final String query) throws DatabaseEngineException {
Statement s = null;
try {
getConnection();
s = conn.createStatement();
return s.executeUpdate(query);
} catch (Exception ex) {
throw new DatabaseEngineException("Error handling native query", ex);
} finally {
if (s != null) {
try {
s.close();
} catch (Exception e) {
logger.trace("Error closing statement.", e);
}
}
}
}
/**
* Executes the given update.
*
* @param query The update to execute.
* @throws DatabaseEngineException If something goes wrong executing the update.
*/
@Override
public synchronized int executeUpdate(final Expression query) throws DatabaseEngineException {
/*
* Reconnection is already assured by "void executeUpdate(final String query)".
*/
final String trans = translate(query);
logger.trace(trans);
return executeUpdate(trans);
}
/**
* Translates the given expression to the current dialect.
*
* @param query The query to translate.
* @return The translation result.
*/
@Override
public String translate(final Expression query) {
inject(query);
return query.translate();
}
/**
* @return The dialect being in use.
*/
@Override
public Dialect getDialect() {
return diaclect;
}
/**
* Creates a new batch that periodically flushes a batch. A flush will also occur when the maximum number of statements in the batch is reached.
*
* Please be sure to call {@link com.feedzai.commons.sql.abstraction.batch.AbstractBatch#destroy() } before closing the session with the database
*
* @param batchSize The batch size.
* @param batchTimeout If inserts do not occur after the specified time, a flush will be performed.
* @return The batch.
*/
@Override
public AbstractBatch createBatch(final int batchSize, final long batchTimeout) {
return createBatch(batchSize, batchTimeout, null);
}
@Override
public AbstractBatch createBatch(final int batchSize, final long batchTimeout, final String batchName) {
return DefaultBatch.create(this, batchName, batchSize, batchTimeout, properties.getMaximumAwaitTimeBatchShutdown());
}
/**
* Validates the entity before trying to create the schema.
*
* @param entity The entity to validate.
* @throws DatabaseEngineException If the validation fails.
*/
private void validateEntity(final DbEntity entity) throws DatabaseEngineException {
if (entity.getName() == null || entity.getName().length() == 0) {
throw new DatabaseEngineException("You have to define the entity name");
}
final int maxIdentSize = properties.getMaxIdentifierSize();
if (entity.getName().length() > maxIdentSize) {
throw new DatabaseEngineException(String.format("Entity '%s' exceeds the maximum number of characters (%d)", entity.getName(), maxIdentSize));
}
if (entity.getColumns().isEmpty()) {
throw new DatabaseEngineException("You have to specify at least one column");
}
int numberOfAutoIncs = 0;
for (DbColumn c : entity.getColumns()) {
if (c.getName() == null || c.getName().length() == 0) {
throw new DatabaseEngineException(String.format("Column in entity '%s' must have a name", entity.getName()));
}
if (c.getName().length() > maxIdentSize) {
throw new DatabaseEngineException(String.format("Column '%s' in entity '%s' exceeds the maximum number of characters (%d)", c.getName(), entity.getName(), maxIdentSize));
}
if (c.isAutoInc()) {
numberOfAutoIncs++;
}
}
if (numberOfAutoIncs > 1) {
throw new DatabaseEngineException("You can only define one auto incremented column");
}
// Index validation
List indexes = entity.getIndexes();
if (!indexes.isEmpty()) {
for (DbIndex index : indexes) {
if (index.getColumns().isEmpty()) {
throw new DatabaseEngineException(String.format("You have to specify at least one column to create an index in entity '%s'", entity.getName()));
}
for (String column : index.getColumns()) {
if (column == null || column.length() == 0) {
throw new DatabaseEngineException(String.format("Column indexes must have a name in entity '%s'", entity.getName()));
}
}
}
}
// FK validation
List fks = entity.getFks();
if (!fks.isEmpty()) {
for (DbFk fk : fks) {
if (fk.getForeignTable() == null || fk.getForeignTable().length() == 0) {
throw new DatabaseEngineException(String.format("You have to specify the table when creating a Foreign Key in entity '%s'", entity.getName()));
}
if (fk.getLocalColumns().isEmpty()) {
throw new DatabaseEngineException(String.format("You must specify at least one local column when defining a Foreign Key in '%s'", entity.getName()));
}
if (fk.getForeignColumns().isEmpty()) {
throw new DatabaseEngineException(String.format("You must specify at least one foreign column when defining a Foreign Key in '%s'", entity.getName()));
}
if (fk.getLocalColumns().size() != fk.getForeignColumns().size()) {
throw new DatabaseEngineException(String.format("Number of local columns does not match foreign ones in entity '%s'", entity.getName()));
}
}
}
}
/**
* Checks if the connection is alive.
*
* @param conn The connection to test.
* @return True if the connection is valid, false otherwise.
*/
protected abstract boolean checkConnection(final Connection conn);
/**
* Checks if the connection is alive.
*
* @param forceReconnect True to force the connection in case of failure.
* @return True if the connection is valid, false otherwise.
*/
@Override
public synchronized boolean checkConnection(final boolean forceReconnect) {
if (checkConnection(conn)) {
return true;
} else if (forceReconnect) {
try {
connect();
recover();
return true;
} catch (Exception ex) {
logger.debug(dev, "reconnection failure", ex);
return false;
}
}
return false;
}
/**
* Checks if the connection is alive.
*
* @return True if the connection is valid, false otherwise.
*/
@Override
public synchronized boolean checkConnection() {
return checkConnection(false);
}
/**
* Add an entry to the batch.
*
* @param name The entity name.
* @param entry The entry to persist.
* @throws DatabaseEngineException If something goes wrong while persisting data.
*/
@Override
public synchronized void addBatch(final String name, final EntityEntry entry) throws DatabaseEngineException {
try {
final MappedEntity me = entities.get(name);
if (me == null) {
throw new DatabaseEngineException(String.format("Unknown entity '%s'", name));
}
PreparedStatement ps = me.getInsert();
entityToPreparedStatement(me.getEntity(), ps, entry, true);
ps.addBatch();
} catch (Exception ex) {
throw new DatabaseEngineException("Error adding to batch", ex);
}
}
/**
* Translates the given entry entity to the prepared statement.
*
* @param entity The entity.
* @param ps The prepared statement.
* @param entry The entry.
* @return The prepared statement filled in.
* @throws DatabaseEngineException if something occurs during the translation.
*/
protected abstract int entityToPreparedStatement(final DbEntity entity, final PreparedStatement ps, final EntityEntry entry, final boolean useAutoIncs) throws DatabaseEngineException;
/**
* Executes the given query.
*
* @param query The query to execute.
* @throws DatabaseEngineException If something goes wrong executing the query.
*/
@Override
public synchronized List