DATACASS-286 - Log all CQL queries executed with CqlTemplate.

Additional refactoring to simplify CqlTemplate logic as well as make CqlTemplate methods more behaviorally consistent.

Origina pull request: #61.
This commit is contained in:
John Blum
2016-05-20 12:40:16 -07:00
committed by Mark Paluch
parent be495728d9
commit 39928948f0
8 changed files with 1123 additions and 744 deletions

View File

@@ -31,6 +31,7 @@ import org.springframework.cassandra.core.keyspace.DropIndexSpecification;
import org.springframework.cassandra.core.keyspace.DropKeyspaceSpecification;
import org.springframework.cassandra.core.keyspace.DropTableSpecification;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
@@ -693,14 +694,17 @@ public interface CqlOperations {
<T> T queryForObject(Select select, RowMapper<T> rowMapper) throws DataAccessException;
/**
* Process a ResultSet through a RowMapper. This is used internal to the Template for core operations, but is made
* available through Operations in the event you have a ResultSet to process. The ResultsSet could come from a
* ResultSetFuture after an asynchronous query.
*
* @param resultSet
* @param rowMapper
* @return
* @throws DataAccessException
* Process {@link ResultSet} with {@link RowMapper}. This method is used internally to the template
* for core operations, but is made available through this interface in the event you have a {@link ResultSet}
* to process. The {@link ResultSet} could come from a {@link ResultSetFuture} after an asynchronous query.
*
* @param resultSet {@link ResultSet} to process.
* @param rowMapper {@link RowMapper} used to process the single row of the result set.
* @throws IllegalArgumentException if {@link ResultSet} is null.
* @throws IncorrectResultSizeDataAccessException if no rows are found, or more than 1 row is found.
* @throws DataAccessException if a Cassandra driver error occurs.
* @see org.springframework.cassandra.core.RowMapper
* @see com.datastax.driver.core.ResultSet
*/
<T> T processOne(ResultSet resultSet, RowMapper<T> rowMapper) throws DataAccessException;

View File

@@ -23,70 +23,84 @@ import org.springframework.util.Assert;
import com.datastax.driver.core.Session;
/**
* A {@link CassandraAccessor} is able to access a Cassandra {@link Session} and the
* {@link CassandraExceptionTranslator}.
* {@link CassandraAccessor} provides access to a Cassandra {@link Session} and the {@link CassandraExceptionTranslator}
* .
* <p>
* Classes providing a higher abstraction level usually extend {@link CassandraAccessor} to provide a richer set of
* functionality on top of a {@link Session}.
*
* @author David Webb
* @author Mark Paluch
* @author John Blum
* @see org.springframework.beans.factory.InitializingBean
* @see com.datastax.driver.core.Session
*/
public class CassandraAccessor implements InitializingBean {
/** Logger available to subclasses */
CassandraExceptionTranslator exceptionTranslator = new CassandraExceptionTranslator();
protected final Logger logger = LoggerFactory.getLogger(getClass());
private Session session;
private CassandraExceptionTranslator exceptionTranslator = new CassandraExceptionTranslator();
/**
* Ensure that the Cassandra Session has been set
* Ensures the Cassandra {@link Session} and exception translator has been propertly set.
*/
@Override
public void afterPropertiesSet() {
Assert.state(session != null, "Session must not be null");
}
Assert.notNull(session, "Session must not be null!");
Assert.notNull(exceptionTranslator, "CassandraExceptionTranslator must not be null!");
/* (non-Javadoc) */
protected void logDebug(String logMessage, Object... array) {
if (logger.isDebugEnabled()) {
logger.debug(logMessage, array);
}
}
/**
* Set the exception translator for this instance.
* Sets the exception translator used by this template to translate Cassandra specific Exceptions into Spring DAO's
* Exception Hierarchy.
*
* @param exceptionTranslator the exception translator to set, must not be {@literal null}.
* @param exceptionTranslator exception translator to set; must not be {@literal null}.
* @see org.springframework.cassandra.support.CassandraExceptionTranslator
*/
public void setExceptionTranslator(CassandraExceptionTranslator exceptionTranslator) {
Assert.notNull(exceptionTranslator, "CassandraExceptionTranslator must not be null!");
Assert.notNull(exceptionTranslator, "CassandraExceptionTranslator must not be null");
this.exceptionTranslator = exceptionTranslator;
}
/**
* Return the exception translator for this instance.
* Return the exception translator used by this template to translate Cassandra specific Exceptions into Spring DAO's
* Exception Hierarchy.
*
* @return the exception translator
* @return the Cassandra exception translator.
* @see org.springframework.cassandra.support.CassandraExceptionTranslator
*/
public CassandraExceptionTranslator getExceptionTranslator() {
Assert.state(this.exceptionTranslator != null, "CassandraExceptionTranslator was not properly initialized");
return this.exceptionTranslator;
}
/**
* Returns the session.
* Sets the Cassandra {@link Session} used by this template to perform Cassandra data access operations.
*
* @return the session.
* @param session Cassandra {@link Session} used by this template. Must not be{@literal null}.
* @see com.datastax.driver.core.Session
*/
public Session getSession() {
return session;
public void setSession(Session session) {
Assert.notNull(session, "Session must not be null");
this.session = session;
}
/**
* @param session The session to set, must not be{@literal null}
* Returns the Cassandra {@link Session} used by this template to perform Cassandra data access operations.
*
* @return the Cassandra {@link Session} used by this template.
* @see com.datastax.driver.core.Session
*/
public void setSession(Session session) {
Assert.notNull(session, "Session must not be null!");
this.session = session;
public Session getSession() {
Assert.state(this.session != null, "Session was not properly initialized");
return this.session;
}
}

View File

@@ -0,0 +1,267 @@
/*
* Copyright 2013-2016 the original author or authors
*
* 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 org.springframework.cassandra.core;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.Iterator;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import com.datastax.driver.core.ColumnDefinitions;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
/**
* The CqlTemplateUnitTests class is a test suite of test cases testing the contract and functionality of the
* {@link CqlTemplate} class.
*
* @author John Blum
* @see org.springframework.cassandra.core.CqlTemplate
* @since 1.5.0
*/
// TODO: add many more unit tests until SUT test coverage is 100%!
@RunWith(MockitoJUnitRunner.class)
public class CqlTemplateUnitTests {
@Rule public ExpectedException exception = ExpectedException.none();
private CqlTemplate template;
@Mock private Session mockSession;
@Before
public void setup() {
template = new CqlTemplate(mockSession);
}
@Test
@SuppressWarnings("unchecked")
public void firstColumnToObjectReturnsColumnValue() {
final Row mockRow = mock(Row.class);
ColumnDefinitions mockColumnDefinitions = mock(ColumnDefinitions.class);
Iterator<ColumnDefinitions.Definition> mockIterator = mock(Iterator.class);
final ColumnDefinitions.Definition mockColumnDefinition = mock(ColumnDefinitions.Definition.class);
when(mockRow.getColumnDefinitions()).thenReturn(mockColumnDefinitions);
when(mockColumnDefinitions.iterator()).thenReturn(mockIterator);
when(mockIterator.hasNext()).thenReturn(true);
when(mockIterator.next()).thenReturn(mockColumnDefinition);
template = new CqlTemplate() {
@Override
<T> T columnToObject(Row row, ColumnDefinitions.Definition columnDefinition) {
assertThat(row, is(sameInstance(mockRow)));
assertThat(columnDefinition, is(sameInstance(mockColumnDefinition)));
return (T) "test";
}
};
assertThat(String.valueOf(template.firstColumnToObject(mockRow)), is(equalTo("test")));
verify(mockRow, times(1)).getColumnDefinitions();
verify(mockColumnDefinitions, times(1)).iterator();
verify(mockIterator, times(1)).hasNext();
verify(mockIterator, times(1)).next();
verifyZeroInteractions(mockColumnDefinition);
}
@Test
@SuppressWarnings("unchecked")
public void firstColumnToObjectReturnsNull() {
Row mockRow = mock(Row.class);
ColumnDefinitions mockColumnDefinitions = mock(ColumnDefinitions.class);
Iterator<ColumnDefinitions.Definition> mockIterator = mock(Iterator.class);
when(mockRow.getColumnDefinitions()).thenReturn(mockColumnDefinitions);
when(mockColumnDefinitions.iterator()).thenReturn(mockIterator);
when(mockIterator.hasNext()).thenReturn(false);
assertThat(template.firstColumnToObject(mockRow), is(nullValue(Object.class)));
verify(mockRow, times(1)).getColumnDefinitions();
verify(mockColumnDefinitions, times(1)).iterator();
verify(mockIterator, times(1)).hasNext();
verify(mockIterator, never()).next();
}
@Test
@SuppressWarnings("unchecked")
public void processOneIsSuccessful() {
ResultSet mockResultSet = mock(ResultSet.class);
Row mockRow = mock(Row.class);
RowMapper<String> mockRowMapper = mock(RowMapper.class);
when(mockResultSet.one()).thenReturn(mockRow);
when(mockResultSet.isExhausted()).thenReturn(true);
when(mockRowMapper.mapRow(eq(mockRow), eq(0))).thenReturn("test");
assertThat(template.processOne(mockResultSet, mockRowMapper), is(equalTo("test")));
verify(mockResultSet, times(1)).one();
verify(mockResultSet, times(1)).isExhausted();
verify(mockRowMapper, times(1)).mapRow(eq(mockRow), eq(0));
verifyZeroInteractions(mockRow);
}
@Test
public void processOneThrowsIncorrectResultSetSizeDataAccessExceptionWhenNoRowsFound() {
ResultSet mockResultSet = mock(ResultSet.class);
RowMapper mockRowMapper = mock(RowMapper.class);
when(mockResultSet.one()).thenReturn(null);
try {
exception.expect(IncorrectResultSizeDataAccessException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage(containsString("expected 1, actual 0"));
template.processOne(mockResultSet, mockRowMapper);
} finally {
verify(mockResultSet, times(1)).one();
verify(mockResultSet, never()).isExhausted();
verifyZeroInteractions(mockRowMapper);
}
}
@Test
public void processOneThrowsIncorrectResultSetSizeDataAccessExceptionWhenTooManyRowsFound() {
ResultSet mockResultSet = mock(ResultSet.class);
Row mockRow = mock(Row.class);
RowMapper mockRowMapper = mock(RowMapper.class);
when(mockResultSet.one()).thenReturn(mockRow);
when(mockResultSet.isExhausted()).thenReturn(false);
try {
exception.expect(IncorrectResultSizeDataAccessException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage("ResultSet size exceeds 1");
template.processOne(mockResultSet, mockRowMapper);
} finally {
verify(mockResultSet, times(1)).one();
verify(mockResultSet, times(1)).isExhausted();
verifyZeroInteractions(mockRowMapper);
verifyZeroInteractions(mockRow);
}
}
@Test
public void processOnePassingNullResultSetThrowsIllegalArgumentException() {
RowMapper mockRowMapper = mock(RowMapper.class);
try {
exception.expect(IllegalArgumentException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage("ResultSet cannot be null");
template.processOne(null, mockRowMapper);
} finally {
verifyZeroInteractions(mockRowMapper);
}
}
@Test
@SuppressWarnings("unchecked")
public void processOneWithRequiredTypeIsSuccessful() {
ResultSet mockResultSet = mock(ResultSet.class);
final Row mockRow = mock(Row.class);
when(mockResultSet.one()).thenReturn(mockRow);
when(mockResultSet.isExhausted()).thenReturn(true);
template = new CqlTemplate() {
@Override
protected Object firstColumnToObject(Row row) {
assertThat(row, is(equalTo(mockRow)));
return 1l;
}
};
Number value = template.processOne(mockResultSet, Long.class);
assertThat(value, is(instanceOf(Long.class)));
assertThat(value.longValue(), is(equalTo(1l)));
verify(mockResultSet, times(1)).one();
verify(mockResultSet, times(1)).isExhausted();
verifyZeroInteractions(mockRow);
}
@Test
public void processOneWithRequiredTypeThrowsIncorrectResultSetSizeDataAccessExceptionWhenNoRowsFound() {
ResultSet mockResultSet = mock(ResultSet.class);
when(mockResultSet.one()).thenReturn(null);
try {
exception.expect(IncorrectResultSizeDataAccessException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage(containsString("expected 1, actual 0"));
template.processOne(mockResultSet, Integer.class);
} finally {
verify(mockResultSet, times(1)).one();
verify(mockResultSet, never()).isExhausted();
}
}
@Test
public void processOneWithRequiredTypeThrowsIncorrectResultSetSizeDataAccessExceptionWhenTooManyRowsFound() {
ResultSet mockResultSet = mock(ResultSet.class);
Row mockRow = mock(Row.class);
when(mockResultSet.one()).thenReturn(mockRow);
when(mockResultSet.isExhausted()).thenReturn(false);
try {
exception.expect(IncorrectResultSizeDataAccessException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage(containsString("ResultSet size exceeds 1"));
template.processOne(mockResultSet, Double.class);
} finally {
verify(mockResultSet, times(1)).one();
verify(mockResultSet, times(1)).isExhausted();
verifyZeroInteractions(mockRow);
}
}
@Test
public void processOneWithRequiredTypePassingNullResultSetThrowsIllegalArgumentException() {
exception.expect(IllegalArgumentException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage(is(equalTo("ResultSet cannot be null")));
template.processOne(null, String.class);
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2013-2016 the original author or authors
*
* 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 org.springframework.cassandra.support;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import com.datastax.driver.core.Session;
/**
* The CassandraAccessorUnitTests class is a test suite of test cases testing the contract and functionality of the
* {@link CassandraAccessor} class.
*
* @author John Blum
* @see org.springframework.cassandra.support.CassandraAccessor
* @since 1.5.0
*/
@RunWith(MockitoJUnitRunner.class)
public class CassandraAccessorUnitTests {
private CassandraAccessor cassandraAccessor;
@Mock private CassandraExceptionTranslator mockExceptionTranslator;
@Rule public ExpectedException exception = ExpectedException.none();
@Mock private Session mockSession;
@Before
public void setup() {
cassandraAccessor = new CassandraAccessor();
}
@Test
public void afterPropertiesSetWithUnitializedSessionThrowsIllegalStateException() {
exception.expect(IllegalStateException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage("Session must not be null");
cassandraAccessor.afterPropertiesSet();
}
@Test
public void setAndGetExceptionTranslator() {
cassandraAccessor.setExceptionTranslator(mockExceptionTranslator);
assertThat(cassandraAccessor.getExceptionTranslator(), is(sameInstance(mockExceptionTranslator)));
}
@Test
public void setExceptionTranslatorToNullThrowsIllegalArgumentException() {
exception.expect(IllegalArgumentException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage(is(equalTo("CassandraExceptionTranslator must not be null")));
cassandraAccessor.setExceptionTranslator(null);
}
@Test
public void getUninitializedExceptionTranslatorReturnsDefault() {
assertThat(cassandraAccessor.getExceptionTranslator(), is(equalTo(cassandraAccessor.exceptionTranslator)));
}
@Test
public void setAndGetSession() {
cassandraAccessor.setSession(mockSession);
assertThat(cassandraAccessor.getSession(), is(sameInstance(mockSession)));
}
@Test
public void setSessionToNullThrowsIllegalArgumentException() {
exception.expect(IllegalArgumentException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage(is(equalTo("Session must not be null")));
cassandraAccessor.setSession(null);
}
@Test
public void getUninitializedSessionThrowsIllegalStateException() {
exception.expect(IllegalStateException.class);
exception.expectCause(is(nullValue(Throwable.class)));
exception.expectMessage(is(equalTo("Session was not properly initialized")));
cassandraAccessor.getSession();
}
}

View File

@@ -49,6 +49,7 @@ import org.springframework.cassandra.core.WriteOptions;
import org.springframework.cassandra.core.keyspace.CreateTableSpecification;
import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.util.CollectionUtils;
import com.datastax.driver.core.BoundStatement;
@@ -607,19 +608,18 @@ public class CqlOperationsIntegrationTests extends AbstractKeyspaceCreatingInteg
/**
* Test that CQL for QueryForObject must only return 1 row or an IllegalArgumentException is thrown.
*/
@Test(expected = IllegalArgumentException.class)
@Test(expected = IncorrectResultSizeDataAccessException.class)
public void queryForObjectTestCqlStringRowMapperNotOneRowReturned() {
// Insert our 3 test books.
insertTestObjectArray();
@SuppressWarnings("unused")
Book book = cqlTemplate.queryForObject("select * from book where isbn in ('1234','2345','3456')",
Book book = cqlTemplate.queryForObject("SELECT * FROM book WHERE isbn IN('1234','2345','3456')",
new RowMapper<Book>() {
@Override
public Book mapRow(Row row, int rowNum) throws DriverException {
Book b = rowToBook(row);
return b;
return rowToBook(row);
}
});
}

View File

@@ -17,8 +17,8 @@ package org.springframework.data.cassandra.core;
import java.util.List;
import org.springframework.cassandra.core.CqlOperations;
import org.springframework.cassandra.core.Cancellable;
import org.springframework.cassandra.core.CqlOperations;
import org.springframework.cassandra.core.QueryForObjectListener;
import org.springframework.cassandra.core.QueryOptions;
import org.springframework.cassandra.core.WriteOptions;

View File

@@ -97,11 +97,16 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation
*/
public CassandraTemplate(Session session, CassandraConverter converter) {
setSession(session);
setConverter(converter != null ? converter : getDefaultCassandraConverter());
setConverter(resolveConverter(converter));
}
private static CassandraConverter getDefaultCassandraConverter() {
/* (non-Javadoc) */
private static CassandraConverter resolveConverter(CassandraConverter cassandraConverter) {
return (cassandraConverter != null ? cassandraConverter : getDefaultCassandraConverter());
}
/* (non-Javadoc) */
private static CassandraConverter getDefaultCassandraConverter() {
MappingCassandraConverter mappingCassandraConverter = new MappingCassandraConverter();
mappingCassandraConverter.afterPropertiesSet();
return mappingCassandraConverter;
@@ -110,8 +115,8 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation
/**
* Set the {@link CassandraConverter} used by this template to perform conversions.
*
* @param cassandraConverter Converter used to perform conversion of Cassandra data types to entity types.
* Must not be {@literal null}.
* @param cassandraConverter Converter used to perform conversion of Cassandra data types to entity types. Must not be
* {@literal null}.
*/
public void setConverter(CassandraConverter cassandraConverter) {
@@ -146,8 +151,8 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation
super.afterPropertiesSet();
Assert.notNull(cassandraConverter, "CassandraConverter must not be null!");
Assert.notNull(mappingContext, "CassandraMappingContext must not be null!");
Assert.notNull(cassandraConverter, "CassandraConverter must not be null");
Assert.notNull(mappingContext, "CassandraMappingContext must not be null");
}
@Override