Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Data for Apache Cassandra converts java.time.LocalDateTime to UTC

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 
like image 694
jcslater Avatar asked Dec 11 '22 11:12

jcslater


2 Answers

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.

like image 163
mp911de Avatar answered May 14 '23 03:05

mp911de


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!

like image 42
jcslater Avatar answered May 14 '23 02:05

jcslater