Spring R2DBC DatabaseClient losing TZ information - java

I have a db entity with some Instant type fields.
org.springframework.data.r2dbc.core.DatabaseClient (which is now deprecated), had a method .as(..) to automatically map to a java entity and it honoured the timezones too. Not sure how that happened internally.
But using org.springframework.r2dbc.core.DatabaseClient which does not have an automatic mapper, I must use a .map(...) which provides the Rows and I map them like
row.get("blah", Instant.class)
But it just gives time in my local TZ, not UTC.
Anyone knows the root cause? Thanks.

Short Answer
I'm assuming that you are using the PostgresSQL database and appropriate driver. Also, I assume that you are using the TIMESTAMP DB type for your Instant filed. To get the correct timezone from org.springframework.r2dbc.core.DatabaseClient you can use TIMESTAMP WITH TIME ZONE as a data type.
Long Answer
I have created a test repo to test the behavior of R2dbcRepository and R2DBC DatabaseClient to retrieve timestamp and timestampz into Instant. To do so, I have created the following table:
CREATE TABLE test_table (id SERIAL PRIMARY KEY, timestamp_without_tz TIMESTAMP, timestamp_with_tz TIMESTAMP WITH TIME ZONE);
And implemented the following service:
package com.patotski.r2dbctimestamps.service;
import com.patotski.r2dbctimestamps.domain.TestEntity;
import com.patotski.r2dbctimestamps.repo.TestEntityRepo;
import lombok.RequiredArgsConstructor;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.Instant;
#Service
#RequiredArgsConstructor
public class TestEntityService {
private final TestEntityRepo testEntityRepo; // uses standard R2dbcRepository implementation
private final DatabaseClient databaseClient;
public Mono<TestEntity> saveUsingRepo(TestEntity entity) {
return testEntityRepo.save(entity);
}
public Mono<TestEntity> getByIdFromRepo(int id) {
return testEntityRepo.findById(id);
}
public Mono<TestEntity> saveUsingDbClient(TestEntity entity) {
return databaseClient.sql("INSERT INTO test_table (timestamp_without_tz, timestamp_with_tz) VALUES(:timestamp_without_tz, :timestamp_with_tz)")
.bind("timestamp_without_tz", entity.getTimestamp_without_tz())
.bind("timestamp_with_tz", entity.getTimestamp_with_tz())
.map(row -> TestEntity.builder()
.id(row.get("id", Integer.class))
.timestamp_with_tz(row.get("timestamp_with_tz", Instant.class))
.timestamp_without_tz(row.get("timestamp_without_tz", Instant.class))
.build()).first();
}
public Mono<TestEntity> getByIdFromDbClient(int id) {
return databaseClient.sql("SELECT * from test_table where id = :id")
.bind("id", id)
.map(row -> TestEntity.builder()
.id(row.get("id", Integer.class))
.timestamp_with_tz(row.get("timestamp_with_tz", Instant.class))
.timestamp_without_tz(row.get("timestamp_without_tz", Instant.class))
.build()).first();
}
}
And created tests that just store entity and retrieve asserting Timestamps:
#SpringBootTest
class TestEntityServiceTest {
#Autowired
TestEntityService testEntityService;
#Test
#DisplayName("Should store and retrieve Entity with both timestamp fields in correct timezones using R2DBC repo.")
void shouldStoreCorrectTimestampsAndRetriveWithRepo() {
Instant now = Instant.now();
TestEntity entity = TestEntity.builder()
.timestamp_with_tz(now)
.timestamp_without_tz(now)
.build();
TestEntity saved = testEntityService.saveUsingRepo(entity).block();
Assertions.assertThat(testEntityService.getByIdFromRepo(saved.getId()).block()).isNotNull()
.extracting(TestEntity::getId,
TestEntity::getTimestamp_without_tz,
TestEntity::getTimestamp_with_tz)
.containsExactly(saved.getId(), now, now);
}
#Test
#DisplayName("Should store and retrieve Entity with both timestamp fields in correct timezones using R2DBC DatabaseClient.")
void shouldStoreCorrectTimestampsAndRetriveWithDbClient() {
Instant now = Instant.now();
TestEntity entity = TestEntity.builder()
.timestamp_with_tz(now)
.timestamp_without_tz(now)
.build();
testEntityService.saveUsingDbClient(entity).block();
Assertions.assertThat(testEntityService.getByIdFromDbClient(1).block()).isNotNull()
.extracting(TestEntity::getId,
TestEntity::getTimestamp_without_tz,
TestEntity::getTimestamp_with_tz)
.containsExactly(1, now, now);
}
}
Results show that these 2 methods are not consistent with each other:
R2dbcRepository test always passes in all TZs
DatabaseClient test passes only when tests are running in UTC TZ and fails when not. The reason is that field defined as TIMESTAMP in DB gets TZ offset.
TIMESTAMPZ field works correctly in both cases always
Repo to reproduce: https://github.com/xp-vit/r2dbc-timestamps
Created and issue for Spring team to at least discuss: https://github.com/spring-projects/spring-data-r2dbc/issues/608

Related

Prevent Jackson auto convert time zone in spring boot

I using RestController in Spring Boot and I have problem with Jackson auto converting time zone. When I try to send date to RestController, the date is auto convert to my current time zone and I don't want to prevent it to happened.
If I send
"NGAY": "2022-07-29T17:00:00.000+00:00",
I want to receive
2022-07-29
no matter what the time zone is
Not
2022-07-30
This is code in model class
#Column(name = "NGAY")
private java.util.Date ngay;
And this is code in my RestController
#RestController
#RequestMapping("/api/sanluong/ngay")
public class Controller {
#PostMapping("/savedata")
public ResponseEntity<?> uploadFile(#RequestBody List<MLoadprofileView> lstData) {
ResponseData result = sanLuongNgayService.onSaveData(lstData);
return ResponseEntity.ok(result);
}
}
I tried in application.properties but it has no effect
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
spring.jackson.deserialization.ADJUST_DATES_TO_CONTEXT_TIME_ZONE = false
EDIT: I want to applied to all java.ulti.Date field in all class

Changing date format when sending a Response entity

We are currently using SpringBoot and PostgreSQL and we have problem with date format.
When we save or edit(sending POST request from front-end) something we need a YYYY-MM-DD format since any other type of format wont save anything to database so #JSONformat on Entity or some type of annotation like that is not possible. But when we are fetching example all users it would be nicer that we get DD-MM-YYYY in a response from server, and im not sure how to do that.
We can do it from front-end but code isn't nice so back-end solution would be better.
Thanks in advance!
You can create a mapper and map your entity object to a DTO that will be sent to your front-end.
Since you don't specify the class you are using for your dates I will use LocalDate as an example, but the same logic can be applied to your date class.
Let's say your entity class looks like that:
public class SampleEntity {
private LocalDate localDate;
}
Your DTO class will look like that:
public class SampleDto {
// the Jackson annotation to format your date according to your needs.
#DateTimeFormat(pattern = "DD-MM-YYYY")
private LocalDate localDate;
}
Then you need to create the mapper to map from SampleEntity to SampleDto and vice versa.
#Component
public class SampleMapper {
public SampleDto mapTo(final SampleEntity sampleEntity){
// do the mapping
SampleDto sampleDto = new SampleDto();
sampleDto.setLocalDate(sampleEntity.getLocalDate());
return sampleDto;
}
public SampleEntity mapFrom(final SampleDto sampleDto){
// do the mapping
SampleEntity sampleEntity = new SampleEntity();
sampleEntity.setLocalDate(sampleDto.getLocalDate());
return sampleEntity;
}
}
You can use all those in your controller like that:
#GetMapping
public ResponseEntity<SampleDto> exampleMethod() {
// service call to fetch your entity
SampleEntity sampleEntity = new SampleEntity(); // lets say this is the fetched entity
sampleEntity.setLocalDate(LocalDate.now());
SampleDto sampleDto = sampleMapper.mapTo(sampleEntity);
return ResponseEntity.ok(sampleDto);
}
With this solution, you can avoid adding Jackson annotations to your entity. Also with this solution, you can control exactly what your front-end can access. More on that here What is the use of DTO instead of Entity?

Spring boot mongoDB aggregation piple-line no property found for type

I have a collection of documents called 'appointment', and I am trying to group together the number of appointments per day and populate an List of objects call AppointmentSummary, where there will be one object per day, I am using Spring boot to try and achieve this however I keep running into issues.
I have created the following three classes in the same package
AppointmentSummaryRepository.java
public interface AppointmentSummaryRepository extends
MongoRepository<Appointment,String>, AppointmentSummaryRepositoryCustom {
}
AppointmentSummaryRepositoryCustom.java
public interface AppointmentSummaryRepositoryCustom {
List<AppointmentSummary> aggregate(LocalDate startDate, LocalDate endDate);
}
AppointmentSummaryRepositoryImpl.java
public class AppointmentSummaryRepositoryImpl implements AppointmentSummaryRepositoryCustom {
private final MongoTemplate mongoTemplate;
private final Logger log = LoggerFactory.getLogger(AppointmentSummaryRepositoryImpl.class);
#Autowired
public AppointmentSummaryRepositoryImpl(MongoTemplate mongoTemplate){
this.mongoTemplate = mongoTemplate;
}
#Override
public List<AppointmentSummary> aggregate(LocalDate startDate, LocalDate endDate){
log.debug("This is a request to aggerate appointment summary between {} to {}", startDate.toString(), endDate.toString());
MatchOperation matchOperation = getMatchOperation(startDate, endDate);
GroupOperation groupOperation = getGroupOperation();
log.debug("End group operaton");
ProjectionOperation projectionOperation = getProjectOperation();
return mongoTemplate.aggregate(Aggregation.newAggregation(
matchOperation,
groupOperation,
projectionOperation
), Appointment.class, AppointmentSummary.class).getMappedResults();
}
private MatchOperation getMatchOperation(LocalDate startDate, LocalDate endDate) {
log.debug("Begin Match Operation");
Criteria appointmentCriteria = where("appointment_date").gt(startDate).andOperator(where("appointment_date").lt(endDate));
log.debug("End Match Operation");
return match(appointmentCriteria);
}
private GroupOperation getGroupOperation() {
log.debug("Performing Group Operation");
return group("appointment_date")
.last("appointment_date").as("appointment_date")
.addToSet("id").as("appointmentIds")
.sum("id").as("count");
}
private ProjectionOperation getProjectOperation() {
log.debug("Begin project operation");
return project("appointment_date","appointmentIds","count")
.and("appointment_date").previousOperation();
}
Whenever I run the it, I keep getting the following error:
org.springframework.data.mapping.PropertyReferenceException: No property appointment found for type Appointment!
I believe the issue is happening in the following code segment, my understanding is that I initialize the different stages of the pipeline and pass them to the mongoTemplate and the 'getMappedResults' will map the fields from the two objects and populate the AppointmentSummary.class with the output of the aggregation pipeline?
return mongoTemplate.aggregate(Aggregation.newAggregation(
matchOperation,
groupOperation,
projectionOperation
), Appointment.class, AppointmentSummary.class).getMappedResults();
To note that the object Appointment does not have a field/property appointment. I added this in but when I ran the code I received another error message complaining of cannot find type date for Appointment.
Thanks in advance for your help!
Use below variant of mongo template aggregate which takes collection name instead of class.
mongoTemplate.aggregate(Aggregation.newAggregation(
matchOperation,
groupOperation,
projectionOperation
), "appointment", AppointmentSummary.class).getMappedResults();
The reason was that when you use typed variant spring runs validation on the fields used in the aggregation pipeline to match the field names in the pojo and fails when it doesn't find the alias.

JPA AttributeConverter is being ignored

I am trying to use an AttributeConverter to store the new Java 8 ZonedDateTime values in a MySQL database (DATETIME field) using Hibernate 4.3.0.
When I try to execute an update I get this error:
...Data truncation: Incorrect datetime value: '\xAC\xED\x00\x05sr\x00\x0Djava.time.Ser\x95]\x84\xBA\x1B"H\xB2\x0C\x00\x00xpw\x0D\x06\x00\x00\x07\xE0\x02\x11\x0B&\xEE\xE0\x08\x' for column...
I have read many SO answers saying to use the converter approach, so I wrote one:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.sql.Date;
import java.time.LocalDate;
import java.time.ZonedDateTime;
#Converter(autoApply = true)
public class ZonedDateTimeConverter implements AttributeConverter<ZonedDateTime, Date> {
#Override
public Date convertToDatabaseColumn(ZonedDateTime zonedDateTime) {
if (zonedDateTime == null) return null;
return java.sql.Date.valueOf(zonedDateTime.toLocalDate());
}
#Override
public ZonedDateTime convertToEntityAttribute(Date date) {
if (date == null) return null;
LocalDate localDate = date.toLocalDate();
return ZonedDateTime.from(localDate);
}
}
... but it never gets called.
I have even added jpaProperties.put("hibernate.archive.autodetection", "class, hbm"); to my jpaProperties, but no luck. The ZonedDateTimeConverter class is in the same package as my Entities, so it should get scanned.
What am I missing here?
Reading JPA 2.1 spec :
"The conversion of all basic types is supported except for the following: Id attributes (including the
attributes of embedded ids and derived identities), version attributes, relationship attributes, and
attributes explicitly annotated as Enumerated or Temporal or designated as such in the XML descriptor."
any chance your ZonedDateTime field is annotated with #Id or #Temporal in your entity ?
OK, this was my mistake. Part of my application is managed by Hibernate, but part of it is NOT. The object I'm having problems with is NOT managed by Hibernate, so it makes sense that the JPA stuff isn't getting called.

How to use joda time with JPA (eclipselink)?

I tried to use the DataTime in my entity class. Adding #Temporal(TemporalType.DATE) above the field, I got the error saying "The persistent field or property for a Temporal type must be of type java.util.Date, java.util.Calendar or java.util.GregorianCalendar". I can introduce the conversion back and forth; using setters and getters as follows:
#Temporal(TemporalType.DATE)
private Calendar attendanceDate;
public DateTime getAttendanceDate() {
return new DateTime(this.attendanceDate);
}
public void setAttendanceDate(DateTime attendanceDate) {
this.attendanceDate = attendanceDate.toCalendar(Locale.getDefault());
}
but I hope eclipselink to do it for me. I have gone thro' Persist Joda-time's DateTime via Hibernate. The answer suggesting to use hibernate, but I have to use eclipselink. I can use the DateTime object in my entity class with DB representation as BLOB, but I need it as Date. Is there anything like jodatime-eclipselink? Or any other suggestion? Thanks for the help.
Basic the link defines an EclipseLink Converter to convert from Joda DateTime to java.sql.Timestamp or Date.
You could use it, or define your own converter and use it through #Convert, #Converter in EclipseLink.
For DDL creation, the converter should define the initialize method and set the type on the mapping's field to java.sql.Timestamp.
Please log a bug (or vote for the existing one) on this in EclipseLink, we should have support for Joda.
I Try use joda-time-eclipselink-integration, but don't work, problably I made something wrong,
So I made more researchs and i found this link http://jcodehelp.blogspot.com.br/2011/12/persist-joda-datetime-with-eclipselink.html, they use #Converter annotation to convert the Joda Date Time.
I Try and works for me, I hope, works for you too.
I wanted to do the same thing, and Googling around actually led me here. I was able to accomplish this using the #Access annotation. First, you annotate the class like this
#Access(AccessType.FIELD)
public class MyClass
{
....
This provides field access to everything so you don't have to annotate the fields individually. Then you create a getter and setter for the JPA to use.
#Column(name="my_date")
#Temporal(TemporalType.DATE)
#Access(AccessType.PROPERTY)
private Date getMyDateForDB()
{
return myDate.toDate();
}
private void setMyDateForDB(Date myDateFromDB)
{
myDate = new LocalDate(myDateFromDB);
}
The #Access(AccessType.PROPERTY) tells JPA to persist and retrieve through these methods.
Finally, you'll want to mark your member variable as transient as follows
#Transient
private LocalDate myDate = null;
This stops JPA from trying to persist from the field as well.
That should accomplish what you're trying to do. It worked great for me.
Ahamed, you mentioned it wasn't working for you. Additionally you need to override the initialize method of the converter to define the desired field type:
#Override
public void initialize(DatabaseMapping mapping, Session session) {
((AbstractDirectMapping) mapping)
.setFieldClassification(java.sql.Timestamp.class);
}
The following is a working example based on the answers available in the topic
Basically the easiest approach is to use EclipseLink #Converter for a DateTime field in your Entity.
The converter usage looks as follows:
import org.eclipse.persistence.annotations.Convert;
import org.eclipse.persistence.annotations.Converter;
import javax.persistence.*;
import org.joda.time.DateTime;
#Entity
public class YourEntity {
#Converter(name = "dateTimeConverter", converterClass = your.package.to.JodaDateTimeConverter.class)
#Convert("dateTimeConverter")
private DateTime date;
And the converter itself:
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.converters.Converter;
import org.eclipse.persistence.sessions.Session;
import org.joda.time.DateTime;
import java.sql.Timestamp;
public class JodaDateTimeConverter implements Converter {
private static final long serialVersionUID = 1L;
#Override
public Object convertDataValueToObjectValue(Object dataValue, Session session) {
return dataValue == null ? null : new DateTime(dataValue);
}
#Override
public Object convertObjectValueToDataValue(Object objectValue, Session session) {
return objectValue == null ? null : new Timestamp(((DateTime) objectValue).getMillis());
}
#Override
public void initialize(DatabaseMapping mapping, Session session) {
// this method can be empty if you are not creating DB from Entity classes
mapping.getField().setType(java.sql.Timestamp.class);
}
#Override
public boolean isMutable() {
return false;
}
}
I am adding this for the purpose of easy copy-and-paste solution.
Solution is here
joda-time-eclipselink-integration
Answer from Atais works well. Below an upgrade to it.
You can omit #converter annotation by registering it globally.
At persistance.xml in persitence-unit add:
<mapping-file>META-INF/xyz-orm.xml</mapping-file>
and file META-INF/xyz-orm.xml with content:
<?xml version="1.0"?>
<entity-mappings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/orm" version="2.1">
<converter class="pl.ds.wsc.storage.converter.JodaDateTimeConverter"/>
</entity-mappings>
If your config file is META-INF/orm.xml then you can omit even first step because it is default confing for all persitence units.

Categories