Hibernate – Collection-Size mit Lazy-Initialization

Es gibt einige klassische Probleme, die man mit JPA ständig zu lösen hat, für die aber scheinbar kaum jemand eine Sofort-Lösung parat hat.

Ein Klassiker dieser Gattung ist die Größe einer Collection zu erfragen, ohne diese vollständig zu initialisieren.

Nehmen wir das Standardbeispiel eines Blogs.

Ein Blog besteht aus Einträgen und Kommentaren. Die Entity für einen Blogeintrag kann beispielsweise so aussehen:

[code lang=”java” inline=”yes”]
@Entity
@Table(name=”POST”)
public class Post {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Column(name=”POST_ID”)
    Integer postId;

    @Column(name=”TITLE”)
    String title;

    @Column(name=”POST_DATE”)
    Date postDate;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name=”POST_ID”,referencedColumnName = “POST_ID”)
    private List<Comment> comments = new ArrayList<Comment>();

    public Integer getPostId() {
        return postId;
    }

    public void setPostId(Integer postId) {
        this.postId = postId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Date getPostDate() {
        return postDate;
    }

    public List<Comment> getComments(){
        return comments;
    }

    public void addComment(Comment comment){
        this.comments.add(comment);
    }

    @PrePersist
    public void setPostDate() {
        this.postDate = new Date();
    }

}
[/code]

Das Setup ist ein Standard-Hibernate/Spring/JPA Setup. Beispielsweise können wir mit Spring-Data für Posts folgendes Repository definieren, um auf die Einträge zuzugreifen.

[code lang=”java” inline=”yes”]

public interface PostRepository extends JpaRepository<Post, Integer> { }

[/code]

Um das Ganze zu testen definieren wir folgenden Test-Case

[code lang=”java” inline=”yes”]

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:META-INF/test-context.xml")
public class PostRepositoryTest {

    @Autowired
    PostRepository repository;

    @Test
    public void test() {
        Post post = new Post();
        Comment firstComment = new Comment("Kommentar Text1","Martin");
        post.addComment(firstComment);
        Comment secondComment = new Comment("Kommentar Text1","Martin");
        post.addComment(secondComment);
        post.setTitle("First Post");

        repository.save(post);

        Post dbpost = repository.findOne(post.getPostId());
        assertNotNull(dbpost);
        System.out.println(dbpost.getTitle());
    }

}

[/code]

Das generierte DDL-SQL von Hibernate sieht so aus

Hibernate: create table POST (POST_ID integer generated by default as identity, POST_DATE timestamp, TITLE varchar(255), primary key (POST_ID))
Hibernate: create table comments (id bigint generated by default as identity, author varchar(255), comment varchar(255), POST_ID integer, primary key (id))
Hibernate: alter table comments add constraint FKDC17DDF4D3BC1BD8 foreign key (POST_ID) references POST

Für die Inserts generiert Hibernate folgende Queries

Hibernate: insert into POST (POST_ID, POST_DATE, TITLE) values (null, ?, ?)
Hibernate: insert into comments (id, author, comment) values (null, ?, ?)
Hibernate: insert into comments (id, author, comment) values (null, ?, ?)
Hibernate: update comments set POST_ID=? where id=?
Hibernate: update comments set POST_ID=? where id=?

Soweit alles Standard. Jetzt wird es interessant. Wir möchten die Anzahl an Kommentaren ermitteln. Kein Problem oder?

Zunächst definieren wir eine neue Methode getCommentsCount() in der Post-Entity

 public Integer getCommentsCount(){
        return comments.size();
 }

Den Test ergänzen wir um einen einfachen Assert.

assertEquals(new Integer(2), post.getCommentsCount());

Das funktioniert nicht, aber nur deswegen, weil Hibernate die Collection nicht direkt beim Laden initialisiert (Lazy-Initialization). Das bedeutet, Kommentare werden nur geladen, wenn auch wirklich auf sie zugegriffen wird. Das Nachladen einer Lazy-Initialisierten Collection funktioniert aber leider nur im Kontext einer Transaktion und nicht mit einer Entity die bereits detached ist.
Der Test spiegelt das auch sofort wieder.

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.effectivetrainings.entities.Post.comments, could not initialize proxy - no Session

Welche Möglichkeiten haben wir jetzt?

Eager Initialization

Wir können Hibernate anweisen, die Collection Eager zu initialisieren, das bedeutet, immer wenn eine Post-Entity geladen wird, wird Hibernate auch gleich alle Kommentare laden.

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name="POST_ID",referencedColumnName = "POST_ID")
private List&lt;Comment&gt; comments = new ArrayList&lt;Comment&gt;();

Hibernate generiert uns folgendes SQL.

Hibernate: select post0_.POST_ID as POST1_0_0_, post0_.POST_DATE as POST2_0_0_, post0_.TITLE as TITLE0_0_ from POST post0_ where post0_.POST_ID=?
Hibernate: select comments0_.POST_ID as POST4_0_1_, comments0_.id as id1_1_, comments0_.id as id1_0_, comments0_.author as author1_0_, comments0_.comment as comment1_0_ from comments comments0_ where comments0_.POST_ID=?

Aus Performance-Gründen kann das nicht erwünscht sein, beispielsweise wenn wir nur eine Liste von Posts anzeigen möchten aber an den Kommentaren zunächst gar nicht interessiert sind. Jetzt haben wir ein Dilemma, aus dem es scheinbar keinen Ausweg gibt, oder? Dachte ich auch, bis ich auf ein spezielles Feature von Hibernate gestossen bin.

Extra-Lazy Initialization

Hibernate bietet ein sehr nettes Feature das sich Extra-Lazy Initialization nennt. Hier initialisiert Hibernate eine Collection teilweise, aber nur die Elemente, die tatsächlich gebraucht werden, was sogar einzelne Elemente in der Liste sein können.

Probieren wir es aus.

Wir ändern hierfür das Collection-Mapping für die Kommentare folgendermaßen.

 @OneToMany(cascade = CascadeType.ALL)
 @JoinColumn(name="POST_ID",referencedColumnName = "POST_ID")
 @LazyCollection(LazyCollectionOption.EXTRA)
 private List&lt;Comment&gt; comments = new ArrayList&lt;Comment&gt;();

Das allein macht unseren Test nicht grün. Zusätzlich fügen wir folgende private Methode in die Post-Klasse ein.

 @PostLoad
 private void initCommentCount(){
        comments.size();
 }

Jedesmal wenn ein Post geladen wird, fragen wir die Größe der Kommentar-Collection ab. Das SQL das jetzt von Hibernate generiert wird ist folgendes:

Hibernate: select post0_.POST_ID as POST1_1_0_, post0_.POST_DATE as POST2_1_0_, post0_.TITLE as TITLE1_0_ from POST post0_ where post0_.POST_ID=?
Hibernate: select count(id) from comments where POST_ID =?

Hibernate ist wirklich unglaublich klug und erkennt, dass nicht die Collection selbst sondern die die Größe benötigt wird. Deswegen wird auch kein Select auf alle Kommentare sondern nur ein select(count) ausgeführt, was aus Performance-Sicht optimal ist.Das lässt sich auch beweisen, in dem wir einfach mal auf ein Element in der Collection zugreifen.

 dbpost.getComments().get(0);

verursacht nach wie vor unsere

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.effectivetrainings.entities.Post.comments, could not initialize proxy - no Session

Fazit

Das ist ein sehr oft diskutiertes Problem, ich finde diese Lösung ziemlich elegant und beeindruckend. Ich hoffe, der/die Eine oder Andere kann damit was anfangen.

Der Code zu diesem Beispiel findet sich übrigens auf GitHub.

 

Effective Trainings & Consulting - Martin Dilger



Hat Ihnen dieser Blog-Eintrag gefallen? Ich stelle in diesem Blog Informationen über Tools, Frameworks und Werkzeuge zur Verfügung, die mich produktiver machen. Vielleicht kann ich auch Ihnen helfen, produktiver zu werden.


Ich unterstütze Sie als freier Mitarbeiter bei der Entwicklung von Software-Projekten, Agiler Arbeit sowie Schulungen / Fortbildungen.


Jeden Tag ein bisschen produktiver - ab heute