
com.arakelian.jdbc.store.JdbcStore Maven / Gradle / Ivy
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.arakelian.jdbc.store;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.arakelian.jdbc.conn.ConnectionFactory;
import com.arakelian.jdbc.conn.IgnoreCloseConnection;
import com.arakelian.jdbc.conn.SingletonConnectionFactory;
import com.arakelian.jdbc.handler.ResultSetHandler;
import com.arakelian.jdbc.model.Index;
import com.arakelian.jdbc.store.strategy.JdbcStrategy;
import com.arakelian.jdbc.utils.DatabaseUtils;
import com.arakelian.jdbc.utils.DatabaseUtils.JdbcIterator;
import com.arakelian.store.AbstractMutableStore;
import com.arakelian.store.IndexedStore;
import com.arakelian.store.StoreException;
import com.arakelian.store.feature.HasId;
import com.google.common.base.Preconditions;
public class JdbcStore extends AbstractMutableStore implements IndexedStore {
/**
* Converts the row into a Java bean.
*/
private class JsonHandler implements ResultSetHandler {
@Override
public T handle(final ResultSet rs, final ResultSetMetaData rsmd) throws SQLException {
if (rs.next()) {
return config.getStrategy().get(rs);
}
return null;
}
@Override
public boolean wasLast(final T result) {
return result == null;
}
}
private static final Logger LOGGER = LoggerFactory.getLogger(JdbcStore.class);
public static final String ID = "id";
public static final String ALL = "_all";
private static final String DELETE_BY_ID = "delete_id";
protected final JdbcStoreConfig config;
protected final Map secondaryIndexes;
protected List indexes;
public JdbcStore(final JdbcStoreConfig config) {
super(config);
this.config = config;
// predefined: queries for selecting and deleting a single record by id
this.secondaryIndexes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
this.secondaryIndexes.put(ALL, config.getSelectSql());
this.secondaryIndexes.put(ID, config.getSelectSql() + " where id=?");
this.secondaryIndexes.put(DELETE_BY_ID, config.getDeleteSql() + " where id=?");
// fetch list of indexes and create named query for each one
this.indexes = addIndexes(config.getConnectionFactory());
}
/**
* Returns the name of a new secondary index, which is generated by concatenating the field
* names that are in the index with double underscores, and converting the result to lower case.
*
* @param fields
* list of field names that are in the secondary index
* @return name of secondary index
*/
public String addSecondaryIndex(final String... fields) {
if (fields == null || fields.length == 0) {
return null;
}
// name will resemble: "first__second__third"
final StringBuilder name = new StringBuilder();
// where will resemble: "first=? and second=? and third=?"
final StringBuilder where = new StringBuilder();
for (int i = 0, size = fields.length; i < size; i++) {
// force field names to use LOWER_UNDERSCORE case format
final String field = fields[i].toLowerCase().replace(' ', '_');
name.append(i != 0 ? "__" : "").append(field);
where.append(i != 0 ? " and " : "").append(field).append("=?");
}
// add named query
final String indexName = name.toString();
addSecondaryIndex(indexName, where.toString());
return indexName;
}
/**
* Adds a secondary index that consists of the given where clause.
*
* @param name
* secondary index name
* @param whereClause
* where clause, without the WHERE keyword, like this: "first=? and second=? and
* third=?"
*/
public void addSecondaryIndex(final String name, final String whereClause) {
Preconditions.checkArgument(!StringUtils.isEmpty(name), "name must be non-empty");
Preconditions.checkArgument(!StringUtils.isEmpty(whereClause), "whereClause must be non-empty");
secondaryIndexes.put(name, config.getSelectSql() + " where " + whereClause);
}
@Override
public void deleteBy(final String indexName, final Object... args) {
final String sql = getNamedQuery(indexName, args);
try {
DatabaseUtils.executeUpdate(config.getConnectionFactory(), sql, args);
} catch (final SQLException e) {
throw new StoreException(e);
}
}
@Override
public T get(final String id) {
return getBy(ID, id);
}
@Override
public List getAllBy(final String indexName, final Object... args) {
List beans = null;
try (JdbcIterator it = iterateBy(indexName, args)) {
while (it.hasNext()) {
if (beans == null) {
beans = new ArrayList<>();
}
beans.add(it.next());
}
}
if (beans == null) {
return Collections. emptyList();
}
return beans;
}
@Override
public T getBy(final String indexName, final Object... args) {
final String sql = getNamedQuery(indexName, args);
try {
final T value = DatabaseUtils.executeQuery( //
config.getConnectionFactory(), //
sql,
new JsonHandler(),
args);
return value;
} catch (final SQLException e) {
throw new StoreException(e);
}
}
public final JdbcStoreConfig getConfig() {
return config;
}
public final List getIndexes() {
return indexes;
}
public JdbcIterator iterateBy(final String indexName, final Object... args) {
return DatabaseUtils.iterator( //
config.getConnectionFactory(), //
getNamedQuery(indexName, args), //
new JsonHandler(), //
args);
}
/**
* Refreshes the list of indexes that are in the database using our internal connection factory.
*/
public void refreshIndexes() {
this.indexes = addIndexes(config.getConnectionFactory());
}
/**
* Refresh the list of indexes that are in the database using the provided connection. This is
* useful when our connection factory may not yet see the changes available inside the
* connection provided, due to caching or transactionality.
*
* @param connection
* connect to use for fetching JDBC metadata.
*/
public void refreshIndexes(final Connection connection) {
this.indexes = addIndexes(new SingletonConnectionFactory(new IgnoreCloseConnection<>(connection)));
}
/**
* Fetches a list of indexes from the database using a JDBC metadata query, and creates name
* queries for each of them.
*
* @param connectionFactory
* connection factory
* @return list of indexes from the database
*/
private List addIndexes(final ConnectionFactory connectionFactory) {
LOGGER.info("Fetching list of indexes for table {}", config.getTable());
final List indexes;
try {
indexes = DatabaseUtils.getIndexes(connectionFactory, null, config.getTable(), false, false);
} catch (final SQLException e) {
throw new StoreException("Unable to fetch list of indexes from table " + config.getTable(), e);
}
for (final Index index : indexes) {
final String[] fields = index.getFieldNames();
LOGGER.info("Table \"{}\" has index for {}", config.getTable(), fields);
addSecondaryIndex(fields);
}
return indexes;
}
private final int countParameters(final String sql) {
if (StringUtils.isEmpty(sql)) {
return 0;
}
int count = 0;
for (int i = 0, size = sql.length(); i < size; i++) {
if (sql.charAt(i) == '?') {
count++;
}
}
return count;
}
private void doDeleteAll(final List> idsOrValues) {
final Object[] ids = idsOf(idsOrValues);
if (ids == null || ids.length == 0) {
return;
}
try {
final String sql = secondaryIndexes
.get(appendInClause(config.getDeleteSql(), ids.length, DELETE_BY_ID));
DatabaseUtils.executeUpdate(config.getConnectionFactory(), sql, ids);
} catch (final SQLException e) {
throw new StoreException("Unable to delete from table " + config.getTable(), e);
}
}
private String getNamedQuery(final String name, final Object... args) {
// fetch sql associated with given name
if (!secondaryIndexes.containsKey(name)) {
throw new StoreException(
"Named query (\"" + name + "\") does not exist for table " + config.getTable());
}
// verify we have been provided correct number of arguments
final String sql = secondaryIndexes.get(name);
final int numParams = countParameters(sql);
if (numParams != args.length) {
throw new StoreException("Named query (\"" + name + "\") with sql \"" + sql + "\" requires "
+ numParams + " arguments: " + sql);
}
return sql;
}
protected String appendInClause(final String baseSql, final int paramCount, final String namePrefix) {
if (paramCount < 1 || paramCount > config.getPartitionSize()) {
throw new StoreException("paramCount " + paramCount + " is invalid; must be be between 1 and "
+ config.getPartitionSize());
}
final String name = namePrefix + "_" + paramCount;
if (secondaryIndexes.containsKey(name)) {
// we have already build the select list of that size
return name;
}
// build select list
final StringBuilder buf = new StringBuilder(
baseSql.length() + " where id in (".length() + 2 * paramCount - 1);
buf.append(baseSql);
buf.append(" where id in (");
for (int i = 0; i < paramCount; i++) {
if (i != 0) {
buf.append(",");
}
buf.append("?");
}
buf.append(")");
final String sql = buf.toString();
// store for future use
secondaryIndexes.put(name, sql);
return name;
}
@Override
protected void doDelete(final String id) {
try {
final String sql = secondaryIndexes.get(DELETE_BY_ID);
DatabaseUtils.executeUpdate(config.getConnectionFactory(), sql, id);
} catch (final SQLException e) {
throw new StoreException("Unable to delete " + id + " from table " + config.getTable(), e);
}
}
@Override
protected void doDeleteAllIds(final List ids) {
doDeleteAll(ids);
}
@Override
protected void doDeleteAllValues(final List values) {
doDeleteAll(values);
}
@Override
protected List doGetAll(List result, final List partition) {
final String indexName = appendInClause(config.getSelectSql(), partition.size(), ID);
final Object[] partitionIds = partition.toArray();
try (JdbcIterator it = iterateBy(indexName, partitionIds)) {
while (it.hasNext()) {
if (result == null) {
result = new ArrayList<>();
}
result.add(it.next());
}
}
return result;
}
@Override
protected void doPut(final T value) {
try {
final JdbcStrategy strategy = config.getStrategy();
final ConnectionFactory connectionFactory = config.getConnectionFactory();
strategy.put(connectionFactory, config.getInsertSql(), value);
} catch (final SQLException e) {
final String id = value.getId();
throw new StoreException("Unable to save " + id + " to table " + config.getTable(), e);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy