#UpdateTimestamp not working on updating the user defined member variable - java

I am trying to update an entity by modifying the user defined member variable using the repository.save() method. The object is correctly updated and persisted but the timestamp of the member variable in the entity with the annotation #UpdateTimestamp is not getting updated.
My Entity classes are CASH & RATE:
#Entity
#Table(name = "CASH")
public class Cash{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "cash_id")
private List<Rate> rates = new ArrayList<>();
#ManyToOne(fetch = FetchType.EAGER)
private User createdBy;
#CreationTimestamp
#Column(name = "CREATED", nullable = false)
private ZonedDateTime created;
#Column(name = "LAST_MODIFIED", nullable = false)
#UpdateTimestamp
private ZonedDateTime lastModified;
#Entity
#Table(name = "RATE")
public class Rate {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#Column(name = "PRODUCT")
private String product;
#Column(name = "PRICE")
private String product;
Repositories for both the CASH and RATE class :
#Repository
public interface CashRepository extends JpaRepository<Cash, Long> {
Optional<Cash> findTopOrderByCreatedDesc();
}
#Repository
public interface RateRepository extends JpaRepository<Rate, Long> {
}
The update() method written in the service of Cash looks like :-
public void updateRate(Long productId, RateDto rateDto, Authentication authentication) {
User user = userService.getCurrentUser(authentication);
Optional<Cash> cashRate = cashRepository.findTopOrderByCreatedDesc();
if (Cash.isPresent()) {
Cash latestCash = cashRate.get();
List<Rate> rateList = latestCash.getRates();
for(Rate rate: rateList){
if(Objects.equals(rate.getId(), productId))){
rate.setProduct("Update Product");
}
}
rate newRates = mapToRates(rateDto, user); // the rateDto will have all the fields different(updated fields)
rateList.add(newRates);
latestCash.setRates(rateList);
Cash updatedCashAlternative = cashAlternativeProductsRepository.save(latestCash);
} else{
throw new CashException();
}
}
The latestCash object is perfectly getting saved with the updated list of rates but the lastModified field in the object is not getting updated.
Is #UpdateTimestamp only works if any of the non composite member variable is changed/updated. ??
Also i tried extending the CrudRepository instead of JpaRepository and tried updating it using the repository.update() method but it didn't work.

Changing the contents of an associated object (Rate) or the join table (cash_rates?) does not make the referring object Cash "dirty". I think if you switch to an #ElementCollection and make the Rate an #Embeddable, any change to these objects will make the object Cash be considered as "dirty" and hence the lastModified attribute will be updated.
If that doesn't work, you'll have to update the timestamp yourself in the code.

Related

How to save multiple Entities with relationship

In my spring boot application I have 3 Entities, meetingSetting, meetingDate and time. MeetingSettingsId is referenced in both entities date and time. time also have dateId as a reference. I know want to send such a json to my backend:
{
"meetingName":"hallo",
"meetingUrl":"",
"meetingPw":"s",
"dates":"2021-05-30",
"times":[
{
"startTime":"15:30",
"endTime":"16:30"
},
{
"startTime":"18:30",
"endTime":"19:30"
}
]
}
But I am getting the following error:
not-null property references a null or transient value : com.cbc.coorporateblinddateservice.entities.times.Time.meetingDate
I want to save with one method meetingSetting witht the shown json.
This is how my entities look:
#Entity
#Table(name = "meeting_date")
#Data
public class Date {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "meeting_date")
private String date;
#OneToMany(mappedBy = "meetingDate", cascade = CascadeType.ALL)
private List<Time> meetingTime = new ArrayList<>();
#ManyToOne()
#JoinColumn(name = "meeting_settings_name", nullable = false)
private MeetingsSetting meetingsSetting;
#JsonCreator
public static Date fromDateAsString(String dateString){
Date date = new Date();
date.setDate(dateString);
return date;
}
}
meetingSettingEntity:
#Entity
#Table(name = "meeting_settings")
#Data
public class MeetingsSetting {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "meeting_name")
private String meetingName;
#Column(name = "meeting_url")
private String meetingUrl;
#Column(name = "meeting_pw")
private String meetingPw;
#OneToMany(mappedBy = "meetingsSetting", cascade = CascadeType.ALL)
private Date dates = new Date();
#OneToMany(mappedBy = "meetingsSetting", cascade = CascadeType.ALL)
private List<Time> times = new ArrayList<>();
}
and finally time:
#Entity
#Table(name = "meeting_times")
#Data
public class Time {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "start_time")
private String startTime;
#Column(name = "end_time")
private String endTime;
#ManyToOne()
#JoinColumn(name = "meeting_settings_name", nullable = false)
private MeetingsSetting meetingsSetting;
#ManyToOne()
#JoinColumn(name = "meeting_date_id")
private Date meetingDate;
}
this is the save method I am using and here probably the error occurs. I guess I kind of have to tell to save time and date too if I am not wrong, but I did not know how to do it the service method calls the save from a jparepository:
#PostMapping("/")
public void saveMeeting(#RequestBody MeetingsSetting meetingsSetting){
meetingSettingService.saveMeeting(meetingsSetting);
}
The problem with your implementation is that you are providing dates field as a String, but in the backend model it's represented as Object of type Date.
You have to make sure your backend model represent your json, by for example introduce DTOs ( Data Transfert Object ) that matches your json model and then map to your entities.
However, if you are just developing this application for tests or as a proof of concept. Here a working solution, just add this method in your Date.java class :
#JsonCreator
public static Date fromDateAsString(String dateString){
Date date = new Date();
date.setDate(dateString);
return date;
}
The idea here is to help Jackson (which is the library responsible for deserializing json to Java Object) to create a Date Instance from the String provided in json.

How to audit a #JoinTable with #ManyToMany

I'm working on a Spring-Boot project with a H2 database. I have two entities Portfolio and Report, and there is a many-to-many association between the two.
I want those entities to be audited, so I followed this tutorial to audit through an AuditorAware interface with custom fields.
The two entities are well audited, the columns are created in the database. However, the join table portfolio_reports is not audited. How can I audit the join table as well ?
Portfolio.java
#Entity
#Table(name = "portfolio")
public class Portfolio extends Auditable<String> implements Serializable {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
#Unique
private String name;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
#JoinTable(name = "portfolio_report", joinColumns = #JoinColumn(name = "portfolio_id"), inverseJoinColumns = #JoinColumn(name = "report_id"))
private List<Report> reports;
// Getters and setters
}
Report.java
#Entity
#Table(name = "report")
public class Report extends Auditable<String> implements Serializable {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "axioma_id")
private Long axiomaId;
#Column(name = "name")
private String name;
#AuditJoinTable
#ManyToMany(mappedBy = "reports", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private List<Portfolio> portfolios;
// Getters and setters
}
Auditable.java
#MappedSuperclass
#EntityListeners(AuditingEntityListener.class)
public abstract class Auditable<U> {
#Version
#Column(name = "version_no")
protected Long versionNo;
#CreatedDate
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "created_date")
protected Date createdDate;
#LastModifiedDate
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "modified_date")
protected Date modifiedDate;
}
AuditorAwareImpl.java
public class AuditorAwareImpl implements AuditorAware<String> {
#Override
public Optional<String> getCurrentAuditor() {
return Optional.of("Admin");
}
}
PersistenceConfiguration.java
#Configuration
#EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class PersistenceConfiguration {
#Bean
public AuditorAware<String> auditorAware() {
return new AuditorAwareImpl();
}
}
Problem:
Clearly here Auditable should add some column to your intermediate table that maintains relation between Portfolio and Report and that table is created behind the scene and you don't have access to that table in your program. Only hibernate can use that table to maintain relation between your entities and do join operation.
Solution:
Here you should make Join table that maintain Many to Many relation between Portfolio and Report explicit so that you can have entity like PortfolioReport in your program that can extends from Auditable. Please read the following post to see how to do that: The best way to map a many-to-many association with extra columns when using JPA and Hibernate

How to issue one query instead of multiple queries for graph of entities

I have a Spring Data repository where I'm getting all positions for a set of accounts:
public interface PositionRepository extends JpaRepository<Position, PositionKey> {
#EntityGraph(value = "Position.all", type = EntityGraphType.LOAD)
List<Position> findByAccountIn(Set<Account> accounts);
}
Position has attributes that are also entities (a few nested levels):
#Entity
#NamedEntityGraph(name = "Position.all",
attributeNodes = {#NamedAttributeNode("account", subgraph = "Account.all"),
#NamedAttributeNode("product", subgraph = "Product.all")
})
#Data
public class Position {
#EmbeddedId
private PositionKey positionKey;
#MapsId("accountId")
#ManyToOne
#JoinColumn(name = "accountId")
private Account account;
#MapsId("productId")
#ManyToOne
#JoinColumn(name = "productId")
private Product product;
}
#Embeddable
#Data
public class PositionKey implements Serializable {
#Column(name = "accountId")
private Long accountId;
#Column(name = "productId")
private Long productId;
}
#Entity
#NamedEntityGraph(includeAllAttributes = true, name = "Product.all",
attributeNodes = {#NamedAttributeNode(value = "vendor", subgraph = "Vendor.all"),
#NamedAttributeNode(value = "type", subgraph = "ProductType.all")}
)
#Data
public class Product {
#Id
#Column(name = "id")
private Long id;
#ManyToOne
#JoinColumn(name = "typeId")
private ProductType type;
#ManyToOne
#JoinColumn(name = "vendorCode")
private Vendor vendor;
}
#Entity
#NamedEntityGraph(name = "Account.all")
#Data
public class Account {
#Id
#Column(name = "accountId")
private Long id;
}
I'm always returning the full entity graph of positions as serialized JSON to a client. So, I'm always going to need all attributes and nested attributes. The position instances are 350 max.
Despite using #NamedEntityGraph, I noticed that individual queries are still being issued. For example, I get a query for each unique vendor. It usually takes ~2-3 seconds to get the results from all the individual queries.
How can I tell JPA to issue one query with multiple joins? I can manually write this SQL (using multiple joins) and it returns in just a few milliseconds.
Update
The data is accessed as follows:
#RestController
#RequestMapping("/position")
#RequiredArgsConstructor
public class PositionController {
private final PositionRepository positionRepo;
#GetMapping
public List<Position> getAllPositions(Set<Account> accounts) {
return positionRepo.findByAccountIn(account);
}
}
When the list of positions is returned, they are serialized via Jackson. So, it's Jackson that actually accesses the data.

Spring Data Jpa not automatically taking Next Id (PK), it always starts from 1

I am developing Spring Boot (2.1.7.RELEASE) +Data Jpa + Postgres example. In this example I am explicitly passing EMP_ID value=100 and next I am allowing data-jpa to automatically take next Id which is 101. I am not sure why its not working in that way??
Employee.java
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(callSuper = true)
#Entity
public class Employee extends BaseEntity{
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "EMP_ID", unique = true, nullable = false)
private Integer empId;
#Column(name = "EMP_NAME", unique = true, nullable = false)
private String empName;
#Column(name = "EMP_EMAIL", unique = true, nullable = false)
private String empEmail;
#Builder(builderMethodName="eBuilder")
public Employee(Integer empId, String empName, String empEmail,
Instant createdDate, Instant lastUpdateDate,String createUser, String lastUpdateUser) {
super(createdDate, lastUpdateDate, createUser, lastUpdateUser);
this.empId = empId;
this.empName = empName;
this.empEmail = empEmail;
}
}
BaseEntity.java
#Data
#MappedSuperclass
#NoArgsConstructor
#AllArgsConstructor
#EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
#CreatedDate
#Column(name = "createdDate", nullable = false, updatable = false)
private Instant createdDate;
#Column(name = "lastUpdateDate", nullable = false)
#LastModifiedDate
private Instant lastUpdateDate;
#Column(name = "createUser", nullable = false, length = 50)
private String createUser;
#Column(name = "lastUpdateUser", length = 50)
private String lastUpdateUser;
}
MainApp.java
#SpringBootApplication
#EnableJpaAuditing
public class MyExampleApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(MyExampleApplication.class, args);
}
#Autowired
private EmployeeRepository employeeRepository;
#Override
public void run(String... args) throws Exception {
Employee e = Employee.eBuilder().empId(100).empName("Shrutika")
.empEmail("shrutika#hotmail.com")
.createUser("Shrutika")
.lastUpdateUser("Shrutika")
.build();
employeeRepository.save(e);
Employee e1 = Employee.eBuilder().empName("Shantaram")
.empEmail("shantaram#hotmail.com")
.createUser("Shantaram")
.lastUpdateUser("Shantaram")
.build();
employeeRepository.save(e1);
}
}
Even if I used below, still things doesn't works well
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emp_generator")
#SequenceGenerator(name="emp_generator", sequenceName = "emp_seq", allocationSize=1)
#Column(name = "EMP_ID", unique = true, nullable = false)
private Integer empId;
Spring JIRA: https://jira.spring.io/browse/DATAJPA-1588
Ensure the type EMP_ID on the database: SERIAL or Integer. For using IDENTITY with postgres it has to be SERIAL (https://www.postgresql.org/docs/8.1/datatype.html#DATATYPE-SERIAL).
I am explicitly passing EMP_ID value=100 and next I am allowing data-jpa to automatically take next Id which is 101. I am not sure why its not working in that way??
JB Nizet answered that in the comments:
a sequence generator consists in getting the next ID from a database sequence. Not in getting the next ID from the ID you last inserted yourself.
I always wanted to save records by always doing max id +1. Is there any way of doing this with Spring Data JPA
Again JB Nizet pointed out this is a terrible idea.
It would require a lock on the or at least the index for every insert including the select to determine the next id.
So: DON'T DO THIS
If you still want to do it Vlad Mihalcea describes how to implement a custom id generator.
This should allow you to implement your own generator.
Of course this is Hibernate specific.
This will work:
#GeneratedValue(strategy=GenerationType.SEQUENCE)

JPA refresh entity

I have two tables in my database USERS and ADDRESSES, each user can have many addresses.
I have build entity classes with NetBeans wizard, and it create the classes well:
#Entity
#Table(name = "USERS")
#XmlRootElement
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "ID")
private Integer id;
// Some fields.......
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private Collection<Address> addressCollection;
public User() {
}
// Getters and Setters.......
}
#Entity
#Table(name = "ADDRESSES")
#XmlRootElement
public class Address implements Serializable {
private static final long serialVersionUID = 1L;
#EmbeddedId
protected AddressPK addressPK;
// Some fields.......
#JoinColumn(name = "USER_ID", referencedColumnName = "ID", insertable = false, updatable = false)
#ManyToOne(optional = false)
private User user;
public Address() {
}
// Getters and Setters.......
}
#Embeddable
public class AddressPK implements Serializable {
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "ID")
private int id;
#Basic(optional = false)
#NotNull
#Column(name = "USER_ID")
private int userId;
public AddressPK() {
}
}
In the session I save the User instance of the current logged in user. But the User's addressCollection never updates when I change the database like:
Address newAddr = new Address();
// Sets values to newAddr......
AddressFacade addrDao = new AddressFacade();
addrDao.create(newAddr); // Succeeded
LoginManager.getCurrentUser().getAddressCollection(); // Returns the old address list.
How can I refresh the current user's instance to get the correct addressCollection?
First, when you have a bidirectional relationship, JPA requires that you keep both sides of the relationship in synch with each other. This allows caching entities and other performance enhancements to be enabled by many providers. In this case, when you set the USER_ID field, you should update the User's addressCollection that is affected by the change so that your object model stays in synch with what you are committing to the database.
An alternative is to force a refresh manually on the User instance. This can be done with a em.refresh(user) call, or through provider specific options and query hints. This is usually the least performant option though as it requires a database hit that isn't needed.

Categories

Resources