LazyInitialization Exception with Spring and Hibernate
I think i'm missing something fundamental about how Hibernate works, specifically with lazy loading. My problem is debugging, as I'm not sure if this is a Hibernate problem or a Spring problem in disguise. I thought I would ask here before doing some major refactoring.
I have two Entities. One holds a collection of the other in a OneToMany relationship. For my web page I wish to grab all of the first entity, and subsequently grab the set of associated entities for each and display them.
I believe my problem is this: I use a JpaTemplate to find all entities. This works fine, however because of Lazy loading I do not get the associated set of related entities. In my view (jsp) I want access to this set, but of course it is null because it is being lazy loaded. Now, i'm getting a LazyInitialization exception stating that the transaction has ended. To me this makes sense, of course the transaction should be over by now. The thing is, how can the assoicated set ever be lazy loaded if the transaction is over?
Entity Classes:
@Entity
public class LearningEntry implements Serializable {
private Long id;
String imagePath = "";
Set<Sample> samples = null;
//------------------------------
// Constructors
//------------------------------
public LearningEntry(){
imagePath = "";
samples = new HashSet<Sample>();
}
//------------------------------
// Instance Methods
//------------------------------
public void addSample(Sample s){
samples.add(s);
}
public void removeSample(Sample s){
samples.remove(s);
}
//------------------------------
// Setters and Getters
//------------------------------
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
//@Column(name = "wisi_LE_IMAGEPATH", length = 100, nullable = false)
public String getImagePath() {
return imagePath;
}
public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}
// TODO - ONly works with fetch type EAGER
//@OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
public Set<Sample> getSamples() {
return samples;
}
public void setSamples(Set<Sample> samples) {
this.samples = samples;
}
}
Sample Entity
@Entity
public class Sample implements Serializable {
private Long id;
Date creationDate;
String audioFileLocation;
Integer votes;
String description;
public Sample(){
creationDate = new Date();
audioFileLocation = "";
votes = 0;
description = "";
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAudioFileLocation() {
return audioFileLocation;
}
public void setAudioFileLocation(String audioFileLocation) {
this.audioFileLocation = audioFileLocation;
}
@Temporal(TemporalType.DATE)
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getVotes() {
return votes;
}
public void setVotes(Integer votes) {
this.votes = votes;
}
}
DAO Classes: LearningEntryDAO
@Transactional
public class JpaLearningEntryDAO implements LearningEntryDAO{
private JpaTemplate jpaTemplate;
public JpaLearningEntryDAO(){
}
public void setJpaTemplate(JpaTemplate jpaTemplate){
this.jpaTemplate = jpaTemplate;
}
@Override
//@Transactional
public void delete(Long leId) {
LearningEntry dp = jpaTemplate.find(LearningEntry.class, leId);
jpaTemplate.remove(dp);
}
@Override
@SuppressWarnings("unchecked")
//@Transactional
public List<LearningEntry> findAll() {
return jpaTemplate.find("from LearningEntry");
}
@Override
//@Transactional
public LearningEntry findById(Long leId) {
return jpaTemplate.find(LearningEntry.class, leId);
}
@Override
//@Transactional
public LearningEntry store(LearningEntry dp) {
return jpaTemplate.merge(dp);
}
@Override
@SuppressWarnings("unchecked")
//@Transactional
public void deleteAll(){
throw new RuntimeException("deleteAll not implemented");
}
}
Sample DAO
@Transactional
public class JpaSampleDAO implements SampleDAO{
private JpaTemplate jpaTemplate;
public JpaSampleDAO(){}
public void setJpaTemplate(JpaTemplate jpaTemplate){
this.jpaTemplate = jpaTemplate;
}
@Override
//@Transactional
public void delete(Long sampleId) {
Sample dp = jpaTemplate.find(Sample.class, sampleId);
jpaTemplate.remove(dp);
}
@Override
@SuppressWarnings("unchecked")
public List<Sample> findAll() {
return jpaTemplate.find("from Sample");
}
@Override
public Sample findById(Long sampleId) {
return jpaTemplate.find(Sample.class, sampleId);
}
@Override
public Sample store(Sample dp) {
return jpaTemplate.merge(dp);
}
@Override
@SuppressWarnings("unchecked")
public void deleteAll(){
throw new RuntimeException("deleteAll not implemented");
}
}
Controller
@RequestMapping(value = "/index.htm", method = RequestMethod.GET)
public ModelAndView sayHello(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
Map<String, Object> model = new HashMap<String, Object>();
List<LearningEntry> le = learningEntryService.getLearningEntries();
model.put("learningEntries", le);
return new ModelAndView("main", model);
}
View
<section id="content" class="body">
<ol id="posts-list" class="hfeed">
<c:forEach items="${learningEntries}" 开发者_StackOverflow中文版var="learningEntry">
<li>
<table class="wisiEntry">
<tr>
<td class="pictureCell">
<img class="wisiEntry-pic" src="${learningEntry.imagePath}" />
</td>
<td class="previousNextCell"
<div class="wisiEntry-nextSampleButton">Next</div>
<div class="wisiEntry-previousSampleButton">Previous</div>
<br />
<div class="wisiEntry-addTagButton">Tag</div>
<div class="wisiEntry-addCommentButton">Comment</div>
<br />
<div class="wisiEntry-uploadButton">Upload</div>
</td>
<td>
<!-- ERROR HAPPENS HERE. Samples should not be null -->
<c:forEach items="${learningEntry.samples}" var="sample" varStatus = "status">
<table class="sampleEntry" ${status.first ? '' : 'style = "display:none"'}>
<tr>
<td class="sampleCell">
<p class="description">
${sample.description}
</p>
<audio src="${sample.audioFileLocation}" controls>
Your browser does not support the <code>audio</code> element.
</audio>
</td>
<td class="voteCell">
<img class="upVote" src="/images/upArrow.jpeg" />
<span class="voteNumber">${sample.votes}</span>
<img class="downVote" src="/images/downArrow.jpeg" />
</td>
</tr>
</table>
</c:forEach>
</td>
</tr>
</table>
</li>
</c:forEach>
</ol><!-- /#posts-list -->
</section><!-- /#content -->
I hope you are using findAll()
method down the call. You can load all the associated samples by modifying your method like below.
public List<LearningEntry> findAll() {
List<LearningEntry> entries = jpaTemplate.find("from LearningEntry");
for(LearningEntry entry : entries){
entry.getSamples().size();
}
return entries;
}
Or, as you already know, you can also achieve this by changing fetch
to FetchType.EAGER
. But this might not suit you in all cases. Therefore, former way is better.
Or you might like to do no change anywhere, and define another method to get all the samples based on LearningEntry
, this way you will be able to fire up an AJAX call on some event. But that might not suit here in this case.
Thanks to Vinegar for providing a working answer (upvoted).
I decided to add this answer that has also worked for me. I took this approach because I may want to make separate ajax calls in the future. In other words, I can ask for the LearningEntry in one transaction, than ask for its samples some time down the road.
@Transactional
public Set<Sample> getSamplesForLearningEntry(LearningEntry le) {
// Reload the le from the database so it is not transient:
LearningEntry le = leDAO.store(le);
le.getSamples.size();
return le.getSamples();
}
Most frameworks offer the 'open session in view' pattern. See https://www.hibernate.org/43.html:
The solution, in two-tiered systems, with the action execution, data access through the Session, and the rendering of the view all in the same virtual machine, is to keep the Session open until the view has been rendered.
For data that is read often and hardly ever updated, query caching can help too. This reduces the load on the database, but increases memory usage. Hibernate can be configured to do this for you.
精彩评论