All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cg.rooster.JdbcDataRepository Maven / Gradle / Ivy

package org.cg.rooster;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.lang.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.cg.rooster.core.Condition;
import org.cg.rooster.core.Query;
import org.cg.rooster.core.RowColumnMapper;
import org.cg.rooster.core.SqlGrammar;
import org.cg.rooster.core.TableDefinition;
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Persistable;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;

/**
 * A generic JDBC data repository that implements {@link PagingAndSortingRepository}
 * @author WZ
 *
 * @param  entity type
 * @param  entity id type (for compound id, just use Object [])
 */
public abstract class JdbcDataRepository , ID extends Serializable> implements DataRepository {

	private static final Logger LOG = LoggerFactory.getLogger(JdbcDataRepository.class);

	private final TableDefinition tableDefinition;
	private final RowColumnMapper rowColumnMapper;
	private final JdbcTemplate jdbcTemplate;
	private final SqlGrammar sqlGrammar;
	private final PlatformTransactionManager transactionManager;

	/**
	 * Get as primary key
	 * 
	 * @param idValues
	 * @return a object array of primary key
	 */
	public static Object[] primaryKey (Object... idValues) {
		return idValues;
	}
	
	/**
	 * Constructor
	 * 
	 * @param tableDefinition the table definition
	 * @param rowColumnMapper the row column mapper 
	 * @param dataSource the data source
	 * @param sqlGrammar the sql grammar
	 */
	public JdbcDataRepository (TableDefinition tableDefinition, RowColumnMapper rowColumnMapper, DataSource dataSource, SqlGrammar sqlGrammar) {
		this(tableDefinition, rowColumnMapper, dataSource, sqlGrammar, true);
	}
	
	/**
	 * Constructor
	 * 
	 * @param tableDefinition the table definition
	 * @param rowColumnMapper the row column mapper 
	 * @param dataSource the data source
	 * @param sqlGrammar the sql grammar
	 * @param lazyinit  is lazy connection initialization or not
	 */
	public JdbcDataRepository (TableDefinition tableDefinition, RowColumnMapper rowColumnMapper, DataSource dataSource, SqlGrammar sqlGrammar, boolean lazyinit) {
		super();
		Preconditions.checkNotNull(tableDefinition, "tableDefinition must be provided");
		Preconditions.checkNotNull(rowColumnMapper, "rowColumnMapper must be provided");
		Preconditions.checkNotNull(dataSource, "dataSource must be provided");
		Preconditions.checkNotNull(sqlGrammar, "sqlGrammar must be provided");
		
		this.tableDefinition = tableDefinition;
		this.rowColumnMapper = rowColumnMapper;
		this.jdbcTemplate = new JdbcTemplate(dataSource, lazyinit);
		this.jdbcTemplate.setFetchSize(1000);
		this.sqlGrammar = sqlGrammar;
		this.transactionManager = new DataSourceTransactionManager(dataSource);
	}
	
	protected JdbcTemplate getJdbcTemplate() {
		return jdbcTemplate;
	}
	
	protected TableDefinition getTableDefinition() {
		return tableDefinition;
	}

	protected RowColumnMapper getRowColumnMapper() {
		return rowColumnMapper;
	}

	protected SqlGrammar getSqlGrammar() {
		return sqlGrammar;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public  S save (S entity) {
		Preconditions.checkNotNull(entity, "entity must be provided");
		Preconditions.checkState(rowColumnMapper!=null, "rowColumnMapper must be initiated");
		Preconditions.checkState(!tableDefinition.isReadonly(), "table is readonly");

		final Map columns = rowColumnMapper.mapColumns(entity);		
		final Map dynamicColumns = rowColumnMapper.mapDynamicColumns(entity);
		
		Preconditions.checkState(columns!=null && !columns.isEmpty(), "rowColumnMapper.mapColumns must be implemented");
		Preconditions.checkState(dynamicColumns!=null, "rowColumnMapper.mapDynamicColumns cannot cannot return null");
				
		boolean isSucceed = this.transactionalUpdate(
				sqlGrammar.save(tableDefinition, columns, dynamicColumns), 
				ArrayUtils.addAll(columns.values().toArray(), dynamicColumns.values().toArray()));	
		if (isSucceed) {
			LOG.info(String.format("[save]entity saved: %s.", entity));
			return entity;
		} else {
			return null;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public  Iterable save (final Iterable entities) {
		Preconditions.checkNotNull(entities, "entities must be provided");
		Preconditions.checkState(rowColumnMapper!=null, "rowColumnMapper must be initiated");
		Preconditions.checkState(!tableDefinition.isReadonly(), "table is readonly");
		
		List batchArgs = new LinkedList();
		Map columns;
		Map dynamicColumns;
		String createQuery = "";
		Iterator iter = entities.iterator();
		S entity;
		while (iter.hasNext()) {
			entity = iter.next();
			columns = rowColumnMapper.mapColumns(entity);
			dynamicColumns = rowColumnMapper.mapDynamicColumns(entity);
			Preconditions.checkState(columns!=null && !columns.isEmpty(), "rowColumnMapper.mapColumns must be implemented");
			Preconditions.checkState(dynamicColumns!=null, "rowColumnMapper.mapDynamicColumns cannot cannot return null");
			batchArgs.add(ArrayUtils.addAll(columns.values().toArray(), dynamicColumns.values().toArray()));
			if (!iter.hasNext()) {
				// In order to get column mapping for query construction, 
				// we only need to get the mapping from the last entity.
				createQuery = sqlGrammar.save(tableDefinition, columns, dynamicColumns);
			}
		}
		long start = System.currentTimeMillis();
		boolean isSucceed = this.transactionalBatchUpdate(createQuery, batchArgs);
		long end = System.currentTimeMillis() - start;
		if (isSucceed) {
			LOG.info(String.format("[save]saved %s entities in %sms", batchArgs.size(), end));
			return entities;
		} else {
			List empty = Collections.emptyList();
			return empty;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean exists (ID id) {
		Preconditions.checkNotNull(id, "id must be provided");

		return this.get(id) != null;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public long count() {
		LOG.info(String.format("[count]%s", tableDefinition.getTableName()));
		return getJdbcTemplate().queryForObject(sqlGrammar.count(tableDefinition), Long.class);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean delete (ID id) {
		Preconditions.checkNotNull(id, "id must be provided");
		Preconditions.checkState(tableDefinition.isMutable(), "table is immutable");
		Preconditions.checkState(!tableDefinition.isReadonly(), "table is readonly");

		final Object[] idColumns = (id instanceof Object[]) ? (Object[]) id : new Object[]{id};
		boolean isSucceed = this.transactionalUpdate(sqlGrammar.delete(tableDefinition), idColumns);
		if (isSucceed) LOG.info(String.format("[delete]deleted %s", id));
		return isSucceed;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean delete (final Iterable ids) {
		Preconditions.checkNotNull(ids, "ids must be provided");
		Preconditions.checkState(tableDefinition.isMutable(), "table is immutable");
		Preconditions.checkState(!tableDefinition.isReadonly(), "table is readonly");

		Iterator iter = ids.iterator();
		Preconditions.checkArgument(iter.hasNext(), "ids must be provided");

		List batchArgs = new LinkedList();
		ID id;
		while (iter.hasNext()) {
			id = iter.next();
			if (id instanceof Object[]) {
				batchArgs.add((Object[]) id);
			} else {
				batchArgs.add(new Object[]{id});
			}
		}
		boolean isSucceed = this.transactionalBatchUpdate(sqlGrammar.delete(tableDefinition), batchArgs);
		if (isSucceed) LOG.info(String.format("[delete]%s entities deleted", batchArgs.size()));
		return isSucceed;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public T get (ID id) {
		final Object[] idColumns = (id instanceof Object[]) ? (Object[]) id : new Object[]{id};
		LOG.info(String.format("[get]id:%s", Arrays.toString(idColumns)));
		
		long start = System.currentTimeMillis();
		final List entity = jdbcTemplate.query(
				sqlGrammar.selectById(
						tableDefinition, 
						null, 
						Query.DEFAULT_QUERY_LIMIT, 
						1, 
						rowColumnMapper.mapDynamicColumnsType(), 
						null), 
				rowColumnMapper,
				idColumns
				);
		long end = System.currentTimeMillis() - start;
		LOG.info(String.format("[get]found in %sms", end));
		return entity.isEmpty() ? null : entity.get(0);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public Iterable findAll () {
		long start = System.currentTimeMillis();
		List result = getJdbcTemplate().query(
				sqlGrammar.selectById(
						tableDefinition, 
						null, 
						Query.DEFAULT_QUERY_LIMIT,
						-1, 
						rowColumnMapper.mapDynamicColumnsType(), 
						null), 
				rowColumnMapper);
		long end = System.currentTimeMillis() - start;
		LOG.info(String.format("[findAll]limit:%s. found %s in %sms", Query.DEFAULT_QUERY_LIMIT, result.size(), end));
		return result;
	}
		
	/**
	 * {@inheritDoc}
	 */
	@Override
	public Iterable find(Query query) {
		Preconditions.checkNotNull(query, "query must be provided");

		List result;
		long start = System.currentTimeMillis();
		if (query.getConditions()==null || query.getConditions().isEmpty()) { 
			result = getJdbcTemplate().query(
					sqlGrammar.selectById(
							tableDefinition, 
							query.getSort(), 
							query.getLimit(), 
							-1, 
							rowColumnMapper.mapDynamicColumnsType(), 
							query.getColumnSelection()), 
					rowColumnMapper);
		} else {
			result = getJdbcTemplate().query(
					sqlGrammar.selectByCondition(
							tableDefinition, 
							query.getSort(), 
							query.getLimit(), 
							query.getConditions(), 
							rowColumnMapper.mapDynamicColumnsType(), 
							query.getColumnSelection()), 
					rowColumnMapper, 
					Condition.getParamsFromConditions(query.getConditions()));	
		}
		long end = System.currentTimeMillis() - start;
		LOG.info(String.format("[find]query: %s in %sms", query, end));
		return result;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public Iterable find(Iterable ids) {
		return this.find(ids, new Query(null, null, null, Query.DEFAULT_QUERY_LIMIT));
	}
			
	/**
	 * {@inheritDoc}
	 */
	@Override
	public Iterable find(Iterable ids, Query query) {
		Preconditions.checkNotNull(ids, "ids must be provided");
		Preconditions.checkNotNull(query, "query must be provided");		
		if (query.getConditions()!=null && !query.getConditions().isEmpty()) {
			LOG.warn("[find]conditions will be ignored when lookup by id list");
		}
		if (!ids.iterator().hasNext()) {
			return Collections.emptyList();
		}
		
		//need to put all id components for all ids in a single flat array
		final List idColumnValuesList = new LinkedList();
		int idSize = 0;
		for (ID id : ids) {
			List idList;
			if (id instanceof Object[]) {
				idList = Arrays.asList((Object[]) id);
			} else {
				idList = Collections.singletonList(id);
			}
			idColumnValuesList.addAll(idList);
			idSize++;
		}
		
		Object[] idsArray = idColumnValuesList.toArray();		
		long start = System.currentTimeMillis();
		List result = getJdbcTemplate().query(
				sqlGrammar.selectById(
						tableDefinition, 
						query.getSort(), 
						query.getLimit(), 
						idSize, 
						rowColumnMapper.mapDynamicColumnsType(), 
						query.getColumnSelection()), 
				rowColumnMapper, 
				idsArray);
		long end = System.currentTimeMillis() - start;
		LOG.info(String.format("[find]ids:%s; query:%s; found %s in %sms", 
				Arrays.toString(idsArray), query, result.size(), end));
		return result;
	}
	
	private boolean transactionalUpdate (String preparedStatement, Object... args) {
		TransactionDefinition def = new DefaultTransactionDefinition();
		TransactionStatus status = transactionManager.getTransaction(def);
		try{
			getJdbcTemplate().update(preparedStatement, args);
			transactionManager.commit(status);
			return true;
		} catch (DataAccessException e) {
			LOG.error("Error in upserting record, rolling back");
			transactionManager.rollback(status);
			LOG.error(Throwables.getStackTraceAsString(e));
			return false;
		}
	}
	
	private boolean transactionalBatchUpdate (String preparedStatement, List args) {
		TransactionDefinition def = new DefaultTransactionDefinition();
		TransactionStatus status = transactionManager.getTransaction(def);
		try{
			getJdbcTemplate().batchUpdate(preparedStatement, args);
			transactionManager.commit(status);
			return true;
		} catch (DataAccessException e) {
			LOG.error("Error in upserting records, rolling back");
			transactionManager.rollback(status);
			LOG.error(Throwables.getStackTraceAsString(e));
			return false;
		}
	}

}