Hibernate Envers - custom RevisionEntity - how to get record - hibernate-envers

I have written my custom RevisionEntity class to store additional data (for example username), like below:
#Entity
#RevisionEntity(AuditListener.class)
#Table(name = "REVINFO", schema = "history")
#AttributeOverrides({
#AttributeOverride(name = "timestamp", column = #Column(name = "REVTSTMP")),
#AttributeOverride(name = "id", column = #Column(name = "REV")) })
public class AuditEntity extends DefaultRevisionEntity {
private static final long serialVersionUID = -6578236495291540666L;
#Column(name = "USER_ID", nullable = false)
private Long userId;
#Column(name = "USER_NAME")
private String username;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
I can see that all rows in database are correctly stored, REVINFO table contains also username.
I would like to query database to get detailed information from my custom RevisionEntity, like username.
How can I do it? Is there any supported API to get it?

Lets assume you know the identifier of the entity you're interested in the revision entity metadata for, you can easily query that information using the following approach:
final AuditReader auditReader = AuditReaderFactory.get( session );
List<?> results = auditReader.createQuery()
.forRevisionsOfEntity( YourEntityClass.class, false, false )
.add( AuditEntity.id().eq( yourEntityClassId ) )
.getResultList();
The returned results will contain an Object array, e.g. Object[] where results[1] will hold the revision entity instance which contains the pertinent information your wanting.
For more details, you can see the java documentation comments here
If you only have the revision number, you can access just the revision entity instance directly by:
// I use YourAuditEntity here because AuditEntity is actually an Envers class
YourAuditEntity auditEntity = auditReader
.findRevision( YourAuditEntity.class, revisionId );
For more details on the AuditReader interface, you can see the java documentation here

Related

How to Select Only Specific Children in One to Many Relation in JPA

Here below is a simple model for a pet shop...
Pet Class
#Entity
#Table(name = "pet")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
public abstract class Pet {
#Column(name = "id", nullable = false)
private Long id;
#Column(name = "name", nullable = false)
private String name;
#Column(name = "birth_date", nullable = false)
private LocalDate birthDate;
#Column(name = "death_date")
private LocalDate deathDate;
#ManyToOne
#JoinColumn(name = "pet_shop_id", nullable = false, referencedColumnName = "id")
#Setter(AccessLevel.NONE)
private PetShop petShop;
public void setPetShop(PetShop petShop) {
setPetShop(petShop, true);
}
public void setPetShop(PetShop petShop, boolean add) {
this.petShop= petShop;
if (petShop!= null && add) {
petShop.addPet(this, false);
}
}
PetShop Class
#Entity
#Table(name = "pet_shop")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
public class PetShop {
#Column(name = "id", nullable = false)
private Long id;
...
#OneToMany(
mappedBy = "petShop",
fetch = FetchType.LAZY,
cascade = {CascadeType.ALL})
private List<Pet> pets= new ArrayList<>();
public void addPet(final Pet pet) {
addPet(pet, true);
}
public void addPet(final Pet pet, boolean set) {
if (pet!= null) {
if (pets.contains(pet)) {
pets.set(pets.indexOf(pet), pet);
} else {
pets.add(pet);
}
if (set) {
pet.setPetShop(this, false);
}
}
}
}
PetShopRepository Interface
public interface PetShopRepository
extends JpaRepository<PetShop, Long> {
#Query(
"SELECT DISTINCT ps FROM PetShop ps"
+ " JOIN ps.pets p"
+ " WHERE ps.id = :id AND p.deathDate IS NULL")
#Override
Optional<PetShop> findById(#NonNull Long id);
}
... and here is how to create a PetShop with 2 Pet instances (one alive and another one dead):
final Pet alive = new Pet();
alive.setName("cat");
alive.setCall("meow");
alive.setBirthDate(LocalDate.now());
final Pet dead = new Pet();
dead.setName("cat");
dead.setCall("meow");
dead.setBirthDate(LocalDate.now().minusYears(15L));
dead.setDeathDate(LocalDate.now());
final PetShop petShop = new PetShop();
petShop.getPets().add(alive);
petShop.getPets().add(dead);
petShopRepositiry.save(petShop);
Now I want to retrieve the PetShop and I'd assume it contains only pets that are alive:
final PetShop petShop = petShopRepository.findById(shopId)
.orElseThrow(() -> new ShopNotFoundException(shopId));
final int petCount = petShop.getPets().size(); // expected 1, but is 2
According to my custom query in PetShopRepository I'd expect petShop.getPets() returns a list with 1 element, but it actually returns a list with 2 elements (it includes also the dead pet).
Am I missing something? Any hint would be really appreciated :-)
This is because Jpa maintains the coherence of the relations despite your query.
I.e. : your query returns the shops having at least one pet alive. But, Jpa will return the shop with the complete set of pets. And you can probably see extra sql queries sent by Jpa (if you set show_sql=true) to refill pets collection on the returned shop.
Fundamently, it's not because you wanted to get the shops with living pets that these shops loose their dead pets.
To get it right you would have to design the pets collection so that it would filter the dead pets. Hibernate provides such annotations (#Filter and #FilterDef), but apparently JPA does not.
I don't think that filtering at #Postload would be a good idea, because you would have to put back the filtered dead pets in the collection before any flush in the database. That looks risky to me.

JPARepository - delete using date comparison with derived query

I'm trying to use JPARepository in Spring Boot to delete records that are less than a certain date, for for a given userid
Should be something like this Delete * from [table] where expiration_date < [date] and userid = [userid]
I thought I should be able to use one of the automatically generated methods
int deleteByExpiryDateBeforeAndUser(Date date, User user);
But this is generating a Select and not a Delete. What am I doing wrong?
Update
Entity class
#Getter
#Setter
#ToString
#Entity(name = "refresh_token")
public class RefreshToken {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#OneToOne
#JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
#Column(nullable = false, unique = true)
private String token;
#Column(nullable = false)
private Date expiryDate;
public RefreshToken() {
}
}
Repository class
#Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
#Modifying
void deleteByUserIdAndExpiryDateBefore(Long userId, Date expiryDate);
int deleteByUser(User user);
}
Here's how I'm calling it
#Transactional
public void deleteExpiredTokens(User user) {
refreshTokenRepository.deleteByUserIdAndExpiryDateBefore(user.getId(), new Date());
}
You see a select statement because Spring Data first loads entities by condition.
Then once entities became 'managed' Spring Data issues a delete query for each entity that was found.
If you want to avoid redundant SQL query - you have to consider #Query annotation.
Then your code will look like this:
#Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
// ...
#Query(value = "DELETE FROM refresh_token WHERE user_id =:userId AND expiry_date < :expiryDate", nativeQuery = true)
#Modifying
void deleteByUserIdAndExpiryDateBefore(Long userId, Date expiryDate);
//...
}

Spring Boot JPA #UpdateTimestamp not working with Postgresql

I'm facing strange issue last modified date is not getting updated automatically.
I'm using Postgresql Version 12.3 and Springboot 2.2.4.RELEASE
Here's my Entity Class
#Entity
#Table(name = "users")
#org.hibernate.annotations.Entity(
dynamicUpdate = true
)
#Data
public class Users {
#Id
#GeneratedValue(generator = "UUID")
#GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
#Column(updatable = false, nullable = false)
private String userId;
private String userName;
private String userEmail;
private String userPhoneNumber;
#CreationTimestamp
#Column(updatable = false)
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+05:30")
private Timestamp createdOn;
#UpdateTimestamp
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+05:30")
private Timestamp lastUpdatedOn;
}
Database Records:
createdon | lastupdatedon
2020-08-27 07:43:37.994 | 2020-08-27 07:43:37.994
2020-08-07 07:49:22.797 | 2020-08-07 07:49:22.797
2020-08-12 13:38:43.503 | 2020-08-12 13:38:43.503
You can see both createdOn and lastUpdatedOn are same. Even though the records updated frequently last modified date is not getting updated.
I'm saving record with jpa repository
ex:
usersRepository.save(user);
Can you try using PrePersist & PreUpdate annotation instead of CreationTimestamp & UpdateTimestampas to have more control on the entity and apply below-
#Entity
#Table(name = "users")
#org.hibernate.annotations.Entity(
dynamicUpdate = true
)
#Data
public class Users {
#Id
#GeneratedValue(generator = "UUID")
#GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
#Column(updatable = false, nullable = false)
private String userId;
private String userName;
private String userEmail;
private String userPhoneNumber;
#Column(updatable = false)
private Timestamp createdOn;
#Column
private Timestamp lastUpdatedOn;
#PrePersist
public void onInsert() {
createdOn = Timestamp.from(ZonedDateTime.now(ZoneId.of("Asia/Kolkata")).toInstant());
lastUpdatedOn = createdOn;
}
#PreUpdate
public void onUpdate() {
lastUpdatedOn = Timestamp.from(ZonedDateTime.now(ZoneId.of("Asia/Kolkata")).toInstant());
}
}
Are you calling the save method within a transaction, by chance?
The timestamps are populated when written to the DB, so you won't see their values until after the transaction has been committed.
When I ran into this issue, I had code that looked like this:
#Transactional
public Response createFooBusinessLogic(Foo foo) {
var createdEntity = fooRepository.save(foo);
return someMethodUsingCreatedTime(createdEntity);
}
There are two solutions:
If the dates are required by another entity in the DB transaction, you can use saveAndFlush instead of save. This forces a write to the DB, returning back your created and modified dates.
Otherwise, refactor the save call into another method and annotate that method with #Transactional.
I went with option (2), which looked like this:
public Response createFooBusinessLogic(Foo foo) {
var createdEntity = facade.saveFoo(foo);
return someMethodUsingCreatedTime(createdEntity);
}
// In Facade.java
#Transactional
public Foo saveFoo(Foo foo) {
return fooRepository.save(foo);
}
Note that for the transaction annotation to work, the call must not be self-invoking (ie. must be called on another class) and the method must be public.

Spring Data JPA order by value from OneToMany relation

I am trying to sort a result by nested collection element value. I have a very simple model:
#Entity
public class User {
#Id
#NotNull
#Column(name = "userid")
private Long id;
#OneToMany(mappedBy = "user")
private Collection<Setting> settings = new HashSet<>();
// getters and setters
}
#Entity
public class Setting {
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "userid")
private User user;
private String key;
private String value;
// getters and setters
}
public interface UserRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
I want to have a result returned sorted by the value of one setting.
Is it possible to order by user.settings.value where settings.name = 'SampleName' using Spring Data JPA with QueryDSL?
I've used JpaSpecificationExecutor. let's see findAll for example.
Page<T> findAll(#Nullable Specification<T> spec, Pageable pageable);
Before call this method you can create your specification dynamically (where condition) and Pageable object with dynamic Sort information.
For example
...
Specification<T> whereSpecifications = Specification.where(yourWhereSpeficiation);
Sort sortByProperty = Sort.by(Sort.Order.asc("property"));
PageRequest orderedPageRequest = PageRequest.of(1, 100, sortByProperty);
userRepository.findAll(whereSpecifications, PageRequest.of(page, limit, orderedPageRequest));

Hibernate Filter being ignored

My application uses Hibernate 5.02 and Wildfly 10 with a PostgreSQL 9.5 database. I'm trying to enable a filter on a #OneToMany collection held within an entity that is constructed via a NamedQuery. Unfortunately, it seems as if the filter is just ignored. Here are the different components, redacted for ease of reading.
#NamedNativeQueries({
#NamedNativeQuery(
name = "getAnalystProcess",
query = "SELECT * FROM analysis.analystprocess WHERE id = :processId",
resultClass = AnalystProcessEntity.class
)})
#FilterDef(
name = "analystProcessUnanalyzedMsgsFilter",
parameters = { #ParamDef(name = "processIds", type = "integer"), #ParamDef(name = "analystIds", type = "integer") })
#Filter(name = "analystProcessUnanalyzedMsgsFilter", condition = "analystprocess_id IN (:processIds) AND id NOT IN (SELECT msg_id FROM analysis.analyzedmsg WHERE analyst_id IN (:analystIds) AND analystprocess_id IN (:processIds)) ORDER BY process_msg_id")
#Entity
#Table(name = "analystprocess", schema = "analyst")
public class AnalystProcessEntity implements JPAEntity {
public static final String GET_PROCESS = "getAnalystProcess";
public static final String MSG_FILTER = "analystProcessUnanalyzedMsgsFilter";
public static final String MSG_FILTER_PROC_ID_PARAM = "processIds";
public static final String MSG_FILTER_ANALYST_ID_PARAM = "analystIds";
private static final long serialVersionUID = 1L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "process")
#OrderColumn(name = "process_msg_id")
#LazyCollection(LazyCollectionOption.EXTRA)
private List<MsgEntity> msgList;
#Entity
#Table(name = "msg", schema = "analyst")
public class MsgEntity implements JPAEntity {
...
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#JoinColumn(name = "analystprocess_id", referencedColumnName = "id")
private AnalystProcessEntity process;
#Column(name = "process_msg_id")
private Integer processMsgId;
private void buildAnalystProcess() {
LOG.info("Building AnalystProcessEntity");
analystUser.getJdbcSession().enableFilter(AnalystProcessEntity.MSG_FILTER)
.setParameter(AnalystProcessEntity.MSG_FILTER_PROC_ID_PARAM, analystProcessId)
.setParameter(AnalystProcessEntity.MSG_FILTER_ANALYST_ID_PARAM, analystUser.getId());
Query query = analystUser.getJdbcSession().getNamedQuery(AnalystProcessEntity.GET_PROCESS)
.setParameter("processId", analystProcessId);
// Query query = analystUser.getJdbcSession().createNativeQuery("SELECT * FROM analysis.analystprocess WHERE id = :processId")
// .setParameter("processId", analystProcessId)
// .addEntity(AnalystProcessEntity.class);
analystProcess = (AnalystProcessEntity) query.getSingleResult();
CREATE TABLE analysis.analystprocess (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_date TIMESTAMP NOT NULL DEFAULT now(),
...
);
CREATE TABLE analysis.msg (
id SERIAL PRIMARY KEY,
analystprocess_id INTEGER NOT NULL REFERENCES analysis.analystprocess(id) ON DELETE CASCADE ON UPDATE CASCADE,
process_msg_id INTEGER NOT NULL,
constraint tbl_statusid_analystprocessid unique(status_id, analystprocess_id)
);
As seen above, I have also tried the filter on constructing the AnalystProcessEntity class via createNativeQuery instead of getNamedQuery and no luck.
I also added a defaultCondition with hardcoded values into the #FilterDef just to see if it would execute the default condition and it still didn't.
I've tried the #Filter above the entity definition as well as above the class definition. I even came across a blog post which made it sound like the condition references entity fields (variable names) and not table fields (column names). Trying to stick to Java naming conventions in the Entity and Postgres naming conventions in the table, so I tried switching the references in the condition and to no avail.
I have sql logging turned on in Hibernate and the condition doesn't show up anywhere, as if it's just simply being ignored.
Any help would be greatly appreciated!
So, the problem was that I had the #FilterDef applied to the wrong class. It was my presumption that because I was constructing the AnalystProcessEntity which holds the MsgEntity collection (which I am trying to filter), that the #FilterDef would be applied to the AnalystProcessEntity class. Instead, it needs to be applied to the entity that it's actually filtering (hindsight being 20/20, that's pretty obvious).
Also, the actual condition needed to be modified to use complete references within the sub-select query.
I hope this helps someone at some point...
#NamedNativeQueries({
#NamedNativeQuery(
name = "getAnalystProcess",
query = "SELECT * FROM analysis.analystprocess WHERE id = :processId",
resultClass = AnalystProcessEntity.class
)})
#Filter(name = "analystProcessUnanalyzedMsgsFilter", condition = "id NOT IN (SELECT amsg.msg_id FROM analysis.analyzedmsg amsg WHERE amsg.analyst_id IN (:analystIds) AND amsg.analystprocess_id IN (:processIds))")
#Entity
#Table(name = "analystprocess", schema = "analyst")
public class AnalystProcessEntity implements JPAEntity {
public static final String GET_PROCESS = "getAnalystProcess";
public static final String MSG_FILTER = "analystProcessUnanalyzedMsgsFilter";
public static final String MSG_FILTER_PROC_ID_PARAM = "processIds";
public static final String MSG_FILTER_ANALYST_ID_PARAM = "analystIds";
private static final long serialVersionUID = 1L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "process")
#OrderColumn(name = "process_msg_id")
#LazyCollection(LazyCollectionOption.EXTRA)
private List<MsgEntity> msgList;
#FilterDef(
name = "analystProcessUnanalyzedMsgsFilter",
parameters = { #ParamDef(name = "processIds", type = "integer"), #ParamDef(name = "analystIds", type = "integer") })
#Entity
#Table(name = "msg", schema = "analyst")
public class MsgEntity implements JPAEntity {
...
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#JoinColumn(name = "analystprocess_id", referencedColumnName = "id")
private AnalystProcessEntity process;
#Column(name = "process_msg_id")
private Integer processMsgId;
Additionally, I ran into another problem with null's appearing in the collection, despite the fact that I am using an #OrderColumn, which I thought fixed that issue. It seems that with the use of the #Filter, null's are inserted in place of what ended up being filtered OUT (excluded).