I am attempting to persist a java.time.LocalDateTime
object in my Cassandra database and keep it timezone agnostic. I am using Spring Data Cassandra to do this.
The problem is that somewhere along the line, something is treating these LocalDateTime
objects as if they are in the timezone of my server, and offsetting them to UTC time when it stores them in the database.
Is this a bug or a feature? Can I work around it in some way?
Configuration:
@Configuration
@EnableCassandraRepositories(
basePackages = "my.base.package")
public class CassandraConfig extends AbstractCassandraConfiguration{
@Override
protected String getKeyspaceName() {
return "keyspacename";
}
@Bean
public CassandraClusterFactoryBean cluster() {
CassandraClusterFactoryBean cluster =
new CassandraClusterFactoryBean();
cluster.setContactPoints("127.0.0.1");
cluster.setPort(9142);
return cluster;
}
@Bean
public CassandraMappingContext cassandraMapping()
throws ClassNotFoundException {
return new BasicCassandraMappingContext();
}
}
Booking record I wish to persist:
@Table("booking")
public class BookingRecord {
@PrimaryKeyColumn(
ordinal = 0,
type = PrimaryKeyType.PARTITIONED
)
private UUID bookingId = null;
@PrimaryKeyColumn(
ordinal = 1,
type = PrimaryKeyType.CLUSTERED,
ordering = Ordering.ASCENDING
)
private LocalDateTime startTime = null;
...
}
Simple Repository Interface:
@Repository
public interface BookingRepository extends CassandraRepository<BookingRecord> { }
Save Call:
...
@Autowired
BookingRepository bookingRepository;
...
public void saveBookingRecord(BookingRecord bookingRecord) {
bookingRepository.save(bookingRecord);
}
Here is the string used to populate the starttime in BookingRecord:
"startTime": "2017-06-10T10:00:00Z"
And here is the output from cqlsh after the timestamp has been persisted:
cqlsh:keyspacename> select * from booking ;
bookingid | starttime
--------------------------------------+--------------------------------
8b640c30-4c94-11e7-898b-6dab708ec5b4 | 2017-06-10 15:00:00.000000+0000
Cassandra stores a Date (timestamp
) as milliseconds since epoch without a specific timezone information. Timezone data is handled in the layers above Cassandra.
LocalDate
/LocalDateTime
represent a point in time relative to your local time. Before the date/time can be saved, it needs to be enhanced with a timezone to calculate the generic representation, which can be saved.
Spring Data uses your system-default timezone (Date.from(source.atZone(systemDefault()).toInstant())
).
If you need timezone precision and want to omit any implicit timezone conversions, use java.util.Date
directly which corresponds with Cassandra's (well, it's the Datastax Driver to be precise) storage format representation.
I do actually want to use LocalDateTime
and LocalDate
in my project, rather than java.util.Date
, since they are newer and have more attractive functionality.
After much searching I have found a workaround.
First, you must create custom implementations of Spring's Converter
interface as follows:
One for Date
to LocalDateTime
:
public class DateToLocalDateTime implements Converter<Date, LocalDateTime> {
@Override
public LocalDateTime convert(Date source) {
return source == null ? null : LocalDateTime.ofInstant(source.toInstant(), ZoneOffset.UTC);
}
}
And one for LocalDateTime
to Date
:
public class LocalDateTimeToDate implements Converter<LocalDateTime, Date> {
@Override
public Date convert(LocalDateTime source) {
return source == null ? null : Date.from(source.toInstant(ZoneOffset.UTC));
}
}
Finally, you must override the customConversions
method in CassandraConfig
as follows:
@Configuration
@EnableCassandraRepositories(basePackages = "my.base.package")
public class CassandraConfig extends AbstractCassandraConfiguration{
@Override
protected String getKeyspaceName() {
return "keyspacename";
}
@Override
public CustomConversions customConversions() {
List<Converter> converters = new ArrayList<>();
converters.add(new DateToLocalDateTime());
converters.add(new LocalDateTimeToDate());
return new CustomConversions(converters);
}
@Bean
public CassandraClusterFactoryBean cluster() {
CassandraClusterFactoryBean cluster =
new CassandraClusterFactoryBean();
cluster.setContactPoints("127.0.0.1");
cluster.setPort(9142);
return cluster;
}
@Bean
public CassandraMappingContext cassandraMapping()
throws ClassNotFoundException {
return new BasicCassandraMappingContext();
}
}
Thanks to mp911de for putting me in the ballpark of where to look for the solution!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With