JdbcBatchItemWriter - setSql - How to fix timestamp type mismatch - spring-boot

I am using JdbcBatchItemWriter in my Springboot application to import data from a CSV file into an Oracle DB, but I’m not able to import in timestamps. I get the error Failed to convert property value of type 'java.lang.String' to required type 'java.time.LocalDateTime
I understand the error – I just don’t understand how to fix it. Is there a way to mark the insert statement so that it reads it as a timestamp?
I did try:
INSERT INTO HB357(START_DATE) VALUES (TO_DATE(:START_DATE, YYYY-MM-DD, HH:mm:SS)
And I tried
TO_TIMESTAMP(:START_DATE, YYYY-MM-DD HH24:mm:SS)
but it didn't work
I also have tried changing the entity type to String instead of LocalDataTime, but this results in not a valid month error.
So I then tried altering the session before the insert (with the entity type still as string):
itemWriter.setSql("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS' NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'");
And then running the insert state but also got the not a valid month error
ENTITY:
#Entity
#NamedQuery(name ="HB357.getListOfResponseIds", query = "SELECT RESPONSE_ID from HB357")
public class HB357 {
#Id
private String RESPONSE_ID;
#JsonFormat(pattern="yyyy-MM-dd#HH:mm:ss")
private LocalDateTime START_DATE;
private String STOVE_OPTION;
private String FIRST_NAME;
private String LAST_NAME;
private String ADDRESS;
private String CITY;
private String ZIPCODE;
private String COUNTY;
//Getters and Setters left out for brevity
Relevant Insert Function
#Bean
public JdbcBatchItemWriter<HB357> writer() {
JdbcBatchItemWriter<HB357> itemWriter = new JdbcBatchItemWriter<>();
itemWriter.setDataSource(appProperties.dataSource());
itemWriter.setSql("INSERT INTO HB357 (" +
"START_DATE, RESPONSE_ID, STOVE_OPTION, FIRST_NAME,LAST_NAME,ADDRESS,CITY,ZIPCODE,COUNTY,PHONE_NUMBER,EMAIL," +
"STOVE_LOCATION,EXEMPTIONS,AGI,INCENTIVE,INCENTIVE_AMT) " +
"VALUES (:START_DATE, :RESPONSE_ID, :STOVE_OPTION, :FIRST_NAME, :LAST_NAME, :ADDRESS, :CITY," +
":ZIPCODE, :COUNTY, :PHONE_NUMBER, :EMAIL, :STOVE_LOCATION, :EXEMPTIONS, :AGI," +
":INCENTIVE, :INCENTIVE_AMT)" );
itemWriter.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>());
return itemWriter;
}

create class eg . public class HB357ParameterSourceProvider implements ItemSqlParameterSourceProvider and implement/do transformation for datatype inside it.
eg MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("start time", <>")
return param
pojo -> use instant start time conver date into timestamp(in case of sql server) and set
ItemSqlParameterSourceProvider valueSetter = new HB357ParameterSourceProvider ()
writer.setItemSqlParameterSourceProvider(valueSetter)

Related

HQL Expects java.time.duration even though method-head and usage suggest LocalDateTime

I am writing a query within a JpaRepository which takes a String and 3 LocalDateTimes as parameters. Within the query I first compare the String like an Id and afterwards I use a different Column of the corresponding Entity to create LocalDateTimes using appropriate operators for Hibernate 6.
The Application starts up normal but when i call the query I get the following Error:
Argument [2023-01-23T11:43:59] of type [java.time.LocalDateTime] did not match parameter type [java.time.Duration (n/a)]
The Argument obviously got parsed correctly by the Restcontroller but Hibernate does not seem to create the query as expected.
The following is the Code for the Repository and the query in question:
public interface ExchangePairRepository extends JpaRepository<MyEntity, Long> {
#Query("SELECT ep.unit FROM MyEntity ep WHERE ep.id= :id AND ((ep.context = 'start' AND ((ep.unit = 'day' AND (:start+ ep.duration day) > :now) "
+ "OR (ep.unit = 'month' AND (:start+ ep.duration month) > :now))) OR (ep.context = 'end' AND ((ep.unit = 'day' AND (:now + ep.duration day) > :end) "
+ "OR (ep.unit = 'month' AND (:now + ep.duration month) > :end)) ))")
List<String> findViable(#Param("matNrOrig") String id, #Param("start") LocalDateTime start, #Param("end") LocalDateTime end,
#Param("now") LocalDateTime now);
}
Below is the Entity which i being used for the query:
#Entity
#Table(name = "my_entity")
#Data
public class MyEntity{
(...)
#Column(name = "id_orig")
private String idOrig;
´
#Column(name = "id_target")
private String idTarget;
#Column(name = "context")
private String context;
#Column(name = "duration")
private int duration;
#Column(name = "unit")
private String unit;
}
Is this a bug or am I doing something wrong? Any help is much appreciated.
I am using Hibernate 6.1.6.Final and Spring Boot 3.0.1 with Java 17
Edit:
Casting the parameters within the query solved the problem for now, though it does not look very pretty. I will just wait for the bug being fixed in one of the next releases.
This is a bug in the new parameter type inference logic. I can reproduce with just this query:
session.createQuery("select :dt + 1 day")
.setParameter("dt", LocalDateTime.now())
.getSingleResult();
I have opened an issue for you here: https://hibernate.atlassian.net/browse/HHH-16102
UPDATE
The following workaround is very ugly, but works:
session.createQuery("select cast(:dt as LocalDateTime) + 1 day")
.setParameter("dt", LocalDateTime.now())
.getSingleResult();

JPA CriteriaQuery Unable to locate appropriate constructor for sum of date diffrerence

I know this question look similar, but I think for me the case is different
this is the exception message
Exception while fetching data (/periodicReport/userReport) :
org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate
appropriate constructor on class
[com.analytics.entity.projections.UserParticipantCountAndDurationImpl].
Expected arguments are: java.util.UUID, long, java.util.Date [select
new
com.analytics.entity.projections.UserParticipantCountAndDurationImpl(generatedAlias0.userID,
count(distinct generatedAlias0.userID), sum(function('TIMEDIFF',
generatedAlias0.endTime, generatedAlias0.startTime))) from
com.analytics.entity.ConferenceParticipant as
generatedAlias0 group by generatedAlias0.userID order by :param0 desc]
I'm expecting the sum to be Long and I have specified it in the criteria query.
I don't have any idea where Date is defined
the snipet for the buider
Expression<Long> sum = criteriaBuilder.sum(
criteriaBuilder.function(
"TIMEDIFF",
Long.class,
participantRoot.<Date>get("endTime"),
participantRoot.<Date>get("startTime")
)
);
Expression<Long> count = criteriaBuilder.countDistinct(participantRoot.get("userID"));
reportQuery.select(criteriaBuilder.construct(
UserParticipantCountAndDurationImpl.class,
participantRoot.get("userID").as(UUID.class).alias(USER_ID),
count.as(Long.class).alias(PARTICIPANTS),
sum.as(Long.class).alias(PARTICIPANT_DURATION)
));
the class in question
#Data
#NoArgsConstructor
public class UserParticipantCountAndDurationImpl implements UserParticipantCountAndDuration, Serializable {
private UUID userID;
private long participants;
public long participantDuration;
public UserParticipantCountAndDurationImpl(
UUID userID,
long participants,
long participantDuration
) {
this.userID = userID;
this.participants = participants;
this.participantDuration = participantDuration;
}
}
And yes I have tried to change signature of the constructor. in that case the query will run, but then thows CastException since it java.util.Date
you need a Expression with the name of the unit you want to extract from the timediff function
public static class TimeUnitExpression extends BasicFunctionExpression<String> implements Serializable {
public TimeUnitExpression(CriteriaBuilderImpl criteriaBuilder, Class<String> javaType,
String functionName) {
super(criteriaBuilder, javaType, functionName);
}
#Override
public String render(RenderingContext renderingContext) {
return getFunctionName();
}
}
so your sum expression becomes
Expression<Long> sum = criteriaBuilder.sum(
criteriaBuilder.function(
"TIMEDIFF",
Long.class,
new TimeUnitExpression(null, String.class, "MILLISECOND"),
participantRoot.<Date>get("endTime"),
participantRoot.<Date>get("startTime")
)
);
TIMEDIFF return date time as the result, so it cant be casted to Long or double, the the Sql object in the raw result is DateTime, refer this.
I can solve this by using a function to get minutes from the datetime. but I solved this by getting difference of UNIXTIMESTAMP for both the datetime for the summation. Since UNIXTTIMESTAMP is always long I can cast it to Long or BigInteger

Spring Data Mongo doesn't persist field value as null - This doesn't works

I already went through the link: https://jira.spring.io/browse/DATAMONGO-1107 and also I dont see option to set spring.data.mongodb.persist-null-fields=true in application.properties file.
Spring Data Mongo doesn't even persist a field having value null. How to allow to save null value if I want to do so ?
Person.java
#Builder
#Data
#AllArgsConstructor
#NoArgsConstructor
#Document
public class Person {
private String id;
private String firstName;
private String lastName;
private String emailId;
#Field
private List<Hobbies> hobbies;
}
Hobbies.java
#Builder
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Hobbies {
private String interest;
private String sports;
}
save() - This method doesn't persist data.
private void savePersons() {
Hobbies hobbies1 = Hobbies.builder().interest("Indoor").sports("Chess").build();
Hobbies hobbies2 = Hobbies.builder().interest("Loveoor").sports("Table Tennis").build();
Hobbies hobbies3 = Hobbies.builder().interest("Gamedoor").sports("Cricket").build();
Hobbies hobbies4 = Hobbies.builder().interest("Happydoor").sports("Lawn Tennis").build();
Person john = Person.builder().firstName("John")
.emailId("john.kerr#gmail.com").hobbies(Arrays.asList(hobbies1, hobbies2)).build();
Person neha = Person.builder().firstName("Neha").lastName("Parate")
.emailId("john.kerr#gmail.com").hobbies(Arrays.asList(hobbies1, hobbies2, hobbies4)).build();
repository.saveAll(Arrays.asList(john, neha));
}
I dont see lastName has been saved in the document. Also, how to save Date as null value?
One sollution is maybe to use defaults.
Your schemas can define default values for certain paths. If you create a new document without that path set, the default will kick in.
So if you want to "allow null", just make it the default value by adding to your schema definition lastName: { type: String, default: null }. When there is no lastName (aka null) you get the default value (null).
I would rethink allowing null because it would make querying complicated: Query on key: null will retrieve you all document where key is null or where key doesn't exist.
This is now possible starting with Spring Data MongoDB 3.3..
For example:
#Field(write = Field.Write.ALWAYS)
private String lastName;

Initialize field with current year and assigned ID

I've got an entity that I am persisting. Its ID is automatically assigned when storing it into the database via Spring Repository.
In the same entity, I have a field build from the Id and the current year: "<current_year>-<id>".
In a method annotated with #PrePersist, the ID has not been assigned yet, so I wrote some code in a #PostPersist method:
#PostPersist
protected void setupOrderNumber() {
this.orderNumber = Calendar.getInstance().get(Calendar.YEAR) + "-" + id;
}
This code does not store the orderNumber into the database, because the entity was stored already.
How can I achieve such a result with JPA directly within the entity?
If not possible with JPA, I could use Hibernate with a #Formula annotation, but I am not sure how to write it: #Formula("extract(year from current_date) + '-' + id") does not seem to work.
As you've already noticed: In #PrePersist a generated ID is not available - just because the ID is set afterwards when persisting into the database. And no changes made in #PostPersist are persisted, just because the persist has already taken place...
You can use a #Formula, as long you don't need the value in the database. But I wouldn't use extract(year from current_date) - as this would change the orderNumber when the year changes - what is different to your experiment with #PostPersist.
Instead use a year field, which you initialize in #PrePersist and reference that one in your formula:
#Entity
public class MyEntity {
#Id
#GeneratedValue(...)
private Long id;
private int year;
#Formula("concat(id, '-', year)")
private String orderNumber;
#PrePersist
private void prePersist() {
this.year = Calendar.getInstance().get(Calendar.YEAR);
}
#PostPersist
private void postPersist() {
this.orderNumber = id + "-" + year;
}
}
I initialize the orderNumber in postPersist() as well, to have a valid value immediately after EntityManager.persist().

How to insert into db in spring-data?

I want to make a request that inserts data into my database. The table has 4 columns: ID_DOCUMENT (PK), ID_TASK, DESCRIPTION, FILEPATH
Entity
...
#Column(name = "ID_TASK")
private Long idTask;
#Column(name = "DESCRIPTION")
private String description;
#Column(name = "FILEPATH")
private String filepath;
...
Repository
#Modifying
#Query("insert into TaskDocumentEntity c (c.idTask, c.description, c.filepath) values (:id,:description,:filepath)")
public void insertDocumentByTaskId(#Param("id") Long id,#Param("description") String description,#Param("filepath") String filepath);
Controller
#RequestMapping(value = "/services/tasks/addDocument", method = RequestMethod.POST)
#ResponseBody
public void set(#RequestParam("idTask") Long idTask,#RequestParam("description") String description,#RequestParam("filepath") String filepath){
//TaskDocumentEntity document = new TaskDocumentEntity();
taskDocumentRepository.insertDocumentByTaskId(idTask,descriere,filepath);
}
When I run my test, I get this error:
Caused by: org.hibernate.hql.ast.QuerySyntaxException: expecting OPEN, found 'c' near line 1, column 32 [insert into TaskDocumentEntity c (c.idTask, c.descriere, c.filepath) values (:id,:descriere,:filepath)]
I tried to remove the alias c, and still doesn`t work.
Spring data provides out of the box save method used for insertion to database - no need to use #Query. Take a look at core concepts of springData (http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.core-concepts)
thus in your controller just create object TaskDocumentEntity and pass it to repository
#RequestMapping(value = "/services/tasks/addDocument", method = RequestMethod.POST)
#ResponseBody
public void set(#RequestParam("idTask") Long idTask,#RequestParam("description") String description,#RequestParam("filepath") String filepath){
// assign parameters to taskDocumentEntity by constructor args or setters
TaskDocumentEntity document = new TaskDocumentEntity(idTask,descriere,filepath);
taskDocumentRepository.save(document);
}
There is a way to do this but it depends on the db you're using. Below worked for me in Oracle (using Dual table):
#Repository
public interface DualRepository extends JpaRepository<Dual,Long> {
#Modifying
#Query("insert into Person (id,name,age) select :id,:name,:age from Dual")
public int modifyingQueryInsertPerson(#Param("id")Long id, #Param("name")String name, #Param("age")Integer age);
}
So in your case, it would be (if Oracle):
#Modifying
#Query("insert into TaskDocumentEntity (idTask,description,filepath) select :idTask,:description,:filepath from Dual")
public void insertDocumentByTaskId(#Param("idTask") Long id,#Param("description") String description,#Param("filepath") String filepath)
I'm not sure which db you're using, here's a link which shows at the bottom which db's support select stmts without a from clause : http://modern-sql.com/use-case/select-without-from

Resources