DATACASS-302 - Support Cassandra time columns.

We now support Cassandra time columns via LocalTime types (JSR-310, Joda and ThreeTenBackport) in domain classes, queries and updates.

@Table
class Schedule {

  @Id String id;

  LocalTime scheduledAt;

}
This commit is contained in:
Mark Paluch
2018-04-06 14:16:37 +02:00
committed by John Blum
parent 3285b8020b
commit 2e20a21843
17 changed files with 432 additions and 30 deletions

View File

@@ -17,10 +17,22 @@ package org.springframework.data.cassandra.core.convert;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.cassandra.core.mapping.CassandraSimpleTypeHolder;
import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.datastax.driver.core.DataType.Name;
/**
* Value object to capture custom conversion. {@link CassandraCustomConversions} also act as factory for
@@ -37,6 +49,12 @@ public class CassandraCustomConversions extends org.springframework.data.convert
private static final List<Object> STORE_CONVERTERS;
/**
* Set of types that indicate usage of Cassandra's time type. Time is represented as long so we need to imply the type
* from an artificial simple type.
*/
private final static Set<Class<?>> NATIVE_TIME_TYPE_MARKERS;
static {
List<Object> converters = new ArrayList<>();
@@ -48,6 +66,23 @@ public class CassandraCustomConversions extends org.springframework.data.convert
STORE_CONVERTERS = Collections.unmodifiableList(converters);
STORE_CONVERSIONS = StoreConversions.of(CassandraSimpleTypeHolder.HOLDER, STORE_CONVERTERS);
List<? extends Class<?>> timeMarkers = STORE_CONVERTERS.stream() //
.filter(Converter.class::isInstance) //
.map(Object::getClass) //
.filter(it -> AnnotatedElementUtils.hasAnnotation(it, WritingConverter.class)) //
.filter(it -> {
CassandraType annotation = AnnotatedElementUtils.getMergedAnnotation(it, CassandraType.class);
return annotation != null && annotation.type() == Name.TIME;
}) //
.map(it -> {
ResolvableType classType = ResolvableType.forClass(it).as(Converter.class).getGeneric(0);
return classType.getRawClass();
}).collect(Collectors.toList());
NATIVE_TIME_TYPE_MARKERS = new HashSet<>(timeMarkers);
}
/**
@@ -58,4 +93,18 @@ public class CassandraCustomConversions extends org.springframework.data.convert
public CassandraCustomConversions(List<?> converters) {
super(STORE_CONVERSIONS, converters);
}
/**
* Returns {@literal true} if the {@link Class type} is used to denote Cassandra's {@code time} column type.
*
* @param type must not be {@literal null}.
* @return {@literal true} if the type maps to Cassandra's {@code time} column type.
* @since 2.1
*/
public boolean isNativeTimeTypeMarker(Class<?> type) {
Assert.notNull(type, "Type must not be null");
return NATIVE_TIME_TYPE_MARKERS.contains(ClassUtils.getUserClass(type));
}
}

View File

@@ -21,9 +21,14 @@ import java.util.Collections;
import java.util.List;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.convert.WritingConverter;
import org.springframework.util.ClassUtils;
import com.datastax.driver.core.DataType.Name;
/**
* Helper class to register JSR-310 specific {@link Converter} implementations to convert between Cassandra types in
* case the library is present on the classpath.
@@ -53,6 +58,8 @@ public abstract class CassandraJodaTimeConverters {
converters.add(CassandraLocalDateToLocalDateConverter.INSTANCE);
converters.add(LocalDateToCassandraLocalDateConverter.INSTANCE);
converters.add(MillisOfDayToLocalTimeConverter.INSTANCE);
converters.add(LocalTimeToMillisOfDayConverter.INSTANCE);
return converters;
}
@@ -89,4 +96,36 @@ public abstract class CassandraJodaTimeConverters {
source.getDayOfMonth());
}
}
/**
* Simple singleton to convert {@link Long}s to their {@link LocalTime} representation.
*
* @author Mark Paluch
*/
public enum MillisOfDayToLocalTimeConverter implements Converter<Long, LocalTime> {
INSTANCE;
@Override
public LocalTime convert(Long source) {
return LocalTime.fromMillisOfDay(source);
}
}
/**
* Simple singleton to convert {@link LocalTime}s to their {@link Long} representation.
*
* @author Mark Paluch
*/
@WritingConverter
@CassandraType(type = Name.TIME)
public enum LocalTimeToMillisOfDayConverter implements Converter<LocalTime, Long> {
INSTANCE;
@Override
public Long convert(LocalTime source) {
return (long) source.getMillisOfDay();
}
}
}

View File

@@ -16,11 +16,18 @@
package org.springframework.data.cassandra.core.convert;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.convert.WritingConverter;
import com.datastax.driver.core.DataType.Name;
/**
* Helper class to register JodaTime specific {@link Converter} implementations in case the library is present on the
@@ -44,6 +51,8 @@ public abstract class CassandraJsr310Converters {
converters.add(CassandraLocalDateToLocalDateConverter.INSTANCE);
converters.add(LocalDateToCassandraLocalDateConverter.INSTANCE);
converters.add(MillisOfDayToLocalTimeConverter.INSTANCE);
converters.add(LocalTimeToMillisOfDayConverter.INSTANCE);
return converters;
}
@@ -80,4 +89,38 @@ public abstract class CassandraJsr310Converters {
source.getDayOfMonth());
}
}
/**
* Simple singleton to convert {@link Long}s to their {@link LocalTime} representation.
*
* @author Mark Paluch
* @since 2.1
*/
public enum MillisOfDayToLocalTimeConverter implements Converter<Long, LocalTime> {
INSTANCE;
@Override
public LocalTime convert(Long source) {
return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(source));
}
}
/**
* Simple singleton to convert {@link LocalTime}s to their {@link Long} representation.
*
* @author Mark Paluch
* @since 2.1
*/
@WritingConverter
@CassandraType(type = Name.TIME)
public enum LocalTimeToMillisOfDayConverter implements Converter<LocalTime, Long> {
INSTANCE;
@Override
public Long convert(LocalTime source) {
return source.getLong(ChronoField.MILLI_OF_DAY);
}
}
}

View File

@@ -19,11 +19,18 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.convert.ThreeTenBackPortConverters;
import org.springframework.data.convert.WritingConverter;
import org.springframework.util.ClassUtils;
import org.threeten.bp.LocalDate;
import org.threeten.bp.LocalTime;
import org.threeten.bp.temporal.ChronoField;
import com.datastax.driver.core.DataType.Name;
/**
* Helper class to register {@link Converter} implementations for the ThreeTen Backport project in case it's present on
@@ -56,6 +63,8 @@ public abstract class CassandraThreeTenBackPortConverters {
converters.add(CassandraLocalDateToLocalDateConverter.INSTANCE);
converters.add(LocalDateToCassandraLocalDateConverter.INSTANCE);
converters.add(MillisOfDayToLocalTimeConverter.INSTANCE);
converters.add(LocalTimeToMillisOfDayConverter.INSTANCE);
return converters;
}
@@ -92,4 +101,39 @@ public abstract class CassandraThreeTenBackPortConverters {
source.getDayOfMonth());
}
}
/**
* Simple singleton to convert {@link Long}s to their {@link LocalTime} representation.
*
* @author Mark Paluch
* @since 2.1
*/
public enum MillisOfDayToLocalTimeConverter implements Converter<Long, LocalTime> {
INSTANCE;
@Override
public LocalTime convert(Long source) {
return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(source));
}
}
/**
* Simple singleton to convert {@link LocalTime}s to their {@link Long} representation.
*
* @author Mark Paluch
* @since 2.1
*/
@WritingConverter
@CassandraType(type = Name.TIME)
public enum LocalTimeToMillisOfDayConverter implements Converter<LocalTime, Long> {
INSTANCE;
@Override
public Long convert(LocalTime source) {
return source.getLong(ChronoField.MILLI_OF_DAY);
}
}
}

View File

@@ -37,13 +37,13 @@ import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.cassandra.core.convert.CassandraCustomConversions;
import org.springframework.data.cassandra.core.cql.CqlIdentifier;
import org.springframework.data.cassandra.core.cql.keyspace.CreateIndexSpecification;
import org.springframework.data.cassandra.core.cql.keyspace.CreateTableSpecification;
import org.springframework.data.cassandra.core.cql.keyspace.CreateUserTypeSpecification;
import org.springframework.data.cassandra.core.mapping.UserTypeUtil.FrozenLiteralDataType;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.CustomConversions.StoreConversions;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.context.AbstractMappingContext;
import org.springframework.data.mapping.context.MappingContext;
@@ -82,8 +82,7 @@ public class CassandraMappingContext
private @Nullable ClassLoader beanClassLoader;
private CustomConversions customConversions =
new CustomConversions(StoreConversions.of(CassandraSimpleTypeHolder.HOLDER), Collections.emptyList());
private CustomConversions customConversions = new CassandraCustomConversions(Collections.emptyList());
private Mapping mapping = new Mapping();
@@ -524,10 +523,7 @@ public class CassandraMappingContext
* @since 1.5
*/
public DataType getDataType(Class<?> type) {
return this.customConversions.getCustomWriteTarget(type)
.map(CassandraSimpleTypeHolder::getDataTypeFor)
.orElseGet(() -> getDataTypeFor(type));
return doGetDataType(type, this.customConversions.getCustomWriteTarget(type).orElse(type));
}
public TupleType getTupleType(CassandraPersistentEntity<?> persistentEntity) {
@@ -552,6 +548,7 @@ public class CassandraMappingContext
return getDataTypeWithUserTypeFactory(property, DataTypeProvider.EntityUserType);
}
@Nullable
private DataType getDataTypeWithUserTypeFactory(CassandraPersistentProperty property,
DataTypeProvider dataTypeProvider) {
@@ -591,6 +588,7 @@ public class CassandraMappingContext
return getDataTypeWithUserTypeFactory(property.getTypeInformation(), dataTypeProvider, property::getDataType);
}
@Nullable
private DataType getDataTypeWithUserTypeFactory(TypeInformation<?> typeInformation, DataTypeProvider dataTypeProvider,
Supplier<DataType> fallback) {
@@ -613,30 +611,56 @@ public class CassandraMappingContext
}
Optional<DataType> customWriteTarget = this.customConversions.getCustomWriteTarget(typeInformation.getType())
.map(CassandraSimpleTypeHolder::getDataTypeFor);
.map(it -> doGetDataType(typeInformation.getType(), it));
DataType dataType = customWriteTarget
.orElseGet(() -> this.customConversions.getCustomWriteTarget(typeInformation.getRequiredActualType().getType())
.filter(it -> !typeInformation.isMap()).map(it -> {
.orElseGet(() -> {
Class<?> propertyType = typeInformation.getRequiredActualType().getType();
return this.customConversions.getCustomWriteTarget(propertyType).filter(it -> !typeInformation.isMap())
.map(it -> {
if (typeInformation.isCollectionLike()) {
if (List.class.isAssignableFrom(typeInformation.getType())) {
return DataType.list(getDataTypeFor(it));
if (typeInformation.isCollectionLike()) {
if (List.class.isAssignableFrom(typeInformation.getType())) {
return DataType.list(doGetDataType(propertyType, it));
}
if (Set.class.isAssignableFrom(typeInformation.getType())) {
return DataType.set(doGetDataType(propertyType, it));
}
}
if (Set.class.isAssignableFrom(typeInformation.getType())) {
return DataType.set(getDataTypeFor(it));
}
}
return doGetDataType(propertyType, it);
return getDataTypeFor(it);
}).orElse(null));
}).orElse(null);
});
return dataType != null ? dataType
: typeInformation.isMap() ? getMapDataType(typeInformation, dataTypeProvider) : fallback.get();
}
/**
* Resolve Cassandra {@link DataType} with conditional handling of {@code time} data type.
*
* @param propertyType
* @param converted
* @return
*/
@Nullable
private DataType doGetDataType(Class<?> propertyType, Class<?> converted) {
if (customConversions instanceof CassandraCustomConversions) {
CassandraCustomConversions conversions = (CassandraCustomConversions) customConversions;
if (conversions.isNativeTimeTypeMarker(converted)
|| (conversions.isNativeTimeTypeMarker(propertyType) && Long.class.equals(converted))) {
return DataType.time();
}
}
return CassandraSimpleTypeHolder.getDataTypeFor(converted);
}
private TupleType getTupleType(DataTypeProvider dataTypeProvider, CassandraPersistentEntity<?> persistentEntity) {
List<DataType> types = new ArrayList<>();
@@ -656,7 +680,7 @@ public class CassandraMappingContext
DataType keyType = getDataTypeWithUserTypeFactory(keyTypeInformation, dataTypeProvider, () -> {
DataType type = getDataTypeFor(keyTypeInformation.getType());
DataType type = doGetDataType(keyTypeInformation.getType(), keyTypeInformation.getType());
if (type != null) {
return type;
@@ -667,7 +691,7 @@ public class CassandraMappingContext
DataType valueType = getDataTypeWithUserTypeFactory(valueTypeInformation, dataTypeProvider, () -> {
DataType type = getDataTypeFor(valueTypeInformation.getType());
DataType type = doGetDataType(valueTypeInformation.getType(), valueTypeInformation.getType());
if (type != null) {
return type;
@@ -680,7 +704,7 @@ public class CassandraMappingContext
}
@Nullable
private DataType getUserDataType(TypeInformation<?> property, DataType elementType) {
private DataType getUserDataType(TypeInformation<?> property, @Nullable DataType elementType) {
if (property.isCollectionLike()) {

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.cassandra.core.mapping;
import java.time.LocalTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -121,6 +122,9 @@ public class CassandraSimpleTypeHolder extends SimpleTypeHolder {
// override UUID to timeuuid as regular uuid as the favored default
classToDataType.put(UUID.class, DataType.uuid());
// override LocalTime to time as time columns are mapped to long by default
classToDataType.put(LocalTime.class, DataType.time());
return classToDataType;
}

View File

@@ -19,8 +19,11 @@ import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import static org.springframework.data.cassandra.core.query.Criteria.*;
import lombok.Data;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collections;
@@ -33,6 +36,7 @@ import java.util.stream.Stream;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.convert.MappingCassandraConverter;
import org.springframework.data.cassandra.core.cql.CqlTemplate;
import org.springframework.data.cassandra.core.mapping.BasicMapId;
@@ -78,6 +82,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI
SchemaTestUtils.potentiallyCreateTableFor(User.class, template);
SchemaTestUtils.potentiallyCreateTableFor(UserToken.class, template);
SchemaTestUtils.potentiallyCreateTableFor(BookReference.class, template);
SchemaTestUtils.potentiallyCreateTableFor(TimeClass.class, template);
SchemaTestUtils.truncate(User.class, template);
SchemaTestUtils.truncate(UserToken.class, template);
SchemaTestUtils.truncate(BookReference.class, template);
@@ -478,4 +483,11 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI
assertThat(ids).containsAll(expectedIds);
assertThat(iterations).isEqualTo(10);
}
@Data
static class TimeClass {
@Id LocalTime id;
LocalTime bar;
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2018 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.data.cassandra.core.convert;
import static org.assertj.core.api.Assertions.*;
import org.joda.time.LocalTime;
import org.junit.Test;
import org.springframework.data.cassandra.core.convert.CassandraJodaTimeConverters.LocalTimeToMillisOfDayConverter;
import org.springframework.data.cassandra.core.convert.CassandraJodaTimeConverters.MillisOfDayToLocalTimeConverter;
/**
* Unit tests for {@link CassandraJodaTimeConverters}.
*
* @author Mark Paluch
*/
public class CassandraJodaTimeConvertersUnitTests {
@Test // DATACASS-302
public void shouldConvertLocalTimeToLong() {
assertThat(MillisOfDayToLocalTimeConverter.INSTANCE.convert(3723000L))
.isEqualTo(LocalTime.fromMillisOfDay(3723000L));
}
@Test // DATACASS-302
public void shouldConvertLongToLocalTime() {
assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.MIDNIGHT)).isZero();
assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.fromMillisOfDay(3723000L)))
.isEqualTo(3723000L);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2018 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.data.cassandra.core.convert;
import static org.assertj.core.api.Assertions.*;
import java.time.LocalTime;
import org.junit.Test;
import org.springframework.data.cassandra.core.convert.CassandraJsr310Converters.LocalTimeToMillisOfDayConverter;
import org.springframework.data.cassandra.core.convert.CassandraJsr310Converters.MillisOfDayToLocalTimeConverter;
/**
* Unit tests for {@link CassandraJsr310Converters}.
*
* @author Mark Paluch
*/
public class CassandraJsr310ConvertersUnitTests {
@Test // DATACASS-302
public void shouldConvertLocalTimeToLong() {
assertThat(MillisOfDayToLocalTimeConverter.INSTANCE.convert(3723000L)).isEqualTo(LocalTime.of(1, 2, 3));
}
@Test // DATACASS-302
public void shouldConvertLongToLocalTime() {
assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.MIDNIGHT)).isZero();
assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.of(1, 2, 3))).isEqualTo(3723000L);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2018 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.data.cassandra.core.convert;
import static org.assertj.core.api.Assertions.*;
import org.junit.Test;
import org.springframework.data.cassandra.core.convert.CassandraThreeTenBackPortConverters.LocalTimeToMillisOfDayConverter;
import org.springframework.data.cassandra.core.convert.CassandraThreeTenBackPortConverters.MillisOfDayToLocalTimeConverter;
import org.threeten.bp.LocalTime;
/**
* Unit tests for {@link CassandraThreeTenBackPortConverters}.
*
* @author Mark Paluch
*/
public class CassandraThreeTenBackPortConvertersUnitTests {
@Test // DATACASS-302
public void shouldConvertLocalTimeToLong() {
assertThat(MillisOfDayToLocalTimeConverter.INSTANCE.convert(3723000L)).isEqualTo(LocalTime.of(1, 2, 3));
}
@Test // DATACASS-302
public void shouldConvertLongToLocalTime() {
assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.MIDNIGHT)).isZero();
assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.of(1, 2, 3))).isEqualTo(3723000L);
}
}

View File

@@ -35,10 +35,10 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.CassandraTemplate;
@@ -48,8 +48,6 @@ import org.springframework.data.cassandra.support.CassandraVersion;
import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTest;
import org.springframework.data.util.Version;
import org.assertj.core.api.Assertions;
import com.datastax.driver.core.DataType;
import com.datastax.driver.core.Duration;
import com.datastax.driver.core.LocalDate;
@@ -431,7 +429,7 @@ public class CassandraTypeMappingIntegrationTests extends AbstractKeyspaceCreati
operations.insert(entity);
AllPossibleTypes loaded = load(entity);
Assertions.assertThat(loaded.getAnEnum()).isEqualTo(entity.getAnEnum());
assertThat(loaded.getAnEnum()).isEqualTo(entity.getAnEnum());
}
@Test // DATACASS-280
@@ -545,6 +543,18 @@ public class CassandraTypeMappingIntegrationTests extends AbstractKeyspaceCreati
assertThat(loaded.getLocalTime()).isEqualTo(entity.getLocalTime());
}
@Test // DATACASS-296
public void shouldReadAndWriteJodaLocalTime() {
AllPossibleTypes entity = new AllPossibleTypes("1");
entity.setJodaLocalTime(org.joda.time.LocalTime.fromMillisOfDay(50000));
operations.insert(entity);
AllPossibleTypes loaded = load(entity);
assertThat(loaded.getJodaLocalTime()).isEqualTo(entity.getJodaLocalTime());
}
@Test // DATACASS-296
public void shouldReadAndWriteInstant() {

View File

@@ -27,13 +27,13 @@ import java.util.Map;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.cql.CqlIdentifier;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
@@ -348,6 +348,17 @@ public class QueryMapperUnitTests {
assertThat(mappedObject).contains(Criteria.where("tuple").is(tupleValue));
}
@Test // DATACASS-302
public void shouldMapTime() {
Filter filter = Filter.from(Criteria.where("localDate").gt(LocalTime.fromMillisOfDay(1000)));
Filter mappedObject = this.queryMapper.getMappedObject(filter,
this.mappingContext.getRequiredPersistentEntity(Person.class));
assertThat(mappedObject).contains(Criteria.where("localdate").gt(1000L));
}
@Test(expected = IllegalArgumentException.class) // DATACASS-523
public void referencingTupleElementsInQueryShouldFail() {
@@ -367,6 +378,8 @@ public class QueryMapperUnitTests {
Integer number;
LocalDate localDate;
MappedTuple tuple;
@Column("first_name") String firstName;

View File

@@ -18,6 +18,7 @@ package org.springframework.data.cassandra.core.convert;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import java.time.LocalTime;
import java.util.Collections;
import java.util.Currency;
import java.util.List;
@@ -238,6 +239,16 @@ public class UpdateMapperUnitTests {
assertThat(update.toString()).isEqualTo("tuple = ('foo')");
}
@Test // DATACASS-302
public void shouldMapTime() {
Update update = this.updateMapper.getMappedObject(Update.empty().set("localTime", LocalTime.of(1, 2, 3)),
this.persistentEntity);
assertThat(update.getUpdateOperations()).hasSize(1);
assertThat(update.toString()).isEqualTo("localtime = 3723000");
}
@Test(expected = IllegalArgumentException.class) // DATACASS-523
public void referencingTupleElementsInQueryShouldFail() {
this.updateMapper.getMappedObject(Update.empty().set("tuple.zip", "bar"), this.persistentEntity);
@@ -252,6 +263,7 @@ public class UpdateMapperUnitTests {
Map<String, Currency> map;
Map<Manufacturer, Currency> manufacturers;
Currency currency;
LocalTime localTime;
Integer number;
MappedTuple tuple;

View File

@@ -29,7 +29,6 @@ import java.util.NoSuchElementException;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
@@ -41,6 +40,7 @@ import org.springframework.data.cassandra.core.cql.keyspace.ColumnSpecification;
import org.springframework.data.cassandra.core.cql.keyspace.CreateIndexSpecification;
import org.springframework.data.cassandra.core.cql.keyspace.CreateIndexSpecification.ColumnFunction;
import org.springframework.data.cassandra.core.cql.keyspace.CreateTableSpecification;
import org.springframework.data.cassandra.domain.AllPossibleTypes;
import org.springframework.data.cassandra.support.UserTypeBuilder;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.MappingException;
@@ -463,6 +463,17 @@ public class CassandraMappingContextUnitTests {
.isEqualTo(DataType.list(DataType.varchar()));
}
@Test // DATACASS-302
public void propertyTypeShouldMapToTime() {
CassandraPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(AllPossibleTypes.class);
assertThat(mappingContext.getDataType(persistentEntity.getRequiredPersistentProperty("localTime")))
.isEqualTo(DataType.time());
assertThat(mappingContext.getDataType(persistentEntity.getRequiredPersistentProperty("jodaLocalTime")))
.isEqualTo(DataType.time());
}
@Test // DATACASS-172, DATACASS-455
public void shouldRegisterUdtTypes() {

View File

@@ -104,6 +104,7 @@ public class AllPossibleTypes {
org.joda.time.DateTime jodaDateTime;
org.joda.time.LocalDate jodaLocalDate;
org.joda.time.LocalDateTime jodaLocalDateTime;
org.joda.time.LocalTime jodaLocalTime;
org.threeten.bp.Instant bpInstant;
org.threeten.bp.LocalDate bpLocalDate;

View File

@@ -9,6 +9,7 @@ This chapter summarizes changes and new features for each release.
* Template API extended with `count(…)` and `exists(…)` methods accepting `Query`.
* <<cassandra.template.query.fluent-template-api,Fluent API>> for CRUD operations.
* Cassandra Mapped Tuple support via `@Tuple`.
* Support for Cassandra `time` columns via `LocalTime`.
* Support for `map` columns using User-defined/converted types.
* <<cassandra.mapping-usage.events>>

View File

@@ -95,6 +95,10 @@ for further details.
(Joda, Java 8, JSR310-BackPort)
| `date`
| `LocalTime`+
(Joda, Java 8, JSR310-BackPort)
| `time`
| `LocalDateTime`, `LocalTime`, `Instant` +
(Joda, Java 8, JSR310-BackPort)
| `timestamp`