JPA date not equal to Oracle date after save - oracle

I have the following oracle table..
create table post (id number(10,0) not null, text varchar2(255 char), title varchar2(255 char), update_date date, version number(10,0), primary key (id));
My entity looks something like this..
#Entity
#EntityListeners(AuditingEntityListener.class)
public class Post {
#Id Integer id;
String title;
String text;
#LastModifiedDate #Temporal(TemporalType.TIMESTAMP) Date updateDate;
#Version Integer version;
....
}
The repository is simply this...
public interface PostRepository extends JpaRepository<Post, Integer>{
}
The following test fails...
#RunWith(SpringRunner.class)
#SpringBootTest
public class PostRepositoryTest {
#Autowired protected JdbcTemplate jdbcTemplate;
#Autowired PostRepository postRepo;
private static final Integer TEST_POST_ID = -1;
private static final String TEST_TEXT = "This is the text.";
#Before
public void setup() {
String insertPostSql = "insert into POST (ID, TITLE, TEXT, UPDATE_DATE, VERSION) values (?, ?, ?, ?, ?)";
jdbcTemplate.update(insertPostSql, new Object[]{TEST_POST_ID, "Title 1.", TEST_TEXT, new Date(), 0});
}
#After
public void teardown() {
String deletePostSql = "delete from POST where ID = ?";
jdbcTemplate.update(deletePostSql, new Object[]{TEST_POST_ID});
}
#Test
public void testUpdateA() throws Exception {
Post post = postRepo.findById(TEST_POST_ID).get();
post.setTitle(TEST_TEXT+" Amendment.");
Post updatedPost = postRepo.save(post);
assertNotEquals(post.getVersion(), updatedPost.getVersion());
Post updatedPost2 = postRepo.save(updatedPost);
assertEquals(updatedPost.getVersion(), updatedPost2.getVersion());
}
}
The second update to the entity is persisted to the database even though nothing has changed. If I change the type of the update date column in the database to TIMESTAMP, the test succeeds. Unfortunately I can't do this, is there something else I can do as the date in the returned instance from the save call is not equal to the date in the database according to JPA dirty check.
Thanks in advance.

To resolve this I ended up implementing my own #PreUpdate method instead of using #LastModifiedDate and within this method I set the update date to an instance of timestamp and set the nano seconds to 0.
#PreUpdate
public void onPreUpdate(Post post){
Timestamp ts = new Timestamp(System.currentTimeMillis()); ts.setNanos(0);
post.setUpdateDate(ts);
}

Related

Primary key violation when inserting child entity

DB Schema (H2 database):
create table book (
id_book bigint auto_increment not null primary key,
title varchar(255) not null,
id_author bigint not null,
id_genre bigint not null
);
create table comment (
id_comment bigint auto_increment not null primary key,
id_book bigint not null,
comment_text varchar(255) not null
);
Domain classes:
public class Book {
#Id
#Column("id_book")
private Long id;
private String title;
#Column("id_author")
AggregateReference<Author, Long> author;
#Column("id_genre")
AggregateReference<Genre, Long> genre;
#MappedCollection(idColumn = "id_book", keyColumn = "id_comment")
List<Comment> comments = new ArrayList<>();
public void addComment(String commentText) {
comments.add(new Comment(commentText));
}
//getters and setters
}
public class Comment {
#Column("id_comment")
private Long id;
#Column("comment_text")
private String text;
public Comment(String text) {
this.text = text;
}
public Comment() {
}
//getters and setters
}
I have the problem when I add a comment to the book.
```java
#Override
#Transactional
public String addComment(long bookId, String commentText) {
var book = bookRepository.findById(bookId);
return book.map(b -> {
b.addComment(commentText);
b = bookRepository.save(b);
return bookConverter.convertToString(b);
}).orElse("Book not found!");
}
It generates SQL like this...
Executing SQL batch update [INSERT INTO "COMMENT" ("comment_text", "id_book", "id_comment") VALUES (?, ?, ?)]
... adds values for id_comment field like 0, 1, 2, 3 and these values intersect with existing ones. So I get Primary Key Violation. Why it adds id_comment field to the INSERT expression?
Why it adds id_comment field to the INSERT expression?
Because you told it to.
The following annotation tells Spring Data JDBC to store the index of the list in the id_comment column.
#MappedCollection(idColumn = "id_book", keyColumn = "id_comment")
Your data model is missing a column for the list index. Add that column and use it as keyColumn in the #MappedCollection annotation

How does insert ignore for batches work in Spring Boot repository

I am working in Spring Boot framework.
I have a working way to run batches of "insert ignore" but i don't fully understand how/why it works.
I am using the Persistable interface to have inserts be done in batches.
In addition i'm using SQLInsert to run insert ignore instead of insert.
The code below demonstrates it:
#Entity
#SQLInsert(sql = "insert ignore into my_table (created_at, data, id) VALUES (?, ?, ?)")
public class MyTable implements Persistable<String> {
#Id
#Column
private String id;
#Column
private String data;
#Column
private Timestamp createdAt;
#Id
#Column
private String data;
public String getId() {
calculateId();
return id;
}
#Override
public boolean isNew() {
return true;
}
}
===============
#Repository
public interface MyTableRepository extends JpaRepository<MyTable, String> {
#Transactional
#Modifying(clearAutomatically = true, flushAutomatically = true)
<S extends MyTable> List<S> saveAll(Iterable<S> entities);
}
In debug, and on the DB side, i see that the save is indeed done in batches.
The query shown in debug mode is of this sort:
["insert ignore into my_table (created_at, data, id) VALUES (?, ?, ?)"], Params:[(2022-06-08 17:44:35.041,data1,id1),(2022-06-08 17:44:35.042,data2,id2),(2022-06-08 17:44:35.042,data3,id3)]
I see that there are 3 new records created in the DB.
The question how does it work if the number of "?" is smaller than the numbers of parameters passed?
If you would try it directly on the DB you would get an error

MyBatis #Many / Spring-Boot

I'm beginner (sorry for my bad explanation, feel free to correct me) in MyBatis Spring-Boot, I have problem to understand and make it works #Many
I'm using 3 layer logic programming (Presentation Layer, Service Layer, Data Layer)
Thanks for your help :)
I have 3 Tables (it's TB_Products and not TB_Product as on the screenshot):
I would like to get data form table TB_Users and TB_Products to "put" it in DTO
I create 4 java object class SearchEntity, ProductEntity (for Data layer)
I create an interface SearchRepositoryMapper.
I also create a SearchService interface and SearchServiceImpl as well.
Java object class:
SearchEntity
public class SearchEntity implements Serializable{
private static final long serialVersionUID = -9143930742617602050L;
private String id;
private String firstName;
private String lastName;
private List<ProductEntity> products;
// Getters and Setters code .....
}
ProductEntity
public class ProductEntity implements Serializable{
private static final long serialVersionUID = -6525703679290992635L;
private String id;
private String productId;
private String product;
private String number;
private String date;
private String description;
// Getters and Setters code .....
}
SearchRepositoryMapper
public interface SearchRepositoryMapper {
// Get some fields from TB_Users and all fields from TB_Products
#Select("SELECT * FROM TB_Users WHERE id = #{id}")
#Results({
#Result(property = "id", column ="id"),
#Result(property = "firstName", column = "firstName"),
#Result(property = "lastName", column= "lastName"),
#Result(property = "products", javaType = List.class, column="id",
many = #Many(select = "getProductIdByUserId"))})
public SearchEntity findAllInfoByUserId(#Param("id") int id);
#Select("SELECT *, productId FROM TB_Products WHERE productId = #{id}")
public ArrayList<ProductEntity> getProductIdByUserId(#Param("id") int id);
// Find id by uderId and return null if it doesn't exist
#Select("SELECT id FROM TB_Users WHERE userId = #{userId}")
int findIdByUserId(#Param("userId") String userId);
}
SearchServiceImpl
#Service
public class SearchServiceImpl implements SearchService {
#Autowired
SearchRepositoryMapper searchRepository;
#Override
public SearchDto getAllInfoByUserId(String id) {
SearchDto returnValue = new SearchDto(); // Init returnValue as SearchDto
int searchId = searchRepository.findIdByUserId(id); // Init searchId with the TB_Users id
SearchEntity searchEntity = searchRepository.findAllInfoByUserId(searchId);
BeanUtils.copyProperties(searchEntity, returnValue);
return returnValue;
}
}
So when I execute the code and do a GET request I get this error message:
{
"message": "nested exception is org.apache.ibatis.executor.ExecutorException: Statement returned more than one row, where no more than one was expected."
}
I found out that come from the mapper and SearchEntity searchEntity = searchRepository.findAllInfoByUserId(searchId);
But i don't know how to resolve it. The way I wrote the code is wrong
Thanks to correct me
The exception clearly says that the query returns multiple results. Plese verify if the data in the table is correct.

Hibernate and JPA #PreUpdate and #PrePersist not working

I am trying to run some code just before Updating or Saving. I have in my entity:
#Data
#EqualsAndHashCode(callSuper=false)
#Table(name="file_management", uniqueConstraints = { #UniqueConstraint(columnNames = {"name"})})
#Entity
public class FileManagement {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
private long id;
#Column(name="name")
private String name;
#Column(name = "SEARCH_STRING", length = 1000)
#Getter #Setter
private String searchString;
#PreUpdate
#PrePersist
void updateSearchString() {
final String fullSearchString = StringUtils.join(Arrays.asList(
name),
" ");
this.searchString = StringUtils.substring(fullSearchString, 0, 999);
}
}
I have in my FileManagementRepository:
#Transactional
#Modifying
#Query("UPDATE FileManagement SET name = :name WHERE id = :id")
public void updateFile(long id, String name);
#Transactional
#Modifying
#Query(value = "INSERT INTO file_management (name, filename, created_by, create_date, is_active, is_deleted) VALUES (:name, :filename, :createdBy, :createdDate, :isActive, :isDeleted)", nativeQuery = true)
public void createFileWithFilename(String name, String filename, String createdBy, Date createdDate, boolean isActive, boolean isDeleted);
and in my FileManagementService.java
public void updateFileWithFilename(String id, String name) {
fileManagementRepository.updateFile(Long.parseLong(id), name);
}
public boolean createFileWithFilename(String name, String filename, String createdBy, Date createdDate, boolean isActive, boolean isDeleted) {
fileManagementRepository.createFileWithFilename(name, filename, createdBy, createdDate, isActive, isDeleted);
return true;
}
But the problem is the updateSearchString() method is not called when I update or insert a new row. The search_string column is (null)
Please help. Thanks.
I am guessing that entities that are inserted or updated using native SQL or JPQL in this way will by pass the persistent context and persistence context will not manage them. However , #PreUpdate and #PrePersist only work for the entities that are managed by persistence context , so your #PreUpdate and #PrePersist will not execute for them.
I think you should insert and update the entities in a more JPA way which ensure persistence context will manage them:
#Service
public class FileManagementService{
#Autowired
private FileManagementRepository fileManagementRepository;
#Transactional
public void updateFileWithFilename(String id, String name) {
Optional<FileManagement> file= fileManagementRepository.findById(id);
if(file.isPresent()){
file.get().setName(name);
}else{
throw new RuntimeException("Record does not exist");
}
}
#Transactional
public void createFileWithFilename(String name, String filename, String createdBy, Date createdDate, boolean isActive, boolean isDeleted) {
FileManagement file= new FileManagement(name,fileName,........);
fileManagementRepository.save(file);
}
}

Save LocalDateTime with namedParameterJdbcTemplate

I have a list of 4 million generated entities that I want to move into table. The entity have field with type LocalDateTime:
#Data
#AllArgsConstructor
#Entity
#Table(name = "invoices")
public class Invoice {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
Long id;
#Column(name = "exact_iss_time")
private LocalDateTime exactIssueTime;
#Column(name = "final_iss_time")
private LocalDateTime finalIssueTime;
#Column(name = "issuer")
private String issuer;
#Column(name = "groupid")
private Integer groupID;
protected Invoice() {
}
}
As it is a big number of entities I want to do it optimally - which I gues is with NamedParameterJdbcTemplate.batchUpdate() like this:
public int[] bulkSaveInvoices(List<Invoice> invoices){
String insertSQL = "INSERT INTO invoices VALUES (:id, :exactIssueTime, :finalIssueTime, :issuer, :groupID)";
SqlParameterSource[] sqlParams = SqlParameterSourceUtils.createBatch(invoices.toArray());
int[] insertCounts = namedParameterJdbcTemplate.batchUpdate(insertSQL, sqlParams);
return insertCounts;
}
However I keep getting error:
org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [INSERT INTO invoices VALUES (?, ?, ?, ?, ?)]; nested exception is org.postgresql.util.PSQLException: Can't infer the SQL type to use for an instance of java.time.LocalDateTime. Use setObject() with an explicit Types value to specify the type to use.
Am I doing this right - if so how to fix this LocalDateTime issue in this case?
If this is the wrong way than what is the most optimal way to INSERT the 4 million generated test entities into the table with Spring-boot?
database?
JPA 2.1 was released before Java 8 and therefore doesn’t support the new Date and Time API.
You can try add convertor, for more check How to persist LocalDate and LocalDateTime with JPA
#Converter(autoApply = true)
public class LocalDateTimeAttributeConverter implements AttributeConverter<LocalDateTime, Timestamp> {
#Override
public Timestamp convertToDatabaseColumn(LocalDateTime locDateTime) {
return (locDateTime == null ? null : Timestamp.valueOf(locDateTime));
}
#Override
public LocalDateTime convertToEntityAttribute(Timestamp sqlTimestamp) {
return (sqlTimestamp == null ? null : sqlTimestamp.toLocalDateTime());
}
}
Or you can try not jdbc approach, but use custom JPA batch

Resources