Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>So I finally figured out a working solution:</p> <p>Ditch the <strong>EntityManager</strong> for updating <em>DTYPE</em>. This is mainly because <strong>Query.executeUpdate()</strong> must run within a transaction. You can try running it within the existing transaction, but that is probably tied to the same persistence context of the Entity you're modifying. What this means is that after you update <em>DTYPE</em> you have to find a way to <strong>evict()</strong> the Entity. The easy way is to call <strong>entityManager.clear()</strong> but this results in all sorts of side effects (read about it in the JPA spec). The better solution is to get the underlying delegate (in my case, a Hibernate <strong>Session</strong>) and call <strong>Session.evict(user)</strong>. This will probably work on simple domain graphs, but mine were very complex. I was never able to get <strong>@Cascade(CascadeType.EVICT)</strong> to work correctly with my existing JPA annotations, like <strong>@OneToOne(cascade = CascadeType.ALL)</strong>. I also tried manually passing my domain graph a <strong>Session</strong> and having each parent Entity evict its children. This also didn't work for unknown reasons. </p> <p>I was left in a situation where only <strong>entityManager.clear()</strong> would work, but I couldn't accept the side effects. I then tried creating a separate Persistence Unit specifically for Entity conversions. I figured I could localize the <strong>clear()</strong> operation to only that PC in charge of conversions. I set up a new PC, a new corresponding <strong>EntityManagerFactory</strong>, a new Transaction Manager for it, and manually injecting this transaction manager into the Repository for manual wrapping of the <strong>executeUpdate()</strong> in a transaction corresponding to the proper PC. Here I have to say that I don't know enough about Spring/JPA container managed transactions, because it ended up being a nightmare trying to get the local/manual transaction for <strong>executeUpdate()</strong> to play nicely with the container managed transaction getting pulled in from the Service layer. </p> <p>At this point I threw out everything and created this class:</p> <pre><code>@Transactional(propagation = Propagation.NOT_SUPPORTED) public class JdbcUserConversionRepository implements UserConversionRepository { @Resource private UserService userService; private JdbcTemplate jdbcTemplate; @Override @SuppressWarnings("unchecked") public User convertUserType(final User user, final Class targetClass) { // Update the DTYPE jdbcTemplate.update("update user set user.DTYPE = ? where user.id = ?", new Object[] { targetClass.getSimpleName(), user.getId() }); // Before we try to load our converted User back into the Persistence // Context, we need to remove them from the PC so the EntityManager // doesn't try to load the cached one in the PC. Keep in mind that all // of the child Entities of this User will remain in the PC. This would // normally cause a problem when the PC is flushed, throwing a detached // entity exception. In this specific case, we return a new User // reference which replaces the old one. This means if we just evict the // User, then remove all references to it, the PC will not be able to // drill down into the children and try to persist them. userService.evictUser(user); // Reload the converted User into the Persistence Context return userService.getUserById(user.getId()); } public void setDataSource(final DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } } </code></pre> <p>There are two important parts of this method which I believe make it work:</p> <ol> <li>I've marked it with <strong>@Transactional(propagation = Propagation.NOT_SUPPORTED)</strong> which should suspend the container managed transaction coming in from the Service layer and allow the conversion to take place external of the PC.</li> <li>Before trying to reload the converted Entity back into the PC, I evict the old copy currently stored in the PC with <strong>userService.evictUser(user);</strong>. The code for this is simply getting a <strong>Session</strong> instance and calling <strong>evict(user)</strong>. See the comments in code for more details, but basically if we don't do this any calls to <strong>getUser</strong> will try to return the cached Entity still in the PC, except that it will throw an error about the type being different.</li> </ol> <p>Though my initial tests have gone well, this solution may still have some problems. I will keep this updated as they are uncovered.</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload