Obtain InputStream from Blob by calling getBinaryStream - java

I have a Spring Boot CRUD application that has the Document class, which is a DB entity with a Blob field. I also have the DocumentWrapper class, which is a class that is used for the transmission of the document and has a MultiPartFile field.
So, Document is the entity that I am storing in the DB, and has a dedicated JPA repository, while DocumentWrapper is the "helper" entity that the controller accepts and returns.
So in my service layer, I perform a transformation between DocumentWrapper and Document types as follow, in order to use the DocumentRepository and using this class as Bean to perform the conversion:
#Configuration
public class DocumentWrapperConverter implements AttributeConverter<DocumentWrapper, Document>, Serializable {
#Autowired
private LobService lobService;
#Override
public DocumentWrapper convertToEntityAttribute(Document document) {
DocumentWrapper documentWrapper = new DocumentWrapper();
documentWrapper.setName(document.getName());
documentWrapper.setDescription(document.getDescription());
documentWrapper.setId(document.getId());
MultipartFile multipartFile = null;
try {
InputStream is = this.lobService.readBlob(document.getBlob());
multipartFile = new MockMultipartFile(document.getName(), document.getOriginalFilename(), document.getContentType().toString(), is);
} catch (IOException e) {
e.printStackTrace();
}
documentWrapper.setFile(multipartFile);
return documentWrapper;
}
#Override
public Document convertToDatabaseColumn(DocumentWrapper documentWrapper) {
Document document = new Document();
document.setName(documentWrapper.getName());
document.setDescription(documentWrapper.getDescription());
document.setId(documentWrapper.getId());
document.setContentType(documentWrapper.getContentType());
document.setFileSize(documentWrapper.getSize());
document.setOriginalFilename(documentWrapper.getOriginalFilename());
Blob blob = null;
try {
blob = this.lobService.createBlob(documentWrapper.getFile().getInputStream(), documentWrapper.getFile().getSize());
} catch (IOException e) {
e.printStackTrace();
}
document.setBlob(blob);
return document;
}
}
I encapsulated the logic to transform a Blob to an InputStream in the LobService and its implementor LobServiceImpl:
#Service
public class LobServiceImpl implements LobService {
#Autowired
private SessionFactory sessionFactory;
#Autowired
private DataSource dataSource;
#Transactional
#Override
public Blob createBlob(InputStream content, long size) {
return this.sessionFactory.getCurrentSession().getLobHelper().createBlob(content, size);
}
#Transactional
#Override
public InputStream readBlob(Blob blob) {
Connection connection = null;
try {
connection = this.dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
InputStream is = null;
try {
is = blob.getBinaryStream();
} catch (SQLException e) {
e.printStackTrace();
}
try {
assert connection != null;
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
return is;
}
}
This is my generic JPARepository interface:
#NoRepositoryBean
public interface GenericRepository<T extends JPAEntityImpl, S extends Serializable> extends JpaRepository<T, S> {}
Which is extended in my Document class:
#Repository
#Transactional
public interface DocumentRepository<T extends JPAEntityImpl, S extends Serializable> extends GenericRepository<Document, UUID> {
}
Also, the Document entity class:
#Entity
#Table(uniqueConstraints = {
#UniqueConstraint(columnNames = "id")
})
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "typeName",
defaultImpl = Document.class)
public class Document extends JPAEntityImpl{
#Id
#Convert(converter = UUIDConverter.class)
#GeneratedValue(generator = "UUID")
#GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
#Column(nullable = false, unique = true)
protected UUID id;
#Column(length = 1000, nullable = false)
protected String name;
#Column(length = 1000, nullable = false)
protected String description;
#Column(length = 1000, nullable = true)
protected String originalFilename;
#Column(length = 1000, nullable = false)
protected MediaType contentType;
#Column
protected long fileSize;
#Column
#Lob
protected Blob blob;
public Document() {}
// GETTERS, SETTERS, TOSTRING....
And the DocumentWrapper entity class:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "typeName",
defaultImpl = DocumentWrapper.class)
public class DocumentWrapper extends JPAEntityImpl {
private UUID id;
private String name;
private String description;
private MultipartFile file;
public DocumentWrapper() {}
// GETTERS, SETTERS, TOSTRING....
I am having problems in the method public InputStream readBlob(Blob blob). The relevant part of the error log is the following:
org.postgresql.util.PSQLException: This connection has been closed.
at org.postgresql.jdbc.PgConnection.checkClosed(PgConnection.java:883)
at org.postgresql.jdbc.PgConnection.getLargeObjectAPI(PgConnection.java:594)
at org.postgresql.jdbc.AbstractBlobClob.getLo(AbstractBlobClob.java:270)
at org.postgresql.jdbc.AbstractBlobClob.getBinaryStream(AbstractBlobClob.java:117)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.hibernate.engine.jdbc.SerializableBlobProxy.invoke(SerializableBlobProxy.java:60)
at com.sun.proxy.$Proxy157.getBinaryStream(Unknown Source)
at org.ICIQ.eChempad.services.LobServiceImpl.readBlob(LobServiceImpl.java:42)
...
This line is the one which produces the Exception:
is = blob.getBinaryStream();
So, I would like to know how can I retrieve an InputStream for the Blob that I am receiving in the method public InputStream readBlob(Blob blob), so I can make the transformation between Document and DocumentWrapper. ¿How can I restore this connection that has been closed to retrieve the InputStream of the Blob? ¿Are there any workarounds?
Looking for answers on the Internet I saw many people performing this transformation using a ResultSet, but I am using a JPARepository interface to manipulate the records of Document in the database, not raw SQL queries; So I do not know how to proceed in that way.
I already tried using #Transactional annotations in this method but it did not work. I also tried setting the spring.jpa.properties.hibernate.current_session_context_class property (application.properties) to the values org.hibernate.context.ThreadLocalSessionContext, thread and org.hibernate.context.internal.ThreadLocalSessionContext but none of them worked.
I also tried to create a new connection, but it did not work.
I tried opening and closing a Session before and after calling blob.getBinaryStream(); but it did not work.
It seems to me that we need the same connection that was used to retrieve the Blob in the first place, but at some point Spring closes it and I do not know how to restore it or avoid that the connection is closed.
I also know that working with Byte[] arrays in my Document class could simplify things, but I do need to work with InputStream since I work with large files and is not convenient that whole files are loaded in memory.
If you need any other information about my code please do not hesitate to ask. I will add any required extra information.
Any tip, help or workaround is very welcome. Thank you.

Whatever method calls your DocumentWrapperConverter#convertToEntityAttribute probably also loads the data from the database. It should be fine to annotate this caller method with #Transactional to keep the Connection through the transaction alive. Note though, that you have to keep the connection open until you close the input stream, so you should probably push the bytes of the input stream to some sink within the transaction. Not sure if your MockMultipartFile consumes the input stream and stores the data into a byte[] or a temporary file, but that's what you will probably have to do for this to work.

Okay, so finally I have found the error by myself. Thanks for the help #Christian Beikov, you were actually hinting in the correct direction. Other Stack Overflow solutions did not work for me, but at least they gave me some idea of what is going on. Check this and this.
The problem is due to Hibernate and the implementation of BLOB types. The buffer inside the BLOB can only be read once. If we try to read it again (by using bob.getBinaryStream()), a database connection and session is needed to reload it.
So the first thing that you need to do if you have this error is check that your Datasource object and your database configuration is properly working and that your methods are correctly delimited by transactional boundaries by using the #Transactional annotation.
If that does not work for you and you still have problems regarding the database connection / session you can explicitly configure a SessionFactory in order to explicitly manage the session. To do so:
Define a bean that provides a SessionFactory
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Properties;
#org.springframework.context.annotation.Configuration
public class HibernateUtil {
private static SessionFactory sessionFactory;
private static HibernateUtil hibernateUtil;
// Singleton
#Autowired
public HibernateUtil(Properties hibernateProperties)
{
try {
Configuration configuration = new Configuration().setProperties(hibernateProperties);
Properties properties = configuration.getProperties();
StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder().applySettings(properties);
sessionFactory = configuration.buildSessionFactory(builder.build());
HibernateUtil.hibernateUtil = this; // singleton
} catch (Throwable ex) {
// Make sure you log the exception, as it might be swallowed
System.err.println("Initial SessionFactory creation failed: " + ex);
throw new ExceptionInInitializerError(ex);
}
}
public static HibernateUtil getInstance() {
if (HibernateUtil.hibernateUtil == null)
{
// Init
Logger.getGlobal().info("HIBERNATE UTIL INSTANCE HAS NOT BEEN INITIALIZED BY SPRING");
}
return HibernateUtil.hibernateUtil;
}
public static SessionFactory getSessionFactory()
{
return HibernateUtil.sessionFactory;
}
public void shutdown() {
// Close caches and connection pools
getSessionFactory().close();
}
This class uses a singleton pattern, so everyone that request the HibernateUtil bean will have the same instance. The first initialization of this class is triggered by Spring Boot, so we can just use it from a global context by typing HibernateUtil.getSessionFactory() and receiving the same factory.
You can use that SessionFactory to delimit the boundaries of your transaction manually like this. I created a LobService class to wrap the logic to read and create BLOBs:
#Service
public class LobServiceImpl implements LobService {
#Transactional
#Override
public Blob createBlob(InputStream content, long size) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
Blob blob = session.getLobHelper().createBlob(content, size);
session.getTransaction().commit();
session.close();
return blob;
}
#Transactional
#Override
public InputStream readBlob(Blob blob) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
InputStream is = null;
try {
is = blob.getBinaryStream();
} catch (SQLException e) {
e.printStackTrace();
}
session.getTransaction().commit();
session.close();
return is;
}
}
If that does not work for you, in my project even if there was a database connection and session another error showed up in the same place as before: SQLException: could not reset reader. What you need to do is reload the database instance that contains the BLOB the second time that you try to access to it. So, you do an extra (useless) query, but in this case requerying for the same instance reloads the underlying BinaryStream and avoids that Exception. For example, in one of the methods of my service I receive DocumentWrapper, transform it to Document, forward call to DocumentService, and transform its return to a DocumentWrapper and returns it to the upper level of controllers):
#Override
public <S1 extends DocumentWrapper> S1 save(S1 entity) {
Document document = this.documentWrapperConverter.convertToDatabaseColumn(entity);
// Internally the BLOB is consumed, so if we use the same instance to return an exception will be thrown
// java.sql.SQLException: could not reset reader
Document documentDatabase = this.documentService.save(document);
// This seems silly, but is necessary to update the Blob and its InputStream
Optional<Document> documentDatabaseOptional = this.documentService.findById((UUID) documentDatabase.getId());
return documentDatabaseOptional.map(value -> (S1) this.documentWrapperConverter.convertToEntityAttribute(value)).orElse(null);
}
Please, notice that this line
// This seems silly, but is necessary to update the Blob and its InputStream
Optional<Document> documentDatabaseOptional = this.documentService.findById((UUID) documentDatabase.getId());
Fetches the same entity from the DB, but now the BinaryStream is reloaded and can be read again.
So, TL;DR reload the managed instance from the database if you already exhausted its BinaryStream buffer.

Related

IllegalStateException LogicalConnectionManagedImpl is closed Hibernate

I'm using H2 Database with hibernate on JAVA and I'm getting a weird error.
I have created my abstract repository to manage the basic CRUD operation.
The exception I am getting is this:
java.lang.IllegalStateException: org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl#d20d74a is closed
at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.errorIfClosed(AbstractLogicalConnectionImplementor.java:37)
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection(LogicalConnectionManagedImpl.java:135)
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getConnectionForTransactionManagement(LogicalConnectionManagedImpl.java:254)
at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.rollback(AbstractLogicalConnectionImplementor.java:116)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.rollback(JdbcResourceLocalTransactionCoordinatorImpl.java:294)
at org.hibernate.engine.transaction.internal.TransactionImpl.rollback(TransactionImpl.java:139)
at repositories.AbstractRepository.save(AbstractRepository.java:32)
at services.ResultService.saveResult(ResultService.java:76)
at services.API.WebRequestService.run(WebRequestService.java:124)
at services.API.ThreadService.run(ThreadService.java:67)
AbstractRepository save method:
public <T> T save(T t) {
Transaction transaction = null;
try (Session session = HibernateConfig.getSessionFactory().openSession()) {
transaction = session.beginTransaction();
Serializable entityId = session.save(t);
transaction.commit();
T createdEntity = (T) session.get(t.getClass(), entityId);
return createdEntity;
} catch (Exception e) {
if (transaction != null) {
transaction.rollback();
}
e.printStackTrace();
}
return null;
}
I am a CS student and I am not very much familiar with Hibernate. I'm not getting this error on my computer, only on other computers with the JAR file builded.
P.S English isn't my main language so I am very sorry if you don't understand me clearly!
After hours of debugging I found the error!
The error was that a column exceeded the length and the exception was coming from the catch block.
The catch block was trying to rollback something that its connection was already closed.
I hope this will be helpful to someone!
I got the same error when trying to create an embedded relationship between two tables using Hibernate version 5.5.3. Yes the above answer was helpful for me to debug the error in a single go. Thanks to #William. It was same in my case too, the catch block was trying to rollback the transaction due to an exception occurred in the Embeddable class. The issue was that I did not have a default constructor inside Embeddable class.
Thanks!
#Vikarm's answer and #William's answer pointed me to the right direction. You simply need to create your composite ID in the constructor of the class that has the EmbeddedId.
Here's how it looks like in code (full example is included for completeness purposes)
#Entity(name = "artwork_rating")
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Setter
public class ArtworkRating {
#EmbeddedId
private ArtworkRatingKey id; // This needs to be instantiated*
private int score;
private String content;
// --------- Relations --------- //
#ManyToOne
#MapsId("userId")
#JoinColumn(name = "user_id")
User user;
#ManyToOne
#MapsId("artworkId")
#JoinColumn(name = "artwork_id")
Artwork artwork;
// --------- Constructors --------- //
public ArtworkRating(int score, String content, User user, Artwork artwork) {
this.id = new ArtworkRatingKey(user.getId(), artwork.getId()); // *as shown here
this.score = score;
this.content = content;
this.user = user;
this.artwork = artwork;
}
}

Corrupted objects when using Spring Batch with Embedded datasource

I got strange results using Spring Batch in conjuction with embedded storages like H2.
Here is my case:
My project is just regular Spring Boot microservice. Domain layer has an entity Model class with transient List field. This field is backed by byte array field, which is in turn not transient and supposed to be stored. I use #PrePersist and
#PostLoad to serialize and deserialize list. After model is loaded I clear array so it doesn't take memory.
The service has two controllers: ModelController and AnalyzeController. Each of them executes Spring Batch job. First job creates Model and stores it into db. Second job pull model from db and use it for calculating.
#Entity
#Getter
#Setter
#Table(name = "Model")
#Slf4j
public class Model {
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "system-uuid")
#GenericGenerator(name = "system-uuid", strategy = "uuid2")
#Column(name = "id", length = 36)
private String id;
#Transient
private List<Cluster<ItemPoints>> clusters;
#Column(columnDefinition = "BYTEA")
private byte[] serializedClusters;
//region Serialization / Deserialization
#PrePersist
public void serializeClusters() {
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out)) {
oos.writeObject(clusters);
serializedClusters = out.toByteArray();
} catch (IOException e) {
log.error(SERIALIZATION_ERROR_MESSAGE, e);
}
}
#PostLoad
#SuppressWarnings("unchecked")
public void deserializeClusters() {
try (ByteArrayInputStream in = new ByteArrayInputStream(serializedClusters);
ObjectInputStream is = new ObjectInputStream(in)) {
clusters = (List<Cluster<TimeSeriesItemPoints>>) is.readObject();
} catch (IOException | ClassNotFoundException e) {
log.error(SERIALIZATION_ERROR_MESSAGE, e);
}
// clear serialized data as it is no more needed
serializedClusters = new byte[]{};
}
//endregion
}
Repository layer is provided by Spring (CrudRepository)
If I try to execute AnalyzeController after ModelController for the first time it works as expected, but further calls to repository return Models with empty array (and List won't be deserialized).
This happens only with embedded database and single DataSource (same for batch processing and models).
If I use two different DataSource, it works correctly. If I use an external database it works correctly even with a single DataSource (PostgreSQL for example). Also it works OK with single datasource and no Spring Batch.
Configuration for single datasource case:
#Configuration
#Profile("default")
public class DefaultDataSourceConfiguration {
#Bean
public DataSource batchDataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
}
}

Saving/updating data with spring

I get an javax.persistence.EntityNotFoundException from the following code:
#Transactional
public ManagementEmailConfig save(ManagementEmailConfig managementEmailConfig)
{
logger.info("Save Management Email Config");
try
{
managementEmailConfig = entityManager.merge(managementEmailConfig);
entityManager.flush();
} catch (Exception e)
{
//ERROR: com.xxx.app.dao.kpi.ManagementEmailConfigDAO -
Not able to save Management Email Config
//javax.persistence.EntityNotFoundException: Unable to find com.xxx.app.model.configuration.AlertCommunicationAddress with id 1260
logger.error("Not able to save Management Email Config", e);
return null;
}
return managementEmailConfig;
}
where the model looks like this (shortened version):
import java.io.Serializable;
import javax.persistence.*;
import java.util.List;
/**
* The persistent class for the MANAGEMENT_EMAIL_CONFIG database table.
*
*/
#Entity
#Table(name="MANAGEMENT_EMAIL_CONFIG")
#NamedQuery(name="ManagementEmailConfig.findAll", query="SELECT m FROM ManagementEmailConfig m")
public class ManagementEmailConfig implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name="MANAGEMENT_EMAIL_CONFIG_ID")
private long managementEmailConfigId;
//bi-directional many-to-one association to AlertCommunicationAddress
#OneToMany(mappedBy="managementEmailConfig")
private List<AlertCommunicationAddress> alertCommunicationAddresses;
public ManagementEmailConfig() {
}
public long getManagementEmailConfigId() {
return this.managementEmailConfigId;
}
public void setManagementEmailConfigId(long managementEmailConfigId) {
this.managementEmailConfigId = managementEmailConfigId;
}
public List<AlertCommunicationAddress> getAlertCommunicationAddresses() {
return this.alertCommunicationAddresses;
}
public void setAlertCommunicationAddresses(List<AlertCommunicationAddress> alertCommunicationAddresses) {
this.alertCommunicationAddresses = alertCommunicationAddresses;
}
public AlertCommunicationAddress addAlertCommunicationAddress(AlertCommunicationAddress alertCommunicationAddress) {
getAlertCommunicationAddresses().add(alertCommunicationAddress);
alertCommunicationAddress.setManagementEmailConfig(this);
return alertCommunicationAddress;
}
public AlertCommunicationAddress removeAlertCommunicationAddress(AlertCommunicationAddress alertCommunicationAddress) {
getAlertCommunicationAddresses().remove(alertCommunicationAddress);
alertCommunicationAddress.setManagementEmailConfig(null);
return alertCommunicationAddress;
}
}
The use case is that the user provides a new alertCommunicationAddress to an existing ManagementEmailConfig and I want create the alertCommunicationAddress then update the ManagementEmailConfig.
If you are using Spring you've made life really difficult for yourself by not using Spring features
I suggest you do the following:
Using Spring Data JPA, write a repository interface to allow
you to easily persist your entity:
public interface ManagementEmailConfigRepository extends JpaRepository { }
use it to persist your entity (save is insert if it's not there,
update if it is)
#Inject
private ManagementEmailConfigRepository managementEmailConfigRepository;
....
managementEmailConfigRepository.save(managementEmailConfig);
This gets rid of the following from your code:
needing to write a save method at all
needing to do a flush
no need for try catch type code
no need for that named query on your entity
(you get that for free on your repository)
I'll leave it up to you to decide where you want the #Transactional annotation; it really shouldn't be on your DAO layer but higher up, e.g. your service layer.

Java persistence - No parrent key found after flushing

I have two entity object that represent two database table.
There is a foreign key relationship beetween two fields in the tables..
I am try to create a new instance from one entity and persist, then try to create another and pass the first object to the secound object as paramter in the set...() method.
But it throws Constraint violation exception because parent key not found in the database then the transaction rolled back.
BPackage newCond = new BPackage();
newCond.setName(condomName);
//...
//...
try {
em.persist(newCond);
em.flush();
} catch (ConstraintViolationException e) {
LOG.warning(e.getMessage());
return null;
}
BAssocPackage newRel = new BAssocPackage();
newRel.setPackageId(newCond); //here try to pass..
//...
//...
try {
em.persist(newRel);
} catch (ConstraintViolationException e) {
LOG.warning(e.getMessage());
return null;
}
My EntityManager is container managed so I can't close it.
#Stateless
public class MainDataAccess implements MainDataAccessLocal {
#PersistenceContext(unitName = "WarmHomeColl-ejbPU")
private EntityManager em;
There is lot of properties in my entity classes, so i will not post all of them but this is my first entity:
public class BPackage implements Serializable {
//...
//...
#OneToMany(mappedBy = "packageId")
private Collection<BAssocPackage> bAssocPackageCollection;
//...
//..
public class BAssocPackage implements Serializable {
//...
//...
#JoinColumn(name = "PACKAGE_ID", referencedColumnName = "ID")
#ManyToOne
private BPackage packageId;
//...
//..
Please help me. :) Thank you!
Solved it.
There was a trigger on the id field in the database and i wasn't refereed to a right id.. when i deleted the trigger from the field it start working fine..

How to get old entity value in #HandleBeforeSave event to determine if a property is changed or not?

I'm trying to get the old entity in a #HandleBeforeSave event.
#Component
#RepositoryEventHandler(Customer.class)
public class CustomerEventHandler {
private CustomerRepository customerRepository;
#Autowired
public CustomerEventHandler(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
#HandleBeforeSave
public void handleBeforeSave(Customer customer) {
System.out.println("handleBeforeSave :: customer.id = " + customer.getId());
System.out.println("handleBeforeSave :: new customer.name = " + customer.getName());
Customer old = customerRepository.findOne(customer.getId());
System.out.println("handleBeforeSave :: new customer.name = " + customer.getName());
System.out.println("handleBeforeSave :: old customer.name = " + old.getName());
}
}
In the event I try to get the old entity using the findOne method but this return the new event. Probably because of Hibernate/Repository caching in the current session.
Is there a way to get the old entity?
I need this to determine if a given property is changed or not. In case the property is changes I need to perform some action.
If using Hibernate, you could simply detach the new version from the session and load the old version:
#RepositoryEventHandler
#Component
public class PersonEventHandler {
#PersistenceContext
private EntityManager entityManager;
#HandleBeforeSave
public void handlePersonSave(Person newPerson) {
entityManager.detach(newPerson);
Person currentPerson = personRepository.findOne(newPerson.getId());
if (!newPerson.getName().equals(currentPerson.getName)) {
//react on name change
}
}
}
Thanks Marcel Overdijk, for creating the ticket -> https://jira.spring.io/browse/DATAREST-373
I saw the other workarounds for this issue and want to contribute my workaround as well, cause I think it´s quite simple to implement.
First, set a transient flag in your domain model (e.g. Account):
#JsonIgnore
#Transient
private boolean passwordReset;
#JsonIgnore
public boolean isPasswordReset() {
return passwordReset;
}
#JsonProperty
public void setPasswordReset(boolean passwordReset) {
this.passwordReset = passwordReset;
}
Second, check the flag in your EventHandler:
#Component
#RepositoryEventHandler
public class AccountRepositoryEventHandler {
#Resource
private PasswordEncoder passwordEncoder;
#HandleBeforeSave
public void onResetPassword(Account account) {
if (account.isPasswordReset()) {
account.setPassword(encodePassword(account.getPassword()));
}
}
private String encodePassword(String plainPassword) {
return passwordEncoder.encode(plainPassword);
}
}
Note: For this solution you need to send an additionally resetPassword = true parameter!
For me, I´m sending a HTTP PATCH to my resource endpoint with the following request payload:
{
"passwordReset": true,
"password": "someNewSecurePassword"
}
You're currently using a spring-data abstraction over hibernate.
If the find returns the new values, spring-data has apparently already attached the object to the hibernate session.
I think you have three options:
Fetch the object in a separate session/transaction before the current season is flushed. This is awkward and requires very subtle configuration.
Fetch the previous version before spring attached the new object. This is quite doable. You could do it in the service layer before handing the object to the repository. You can, however not save an object too an hibernate session when another infect with the same type and id it's known to our. Use merge or evict in that case.
Use a lower level hibernate interceptor as described here. As you see the onFlushDirty has both values as parameters. Take note though, that hibernate normally does not query for previous state of you simply save an already persisted entity. In stead a simple update is issued in the db (no select). You can force the select by configuring select-before-update on your entity.
Create following and extend your entities with it:
#MappedSuperclass
public class OEntity<T> {
#Transient
T originalObj;
#Transient
public T getOriginalObj(){
return this.originalObj;
}
#PostLoad
public void onLoad(){
ObjectMapper mapper = new ObjectMapper();
try {
String serialized = mapper.writeValueAsString(this);
this.originalObj = (T) mapper.readValue(serialized, this.getClass());
} catch (Exception e) {
e.printStackTrace();
}
}
}
I had exactly this need and resolved adding a transient field to the entity to keep the old value, and modifying the setter method to store the previous value in the transient field.
Since json deserializing uses setter methods to map rest data to the entity, in the RepositoryEventHandler I will check the transient field to track changes.
#Column(name="STATUS")
private FundStatus status;
#JsonIgnore
private transient FundStatus oldStatus;
public FundStatus getStatus() {
return status;
}
public FundStatus getOldStatus() {
return this.oldStatus;
}
public void setStatus(FundStatus status) {
this.oldStatus = this.status;
this.status = status;
}
from application logs:
2017-11-23 10:17:56,715 CompartmentRepositoryEventHandler - beforeSave begin
CompartmentEntity [status=ACTIVE, oldStatus=CREATED]
Spring Data Rest can't and likely won't ever be able to do this due to where the events are fired from. If you're using Hibernate you can use Hibernate spi events and event listeners to do this, you can implement PreUpdateEventListener and then register your class with the EventListenerRegistry in the sessionFactory. I created a small spring library to handle all of the setup for you.
https://github.com/teastman/spring-data-hibernate-event
If you're using Spring Boot, the gist of it works like this, add the dependency:
<dependency>
<groupId>io.github.teastman</groupId>
<artifactId>spring-data-hibernate-event</artifactId>
<version>1.0.0</version>
</dependency>
Then add the annotation #HibernateEventListener to any method where the first parameter is the entity you want to listen to, and the second parameter is the Hibernate event that you want to listen for. I've also added the static util function getPropertyIndex to more easily get access to the specific property you want to check, but you can also just look at the raw Hibernate event.
#HibernateEventListener
public void onUpdate(MyEntity entity, PreUpdateEvent event) {
int index = getPropertyIndex(event, "name");
if (event.getOldState()[index] != event.getState()[index]) {
// The name changed.
}
}
Just another solution using model:
public class Customer {
#JsonIgnore
private String name;
#JsonIgnore
#Transient
private String newName;
public void setName(String name){
this.name = name;
}
#JsonProperty("name")
public void setNewName(String newName){
this.newName = newName;
}
#JsonProperty
public void getName(String name){
return name;
}
public void getNewName(String newName){
return newName;
}
}
Alternative to consider. Might be reasonable if you need some special handling for this use-case then treat it separately. Do not allow direct property writing on the object. Create a separate endpoint with a custom controller to rename customer.
Example request:
POST /customers/{id}/identity
{
"name": "New name"
}
I had the same problem, but I wanted the old entity available in the save(S entity) method of a REST repository implementation (Spring Data REST).
What I did was to load the old entity using a 'clean' entity manager from which I create my QueryDSL query:
#Override
#Transactional
public <S extends Entity> S save(S entity) {
EntityManager cleanEM = entityManager.getEntityManagerFactory().createEntityManager();
JPAQuery<AccessControl> query = new JPAQuery<AccessControl>(cleanEM);
//here do what I need with the query which can retrieve all old values
cleanEM.close();
return super.save(entity);
}
The following worked for me. Without starting a new thread the hibernate session will provide the already updated version. Starting another thread is a way to have a separate JPA session.
#PreUpdate
Thread.start {
if (entity instanceof MyEntity) {
entity.previous = myEntityCrudRepository.findById(entity?.id).get()
}
}.join()
Just let me know if anybody would like more context.
Don't know if you're still after an answer, and this is probably a bit 'hacky', but you could form a query with an EntityManager and fetch the object that way ...
#Autowired
EntityManager em;
#HandleBeforeSave
public void handleBeforeSave(Customer obj) {
Query q = em.createQuery("SELECT a FROM CustomerRepository a WHERE a.id=" + obj.getId());
Customer ret = q.getSingleResult();
// ret should contain the 'before' object...
}

Categories

Resources