Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

@Transactional(readOnly = true) leads to LazyInitializationException

I have a many-to-many relation with an additional column in the link table. I've configured it in a way that the owning side fetches children eager (so I don't get LazyInitializationException) and in the opposite direction it is lazy. This works.

I now wanted to fine-tune the transactions (before there was just @Transactional on class level of DAO and Service classes. I set method getById to readOnly = true:

@Transactional(readOnly  = true)
public Compound getById(Long id) {
    return compoundDAO.getById(id);
}

After this change I get a LazyInitializationException in following snippet:

Compound compound = compoundService.getById(6L);        
Structure structure = compound.getComposition().get(0).getStructure();
System.out.println("StructureId: "+ structure.getId()); // LazyInitializationException

If I remove (readOnly = true) this works! Can anyone explain this behavior? I use Spring + Hibernate. Kind of confusing as I don't see any reason why this should affect which data is loaded?


EDIT:

Snippets of relationship definitions. This is a many-to-many with a column in the link table.

Owning side (eg Compound contains Structures):

@OneToMany(fetch = FetchType.EAGER, mappedBy = "pk.compound",
    cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("pk.structure.id ASC")
private List<CompoundComposition> composition = new ArrayList<>();

Belongs to side:

@OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.structure",
cascade = CascadeType.ALL)
@OrderBy("pk.compound.id ASC")
private List<CompoundComposition> occurence;

Many-To-One in @Embeddable ID class

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
public Compound getCompound() {
    return compound;
}

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
public Structure getStructure() {
    return structure;
}

EDIT 2:

Stack Trace

org.hibernate.LazyInitializationException: could not initialize proxy - no Session
    at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:165) ~[hibernate-core-4.1.7.Final.jar:4.1.7.Final]
    at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:272) ~[hibernate-core-4.1.7.Final.jar:4.1.7.Final]
    at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:185) ~[hibernate-core-4.1.7.Final.jar:4.1.7.Final]
    at org.bitbucket.myName.myApp.entity.Structure_$$_javassist_0.getId(Structure_$$_javassist_0.java) ~[classes/:na]
    at org.bitbucket.myName.myApp.App.main(App.java:31) ~[classes/:na]

EDIT 3:

Also see my comment:

Log is very different with readOnly and it is missing the part were the relations are loaded, eg. some selects are missing in the log.

EDIT 4:

So I tired with a basic DriverManagerDataSource and no Connection pool. The issue is exactly the same. For me looks like an issue in Hibernate.

like image 349
beginner_ Avatar asked Oct 09 '12 12:10

beginner_


1 Answers

This is just wow. I'm starting to understand why some people hate ORMs...Just feels like I'm constantly having to spend hours to solve a weird issue and the solution is a very specific set of annotations + some code to work around the limitations of said annotations.

First to why this happens (why meaning with which annotations, but not in terms of making logical sense, which is the actual problem here as using common-sense is useless. Only trial and error helps). In the owning side, in @OneToMany I have orphanRemoval = true (which I have found out is required for consistency. one would think database constraints should handle that...just one of the many things that can drive you crazy.). It seems that if the transaction is not read-only, then this setting leads to some data being fetched even so its lazy, namely here:

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
public Structure getStructure() {
    return structure;
}

In a read-only transaction, this fetching does not happen. I would guess because if you can't change anything you will also not have to remove orphans and hence any data that the logic behind this setting requires is not needed in a read-only tx.

So the obvious solution would be in above relation to change to FetchType.EAGER. Wrong! If you do that you will not be able to update the owning side (Compound) using session.merge. This will lead to a StackOverFlowError.

The real solution was actually already mentioned. Just leave the config as is but explicitly load the desired relations in the Service layer:

@Transactional(readOnly = true)
@Override    
public Compound getById(Long id) {

    Compound  compound = compoundDAO.getById(id);
    for (CompoundComposition composition : compound.getComposition()){
        Hibernate.initialize(composition.getStructure());
    }        
    return compound;
}

I admit I'm tending to fall in the premature optimization trap. This doesn't look very efficient and also seems to break how SQL works in the first place. But then I'm in the lucky position that in most cases CompoundComposition will contain only 1 or 2 elements.

like image 188
beginner_ Avatar answered Sep 19 '22 14:09

beginner_