@Transactional(readOnly = true) leads to LazyInitializationException

狂风中的少年 提交于 2021-02-07 06:29:05

问题


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.


回答1:


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.




回答2:


Perhaps you could put

value.getComposition().get(i).getStructure();

in the body of the getById() method, so that the lazy loading happens within the transaction. I realize in this case you'd have to loop over i which might be inconvenient.




回答3:


Two things :-

Lazy fetch works on Collections Interface. Since ...

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

... this is not a collection interface (like List<Structure> would have been), it will be fetched in Eager fetch mode.

Make service method as transactional. It seems that after fetching from the dao layer, your structure is detached with NEVER flush mode. This is the underlying ORM issue I guess.



来源:https://stackoverflow.com/questions/12800531/transactionalreadonly-true-leads-to-lazyinitializationexception

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!