Hibernate LazyInitializationException | Fix කරන්නෙ කොහොමද? | Sinhala Software Engineering Guide

Hibernate LazyInitializationException | Fix කරන්නෙ කොහොමද? | Sinhala Software Engineering Guide

ආයුබෝවන්! කොහොමද ඉතින් ඔක්කොටම? ඔයාලා හැමෝම Java, Spring Boot, Hibernate එක්ක වැඩ කරනකොට, සමහර වෙලාවට නිතරම වගේ මුහුණ දෙන common error එකක් තමයි LazyInitializationException කියන්නේ. මේක දැක්කම සමහරවිට ඔලුව කකියනවා වගේ දැනෙනවා ඇති නේද? 🤣

අද අපි මේ LazyInitializationException කියන භූතයාව හොයාගෙන, ඌව පලවා හරින්නෙ කොහොමද කියලා කතා කරමු. මොකද මේක නොතේරුණොත්, අපේ applications වල performance එකටත්, memory usage එකටත් ලොකු බලපෑමක් වෙන්න පුළුවන්.

මේ blog post එක කියවලා ඉවර වෙනකොට, ඔයාලට මේ exception එක ගැන හොඳ අවබෝධයක් ලැබෙයි, ඒ වගේම ඒකට හොඳ විසඳුම් මොනවද කියලා දැනගන්නත් පුළුවන් වෙයි. එහෙනම් කෙලින්ම බහිමු වැඩේට!

මොකක්ද මේ LazyInitializationException? (What is LazyInitializationException?)

මේක තේරුම් ගන්න කලින් අපි Hibernate වල තියෙන fundamental concept දෙකක් ගැන දැනගෙන ඉන්න ඕනේ: Lazy Loading සහ Eager Loading.

Lazy Loading කියන්නේ මොකක්ද?

සරලවම කිව්වොත්, Lazy Loading කියන්නේ "අවශ්‍යම වෙලාවට" විතරක් data load කරන ක්‍රමයක්. හිතන්නකෝ ඔයාලගේ Database එකේ Book කියන entity එකයි, Author කියන entity එකයි තියෙනවා කියලා. එක Book එකකට Author කෙනෙක් ඉන්න පුළුවන් (Many-to-One), එහෙමත් නැත්නම් Author කෙනෙක්ට Books ගොඩක් ලියන්න පුළුවන් (One-to-Many).

දැන් ඔයාලා Book එකක් Database එකෙන් retrieve කරනවා කියලා හිතමු. ඒ වෙලාවේදී ඔයාලට ඒ Book එකේ Author ගේ විස්තර අවශ්‍ය නැති වෙන්න පුළුවන්. ඒ වගේ වෙලාවකදී, Hibernate ස්වභාවිකවම (by default) Author ගේ විස්තර load කරන්නේ නෑ. ඒ වෙනුවට Author object එකේ proxy එකක් තමයි return කරන්නේ. ඔයාලා book.getAuthor().getName() වගේ දෙයක් කරන්න ගියොත් විතරයි, Hibernate ගිහින් Database එකෙන් Author ගේ විස්තර load කරන්නේ. මේ ක්‍රමයට තමයි Lazy Loading කියන්නේ.

මේකෙන් තියෙන වාසි තමයි:

  • Performance වැඩිවීම: අවශ්‍ය දත්ත විතරක් load කරන නිසා queries ඉක්මන් වෙනවා.
  • Memory Usage අඩු වීම: අමතර data memory එකට load කරන්නේ නැති නිසා memory එක save වෙනවා.

Eager Loading කියන්නේ මොකක්ද?

Eager Loading කියන්නේ Lazy Loading එකේ අනිත් පැත්ත. මේකෙන් වෙන්නේ, main entity එක retrieve කරනකොටම ඒකට සම්බන්ධ අනිත් entities ටිකත් එකපාරටම load කරන එක. උදාහරණයක් විදියට, Book එකක් load කරනකොටම Author ගේ විස්තරත් එකටම load වෙනවා. මේක default behavior එක නෙවෙයි, අපි manually specify කරන්න ඕනේ. මේකට FetchType.EAGER කියලා annotation එකක් පාවිච්චි කරනවා.

හරි, දැන් ඔයාලට Lazy and Eager loading ගැන අවබෝධයක් තියෙන නිසා, LazyInitializationException එකට යමු.

LazyInitializationException එකක් එන්නේ ඇයි?

LazyInitializationException එකක් එන්නේ, ඔයාලා Lazy loaded relationship එකකට අදාල data, Hibernate session එක close වුනාට පස්සේ access කරන්න හදනකොට.

Hibernate session එක කියන්නේ Database එකත් එක්ක කරන transactions වලට තියෙන connection එක වගේ දෙයක්. සාමාන්‍යයෙන්, Database එකෙන් data ටික load කරගත්තට පස්සේ, ඒ session එක close වෙනවා (transaction එක commit වුනාට පස්සේ). එතකොට, ඔයාලා Lazy Loaded entity එකක proxy එකක් return කරලා තිබුණොත්, session එක close වුනාට පස්සේ ඒ proxy එක හරහා actual data ටික retrieve කරන්න Hibernate ට බැරි වෙනවා. මොකද Database එකත් එක්ක තිබ්බ සම්බන්ධය (session) නැති වෙලානේ. ඒ වෙලාවට තමයි මේ exception එක throw වෙන්නේ.

සරලව කිව්වොත්: "මම උඹට proxy එක දුන්නා, ඒක ඇත්ත data load කරන්න නම් මගේ session එක ඕනේ. ඒත් session එක දැන් නෑ! සොරි, ඒ නිසා මම මේ error එක දානවා." කියලා Hibernate කියනවා වගේ වැඩක්.

කෝ බලන්න මේක එන්නේ කොහොමද? (Let's see how this comes about?)

පොඩි code example එකකින් අපි බලමු මේ exception එක කොහොමද එන්නේ කියලා. අපි Post සහ Comment කියන entity දෙකක් ගමු.

// Post.java
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY) // Default fetch type for @OneToMany is LAZY
    private Set<Comment> comments = new HashSet<>();

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public Set<Comment> getComments() { return comments; }
    public void setComments(Set<Comment> comments) { this.comments = comments; }
}

// Comment.java
@Entity
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String content;

    @ManyToOne(fetch = FetchType.LAZY) // Default fetch type for @ManyToOne is EAGER. We explicitly make it LAZY here for demonstration
    @JoinColumn(name = "post_id")
    private Post post;

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
    public Post getPost() { return post; }
    public void setPost(Post post) { this.post = post; }
}

// PostService.java
@Service
public class PostService {

    @Autowired
    private PostRepository postRepository;

    @Transactional(readOnly = true)
    public Post getPostById(Long id) {
        return postRepository.findById(id).orElse(null);
    }

    // This method will cause LazyInitializationException
    public Post getPostWithCommentsOutsideSession(Long id) {
        Post post = postRepository.findById(id).orElse(null); // Session is active here
        if (post != null) {
            // Here, the session is likely closed after the transaction for findById completes,
            // especially if not using Open Session In View (OSIV) or a larger transaction scope.
            // Trying to access comments here will cause the exception.
            System.out.println("Post Title: " + post.getTitle());
            // This line will throw LazyInitializationException if session is closed:
            System.out.println("Number of comments: " + post.getComments().size()); 
        }
        return post;
    }
}

උඩ තියෙන getPostWithCommentsOutsideSession method එක දිහා බලන්න. postRepository.findById(id) call එකෙන් පස්සේ (@Transactional annotation එක getPostById වගේ method එකක තිබ්බොත්), transaction එක ඉවර වෙලා session එක close වෙනවා. ඊට පස්සේ අපි post.getComments().size() කියලා access කරන්න හදනකොට, comments කියන collection එක Lazy Loaded නිසා, Hibernate එකට data ටික load කරන්න session එකක් නැති වෙලා LazyInitializationException එක throw කරනවා. හරිම සරලයි!

විසඳුම් ටිකක් (A Few Solutions)

හොඳයි, දැන් අපි දන්නවා මේක එන්නේ ඇයි කියලා. එහෙනම් මේක fix කරන්න පුළුවන් ප්‍රායෝගික ක්‍රම කිහිපයක් බලමු.

1. Eager Loading (FetchType.EAGER)

මේක තමයි ලේසිම විසඳුම. ඔයාලට යම් relationship එකක data හැම වෙලේම main entity එකත් එක්කම load වෙන්න ඕනේ නම්, ඒ association එකට FetchType.EAGER කියලා specify කරන්න පුළුවන්.

// Post.java
@Entity
public class Post {
    // ... other fields ...

    @OneToMany(mappedBy = "post", fetch = FetchType.EAGER) // Changed to EAGER
    private Set<Comment> comments = new HashSet<>();

    // ... getters and setters ...
}

දැන් Post object එක load කරනකොටම ඒකේ comments ටිකත් load වෙනවා. ඒ නිසා session එක close වුනාට පස්සේ comments access කරන්න ගියත් ප්‍රශ්නයක් නෑ.

වාසි: Implement කරන්න හරිම ලේසියි.

අවාසි: හැම වෙලේම මේක හොඳ විසඳුමක් නෙවෙයි. පොඩි data set එකකට හොඳ වුණාට, relation එකක ගොඩක් data තියෙනවා නම්, අනවශ්‍ය විදියට memory usage එක වැඩි වෙනවා, performance එකත් බහිනවා. N+1 query problem එක වගේ ප්‍රශ්නත් එන්න පුළුවන්.

2. Open Session In View (OSIV) Pattern

Spring Boot applications වලදී මේක default විදියට enable වෙලා තියෙන්නේ. මේකෙන් වෙන්නේ HTTP request එකක් එන වෙලාවේ ඉඳන් response එක යනකම් Hibernate session එක open කරලා තියාගන්න එක. එතකොට ඕනම වෙලාවක Lazy Loaded data access කරන්න පුළුවන්.

application.properties / application.yml:


spring.jpa.open-in-view=true # Default is true in Spring Boot 2+, false in Spring Boot 3+

වාසි: LazyInitializationException එක හරිම ලේසියෙන් විසඳෙනවා. Code එක simplify වෙනවා.

අවාසි: මේක ගොඩක් වෙලාවට Production environments වලට එච්චර හොඳ නෑ. මොකද Session එක ගොඩක් වෙලා open කරලා තියාගෙන ඉන්න එකෙන් Database connections ඉක්මනට release වෙන්නේ නෑ. ඒකෙන් application එකේ Scalability එකට බලපෑම් ඇති වෙන්න පුළුවන්. ඒ වගේම transaction boundaries manage කරන්නත් අපහසු වෙනවා.

3. JPQL/Criteria API Fetch Joins

මේක තමයි ගොඩක් වෙලාවට හොඳම සහ flexibleම විසඳුම. අපිට අවශ්‍ය data ටික Database එකෙන් retrieve කරන query එක ඇතුලෙම JOIN FETCH කියලා mention කරන්න පුළුවන්. එතකොට Hibernate session එක ඇතුළතදී, අවශ්‍ය relations ටිකත් එක්කම data load කරලා දෙනවා.

PostRepository.java:


public interface PostRepository extends JpaRepository<Post, Long> {

    @Query("SELECT p FROM Post p JOIN FETCH p.comments WHERE p.id = :id")
    Optional<Post> findByIdWithComments(@Param("id") Long id);
}

PostService.java:


@Service
public class PostService {

    @Autowired
    private PostRepository postRepository;

    @Transactional(readOnly = true)
    public Post getPostWithComments(Long id) {
        return postRepository.findByIdWithComments(id).orElse(null);
    }
}

දැන් getPostWithComments call කරලා එන Post object එකේ comments collection එක load වෙලා තියෙන්නේ. ඒ නිසා session එක close වුනාට පස්සෙත් කිසිම ප්‍රශ්නයක් නැතුව access කරන්න පුළුවන්.

වාසි: අවශ්‍ය වෙලාවට අවශ්‍ය data විතරක් load කරගන්න පුළුවන් (selective eager loading). N+1 problem එක avoid කරනවා. Scalable solution එකක්.

අවාසි: Queries ටිකක් සංකීර්ණ වෙන්න පුළුවන්.

4. DTOs (Data Transfer Objects)

DTOs කියන්නේ Database entities වලින් view layer එකට හෝ client එකට අවශ්‍ය data ටික විතරක් අරන් යන්න පාවිච්චි කරන simple Java objects. මේවා Database entities නෙවෙයි. Application layers අතර data transfer කරන්න තමයි මේවා පාවිච්චි කරන්නේ.

අපි PostWithCommentsDto කියලා DTO එකක් හදමු.


public class PostWithCommentsDto {
    private Long id;
    private String title;
    private List<CommentDto> comments;

    // Constructor to map from Post entity
    public PostWithCommentsDto(Post post) {
        this.id = post.getId();
        this.title = post.getTitle();
        this.comments = post.getComments().stream()
                                .map(CommentDto::new)
                                .collect(Collectors.toList());
    }

    // Getters
    public Long getId() { return id; }
    public String getTitle() { return title; }
    public List<CommentDto> getComments() { return comments; }
}

public class CommentDto {
    private Long id;
    private String content;

    public CommentDto(Comment comment) {
        this.id = comment.getId();
        this.content = comment.getContent();
    }

    // Getters
    public Long getId() { return id; }
    public String getContent() { return content; }
}

දැන් service layer එකේදී, transaction එක active වෙලා තියෙන වෙලාවෙදිම entity එකෙන් DTO එකට map කරන්න පුළුවන්.


@Service
public class PostService {

    @Autowired
    private PostRepository postRepository;

    @Transactional(readOnly = true)
    public PostWithCommentsDto getPostDtoWithComments(Long id) {
        Post post = postRepository.findById(id).orElse(null);
        if (post != null) {
            // Accessing comments within the @Transactional scope, they will be initialized.
            // Then map to DTO.
            return new PostWithCommentsDto(post);
        }
        return null;
    }
}

වාසි: API එක cleaner වෙනවා, client එකට අවශ්‍ය data විතරක් return කරන නිසා network payload එක අඩු වෙනවා. LazyInitializationException එක solve වෙනවා මොකද data map කරන්නේ session එක active වෙලා තියෙන වෙලාවේ නිසා.

අවාසි: DTOs සහ mapping logic maintain කරන්න වෙනවා. Project එක ලොකු වෙනකොට මේක ටිකක් අමාරු වෙන්න පුළුවන් (MapStruct වගේ libraries පාවිච්චි කරන්න පුළුවන්).

5. Initializing Lazy Collections within the Session

අවශ්‍ය නම්, Hibernate session එක active වෙලා තියෙන වෙලාවේදී, Lazy collection එකක් manually initialize කරන්න පුළුවන්. මේක කරන්න පුළුවන් ක්‍රම දෙකක් තියෙනවා:

  1. Hibernate.initialize() method එක පාවිච්චි කිරීම.
  2. සරලවම collection එක access කිරීම (e.g., post.getComments().size()).

@Service
public class PostService {

    @Autowired
    private PostRepository postRepository;

    @Transactional(readOnly = true)
    public Post getPostAndInitializeComments(Long id) {
        Post post = postRepository.findById(id).orElse(null);
        if (post != null) {
            // Option 1: Explicitly initialize the collection
            Hibernate.initialize(post.getComments());
            
            // Option 2: Simply access the collection (this also initializes it)
            // post.getComments().size(); 
            System.out.println("Comments initialized!");
        }
        return post;
    }
}

මේ ක්‍රමය සාමාන්‍යයෙන් භාවිතා කරන්නේ ඉතාම specific අවස්ථා වලදී. මොකද මේකෙන් වෙන්නේ session එක ඇතුලෙදී අවශ්‍ය data load කරගන්න එක විතරයි. එතකොට ඒ data ටික session එකෙන් එළියට ගියාට පස්සේත් පාවිච්චි කරන්න පුළුවන්.

වාසි: අවශ්‍ය collection එක විතරක් initialize කරන්න පුළුවන්.

අවාසි: මේකත් JPQL fetch join වලට වඩා අඩු flexible. ගොඩක් relations තියෙනවා නම් මේක practical නෑ.

හොඳම විසඳුම මොකක්ද? (What's the best solution?)

ඉතින්, LazyInitializationException එකට "හොඳම" විසඳුමක් කියලා එකක් නෑ. ඒක සම්පූර්ණයෙන්ම රඳා පවතින්නේ ඔයාලගේ application එකේ requirement එක, performance needs එක සහ architecture design එක මත.

  • සෑම විටම අවශ්‍ය නම්: ඒ relationship එකට FetchType.EAGER දෙන්න පුළුවන්. හැබැයි මේකෙන් N+1 query problem එක සහ performance issue එන්න පුළුවන් නිසා පරිස්සම් වෙන්න.
  • විශේෂිත queries වලට අවශ්‍ය නම්: JOIN FETCH එක්ක JPQL/Criteria API පාවිච්චි කරන්න. මේක තමයි ගොඩක් වෙලාවට වඩාත්ම recommended solution එක.
  • UI/API එකට අවශ්‍ය data ටික විතරක්: DTOs පාවිච්චි කරන්න. ඒකෙන් API එක clean වෙනවා වගේම performance එකත් වැඩි වෙනවා.
  • ඉතාම specific scenarios වලට: Hibernate.initialize() වගේ දේවල් පාවිච්චි කරන්න.

Open Session In View කියන concept එකෙන් LazyInitializationException එක fix වුනත්, production applications වලදී ඒකෙන් ඇති වෙන්න පුළුවන් side effects ගැන අවබෝධයක් තියාගෙන පාවිච්චි කරන එක හොඳයි. ගොඩක් වෙලාවට මේක disable කරලා, අවශ්‍ය තැන් වලදී JPQL/Criteria API JOIN FETCH හෝ DTOs පාවිච්චි කරන එක තමයි recommended practice එක.

අවසාන වශයෙන් (In Conclusion)

LazyInitializationException එක කියන්නේ Hibernate එක්ක වැඩ කරනකොට නිතරම වගේ හම්බවෙන exception එකක්. ඒත් දැන් ඔයාලා දන්නවා ඒක එන්නේ ඇයි කියලා, ඒ වගේම ඒක fix කරන්න පුළුවන් ක්‍රම කිහිපයකුත් දන්නවා. වැදගත්ම දේ තමයි, මේ හැම solution එකකම තියෙන වාසි සහ අවාසි තේරුම් අරගෙන, ඔයාලගේ project එකට වඩාත්ම ගැලපෙන solution එක තෝරාගන්න එක.

හරි, අදට එහෙනම් මේක ඇති කියලා හිතනවා! මේ post එක ඔයාලට වැදගත් වෙන්න ඇති කියලා හිතනවා. මොනවා හරි ප්‍රශ්න තියෙනවා නම්, හරි වෙන මොනවා හරි ගැන කතා කරන්න ඕනේ නම්, comment section එකේ කියන්න. ඒ වගේම මේක තවත් යාළුවන්ටත් දැනගන්න share කරන්න අමතක කරන්න එපා.

අපි ආයෙත් මේ වගේම වැදගත් tech topic එකකින් හම්බවෙමු! පරිස්සමෙන් ඉන්න, coding දිගටම කරගෙන යන්න! ආයෙත් හම්බවෙමු! 👋