Spring Boot Hibernate JPA Query Optimization සිංහලෙන් | TechBadda SC Guide

Spring Boot Hibernate JPA Query Optimization සිංහලෙන් | TechBadda SC Guide

ඉතින් කොහොමද යාලුවනේ? මේක තමයි අපේ TechBadda SC Guide එකෙන් ඔයාලට අරන් එන අලුත්ම ලිපිය. අද අපි කතා කරන්න යන්නේ software development වලදි අතිශය වැදගත් වෙන, ඒ වගේම ගොඩක් දෙනෙක්ට ප්‍රශ්න ඇතිවෙන මාතෘකාවක් ගැන – ඒ තමයි Spring Boot එක්ක Hibernate ORM භාවිත කරලා JPA Queries Optimize කරන හැටි!

ඔයාලා දන්නවා ඇතිනේ, අදාල data set එක පොඩි වෙද්දි නම් ඕනෑම application එකක් ලේසියෙන් run කරන්න පුළුවන්. ඒත් දත්ත ප්‍රමාණය වැඩි වෙද්දි, විශේෂයෙන්ම real-world applications වල, queries හරි විදියට optimize කරලා නැත්නම් performance issues එන්න පුළුවන්. Slow loading times, server overload, user frustration – මේ වගේ දේවල් ඕනෑම developer කෙනෙක්ට හොඳටම headache එකක්. ඉතින්, අද අපි මේක ගැඹුරින්ම dissect කරලා බලමු, ඔයාලගේ applications lightning fast කරන්න පුළුවන් techniques ටිකක් එක්කම.


Hibernate ORM එකේ මූලිකාංග: SQL ලෝකෙට bridge එකක්!

පළමුවෙන්ම, අපි පොඩ්ඩක් Hibernate ORM (Object-Relational Mapping) ගැන කතා කරමු. ඔයාලා දන්නවනේ, අපි Java වලින් application එකක් හදනකොට, ඒකේ data save කරන්නේ database එකක. ඒ database එක එක්ක කතා කරන්න අපි SQL queries ලියන්න ඕනේ. ඒත්, හැමදේටම SQL queries ලියන එක හරි වෙහෙසකරයි, වගේම error-prone වෙන්නත් පුළුවන්. මෙන්න මේකට තමයි ORM Tools වැදගත් වෙන්නේ.

Hibernate කියන්නේ Java applications වලට database එකත් එක්ක interact කරන්න උදව් කරන බලවත් ORM framework එකක්. මේකෙන් කරන්නේ, අපේ Java objects (ඒ කියන්නේ entities) database tables වලට map කරන එක. එතකොට අපිට පුළුවන් Java code වලින්ම database operations කරන්න – data save කරන්න, retrieve කරන්න, update කරන්න, delete කරන්න. SQL ලියන්න ඕනේ නැහැ, Hibernate එකට ඒක බලාගන්නවා. නියමයි නේද?

Spring Boot එකත් එක්ක Spring Data JPA භාවිත කරනකොට, Hibernate කියන්නේ default ORM provider එක. ඒ නිසා අපිට හුඟක් දේවල් configure කරන්න ඕනේ නැහැ, Spring Boot එකෙන් ඒක automagically කරලා දෙනවා. Hibernate වලට තියෙනවා key concepts කීපයක්:

  • Session / EntityManager: මේක තමයි Hibernate එක්ක database එක connect කරන්න පාවිච්චි කරන ප්‍රධාන interface එක. සියලුම database operations මේක හරහා තමයි සිද්ධ වෙන්නේ. JPA වලදි අපි මේකට EntityManager කියනවා.
  • Persistence Context: මේක කියන්නේ Hibernate එක entity objects ටික තාවකාලිකව save කරගෙන ඉන්න තැන. අපි database එකට data save කරන්න කලින්, ඒ data ටික මේ context එකේ තියාගන්නවා. මේක තමයි First-Level Cache එක විදියටත් ක්‍රියා කරන්නේ.
  • Entities: අපේ Java objects තමයි database tables වලට map වෙන්නේ. මේවාට @Entity annotation එක දාලා identify කරනවා.

සරල උදාහරණයක් විදියට, Student කෙනෙක්ගේ විස්තර save කරන Entity එකක් මෙහෙම ලියන්න පුළුවන්:


import javax.persistence.*;

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    private String email;

    // Getters and Setters
    // ...
}

මේ වගේ entity එකක් හදලා, Spring Data JPA repository එකක් හරහා අපිට ලේසියෙන්ම data save කරන්න, retrieve කරන්න පුළුවන්.


JPA Queries සහ ඒවායේ වර්ග: Data ගන්න පුළුවන් විවිධ ක්‍රම!

දැන් අපි බලමු data retrieve කරගන්න පුළුවන් විවිධ ක්‍රම මොනවද කියලා. JPA වල queries ලියන්න ක්‍රම කීපයක් තියෙනවා:

1. JPQL (Java Persistence Query Language)

මේක SQL වගේම syntax එකක් තියෙන, ඒත් database table names වෙනුවට entity names සහ field names පාවිච්චි කරන query language එකක්. මේක Object-Oriented විදියට තමයි වැඩ කරන්නේ. මේකේ වාසිය තමයි, අපිට database vendor-specific SQL ලියන්න ඕනේ නැහැ, JPQL එක ඕනෑම database එකක වැඩ කරනවා.


// Spring Data JPA Repository එකක JPQL Query එකක්
public interface StudentRepository extends JpaRepository<Student, Long> {

    @Query("SELECT s FROM Student s WHERE s.email = ?1")
    Student findByEmailJPQL(String email);

    @Query("SELECT s FROM Student s JOIN FETCH s.courses c WHERE s.id = ?1")
    Optional<Student> findStudentWithCoursesJPQL(Long studentId);
}

2. Native SQL Queries

සමහර වෙලාවට, අපිට Hibernate / JPA එකෙන් generate කරන SQL වලට වඩා complex queries ලියන්න වෙනවා. එහෙමත් නැත්නම් database specific functions පාවිච්චි කරන්න ඕනේ වෙනවා. මෙවැනි අවස්ථාවලදී අපිට Native SQL queries පාවිච්චි කරන්න පුළුවන්.


// Native SQL Query එකක්
public interface StudentRepository extends JpaRepository<Student, Long> {

    @Query(value = "SELECT * FROM students s WHERE s.first_name LIKE %?1%", nativeQuery = true)
    List<Student> findStudentsByFirstNameNative(String firstName);
}

3. Criteria API

මේක Java code වලින්ම queries ලියන්න පුළුවන් programatic API එකක්. dynamic queries හදනකොට, ඒ කියන්නේ run-time එකේදී query එකේ conditions වෙනස් වෙන වෙලාවට මේක ගොඩක් ප්‍රයෝජනවත්. මේක SQL injection වලටත් වඩා ආරක්ෂිතයි.


import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.util.List;

// Service layer එකක Criteria API Query එකක්
public class StudentService {

    private final EntityManager entityManager;

    public StudentService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public List<Student> findStudentsByLastName(String lastName) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Student> cq = cb.createQuery(Student.class);
        Root<Student> student = cq.from(Student.class);
        cq.where(cb.equal(student.get("lastName"), lastName));
        return entityManager.createQuery(cq).getResultList();
    }
}

මේවා තමයි ප්‍රධාන වශයෙන්ම queries ලියන්න පුළුවන් ක්‍රම. දැන් අපි බලමු මේ queries optimize කරන්නේ කොහොමද කියලා.


JPA Queries Optimize කරමු! Performance වැඩි කරන රහස්!

Query optimization කියන්නේ database-driven applications වල performance වැඩි කරන්න අත්‍යවශ්‍ය දෙයක්. විශේෂයෙන්ම large datasets එක්ක වැඩ කරනකොට මේකේ වටිනාකම තේරෙයි. අපි බලමු ප්‍රධාන optimization techniques ටිකක්.

1. N+1 Problem එක විසඳමු!

මේක තමයි Hibernate / JPA භාවිත කරනකොට එන ප්‍රධානම performance issue එකක්. අපි උදාහරණයක් ගමු. Student කෙනෙක්ට Courses කිහිපයක් තියෙනවා කියලා හිතන්න (OneToMany relation එකක්). අපි Studentලා ටිකක් fetch කරලා, ඊට පස්සේ හැම Student කෙනෙක්ටම අදාළ Courses ටික වෙන වෙනම fetch කරන්න යනකොට, මුලින් Studentලා ටික ගන්න එක query එකක් (N=1), ඊට පස්සේ හැම Student කෙනෙක්ටම වෙන වෙනම Courses ගන්න තව N queries (N=N). මේක තමයි N+1 problem එක. ඒ කියන්නේ, එක ප්‍රධාන query එකකින් පස්සේ තව N ගණනක් queries execute වෙනවා. මේකෙන් database එකට unnecessarily load වෙනවා.

මේකට විසඳුම් කීපයක් තියෙනවා:

a. FetchType (LAZY vs EAGER)

Relations වලදී FetchType එක define කරන්න පුළුවන්. EAGER කියන්නේ parent entity එක fetch කරනකොටම child entities ටිකත් fetch කරනවා. මේක N+1 problem එකට හේතුවක් වෙන්න පුළුවන්. ඒ නිසා, හැමවිටම FetchType.LAZY භාවිත කරන්න උත්සාහ කරන්න. LAZY කියන්නේ child entity එක අවශ්‍ය වුණාම විතරයි fetch කරන්නේ. OneToOne සහ ManyToOne වලට default එක EAGER, OneToMany සහ ManyToMany වලට default එක LAZY.

b. JOIN FETCH භාවිතය

මේක තමයි N+1 problem එකට තියෙන හොඳම විසඳුම. අපිට JPQL query එක ඇතුලේ JOIN FETCH භාවිත කරලා අවශ්‍ය relations ටික එකම query එකකින් fetch කරගන්න පුළුවන්. මේකෙන් වෙනම queries execute වෙන එක වළක්වනවා.


// Student Entity එකට Course Entity එකක් එක්ක relation එකක් එකතු කරමු
@Entity
public class Student {
    // ... basic fields ...

    @OneToMany(mappedBy = "student", fetch = FetchType.LAZY) // Default to LAZY
    private List<Course> courses = new ArrayList<>();

    // Getters and Setters
}

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

    @ManyToOne(fetch = FetchType.LAZY) // Default to LAZY
    @JoinColumn(name = "student_id")
    private Student student;

    // Getters and Setters
}

// StudentRepository එකේ JOIN FETCH query එකක්
public interface StudentRepository extends JpaRepository<Student, Long> {
    @Query("SELECT s FROM Student s JOIN FETCH s.courses WHERE s.id = ?1")
    Optional<Student> findStudentWithCoursesJoinFetch(Long studentId);

    // සියලුම Students සහ ඔවුන්ගේ courses fetch කරන්න
    @Query("SELECT s FROM Student s JOIN FETCH s.courses")
    List<Student> findAllStudentsWithCourses();
}

findAllStudentsWithCourses() query එක run කරලා බලන්න, ඒකෙන් එකම query එකකින් සියලුම students ලාට අදාළ courses ටිකත් fetch කරනවා. මේක N+1 problem එකට හොඳම විසඳුමක්!

c. @BatchSize

@BatchSize annotation එක භාවිත කරලා අපිට lazy loaded collections / entities ටිකක් group වශයෙන් fetch කරන්න පුළුවන්. මේකෙන් වෙනම queries ගොඩක් generate වෙන එක වෙනුවට, එක query එකකින් විශාල data set එකක් batch එකක් විදියට fetch කරනවා.


import org.hibernate.annotations.BatchSize;

@Entity
public class Student {
    // ... basic fields ...

    @OneToMany(mappedBy = "student", fetch = FetchType.LAZY)
    @BatchSize(size = 10) // 10 students ලාගේ courses එක batch එකකට fetch කරන්න
    private List<Course> courses = new ArrayList<>();

    // Getters and Setters
}

මේක N+1 problem එක සම්පූර්ණයෙන්ම විසඳන්නේ නැහැ, ඒත් queries ගණන BatchSize එකට අනුව අඩු කරනවා. උදාහරණයක් විදියට, students 100 ක් ඉන්නවා නම්, BatchSize 10 දාද්දි, queries 100 ක් වෙනුවට queries 10 ක් වගේ අඩු වෙනවා.

d. Entity Graphs

JPA 2.1 වලින් හඳුන්වා දුන්න Entity Graphs කියන්නේ N+1 problem එකට තව හොඳ විසඳුමක්. මේකෙන් අපිට query එකක් run කරනකොට load කරන්න ඕනේ relationships මොනවද කියලා specify කරන්න පුළුවන්. මේක වඩාත් flexible ක්‍රමයක්.


import org.springframework.data.jpa.repository.EntityGraph;

public interface StudentRepository extends JpaRepository<Student, Long> {

    @EntityGraph(attributePaths = {"courses"})
    Optional<Student> findById(Long studentId);

    @EntityGraph(attributePaths = {"courses"})
    List<Student> findAll();
}

මේකෙන් කියන්නේ findById හෝ findAll method එක call කරනකොට Student object එකත් එක්කම ඒකේ courses collection එකත් EAGERly fetch කරන්න කියලයි.

2. Pagination සහ Sorting

විශාල data sets එක්ක වැඩ කරනකොට, එකවර සියලුම data retrieve කරන එක performance වලට අහිතකරයි. ඒකට තමයි Pagination සහ Sorting භාවිත කරන්නේ. Spring Data JPA එකේ මේකට Pageable interface එක තියෙනවා.


import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

public interface StudentRepository extends JpaRepository<Student, Long> {

    // Pageable parameter එකක් දාලා queries කරන්න පුළුවන්
    Page<Student> findAll(Pageable pageable);

    // උදාහරණයක්: වයස අනුව sort කරලා page 10 ක් ගන්න, එක page එකක students 20 ක් ඉන්නවා
    // Pageable pageable = PageRequest.of(0, 20, Sort.by("age").descending());
    // Page<Student> studentPage = studentRepository.findAll(pageable);
}

3. Caching (Cache එකෙන් වැඩේ ලේසි කරගමු!)

Caching කියන්නේ Hibernate queries optimize කරන්න තියෙන තවත් ප්‍රබල ක්‍රමයක්. එක පාරක් fetch කරපු data ආයෙත් database එකෙන් ගන්නේ නැතුව, cache එකේ තියාගෙන, ඊලඟ පාර ඒ data ටික cache එකෙන්ම ගන්න පුළුවන්. මේකෙන් database hits අඩු වෙනවා.

  • First-Level Cache (Persistence Context): මේක Hibernate එකෙන් automatically manage කරනවා. එකම Transaction එකක් ඇතුලේ එකම entity එක නැවත නැවතත් fetch කරනකොට, පළමු වතාවට database එකෙන් අරන්, ඊට පස්සේ cache එකෙන් දෙනවා.
  • Second-Level Cache: මේක application-wide cache එකක්. Ehcache, Redis, Infinispan වගේ technologies මේකට පාවිච්චි කරන්න පුළුවන්. මේකෙන් වෙනත් sessions වලටත් data cache කරන්න පුළුවන්.
  • Query Cache: මේකෙන් queries වල result sets cache කරනවා. Query එකක් ආයෙත් execute වෙනකොට, ඒකේ result set එක cache එකේ තිබ්බොත්, database එකට යන්නේ නැහැ.

// application.properties / application.yml වල Second-Level Cache enable කරන හැටි
// spring.jpa.properties.hibernate.cache.use_second_level_cache=true
// spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.jcache.JCacheRegionFactory

// Entity එකකට cache කරන්න @Cacheable annotation එක පාවිච්චි කරන්න පුළුවන්
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Cache;

@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // Cache configuration
public class Student {
    // ...
}

4. Read-Only Transactions

ඔයාලා data update කරන්නේ නැතුව retrieve කරනවා නම්, @Transactional(readOnly = true) භාවිත කරන්න. මේකෙන් Hibernate එකට කියනවා මේ transaction එකේදී data modifications වෙන්නේ නැහැ කියලා. මේකෙන් performance වැඩි වෙනවා, මොකද Hibernate එකට dirty checking කරන්න, flush operations කරන්න ඕනේ වෙන්නේ නැහැ.


import org.springframework.transaction.annotation.Transactional;

@Service
public class StudentService {

    private final StudentRepository studentRepository;

    public StudentService(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    @Transactional(readOnly = true) // Read-only transaction
    public List<Student> getAllStudents() {
        return studentRepository.findAll();
    }

    @Transactional
    public Student saveStudent(Student student) {
        return studentRepository.save(student);
    }
}

ප්‍රායෝගික උදාහරණ: කෝඩ් එකෙන් බලමු!

දැන් අපි බලමු මේ concepts ටික පොඩි practical example එකකින්. අපි Student සහ Course entities දෙකම භාවිත කරලා N+1 problem එක solve කරගන්න විදියක් බලමු.

මුලින්ම, අපේ entities ටික:


// Student.java
package com.techbadda.guide.entity;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

    @OneToMany(mappedBy = "student", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Course> courses = new ArrayList<>();

    public Student() {}

    public Student(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public List<Course> getCourses() { return courses; }
    public void setCourses(List<Course> courses) { this.courses = courses; }

    public void addCourse(Course course) {
        courses.add(course);
        course.setStudent(this);
    }

    public void removeCourse(Course course) {
        courses.remove(course);
        course.setStudent(null);
    }
}

// Course.java
package com.techbadda.guide.entity;

import javax.persistence.*;

@Entity
@Table(name = "courses")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "student_id")
    private Student student;

    public Course() {}

    public Course(String title) {
        this.title = title;
    }

    // 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 Student getStudent() { return student; }
    public void setStudent(Student student) { this.student = student; }
}

ඊළඟට, අපේ Repository:


// StudentRepository.java
package com.techbadda.guide.repository;

import com.techbadda.guide.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    // N+1 Problem එක ඇති වෙනවා: Studentලා ටිකක් අරන්, පස්සේ ඒ හැම Student කෙනෙක්ටම Courses වෙනම fetch කරනකොට
    // List<Student> findAll(); // මේකෙන් Studentලා විතරයි EAGERly load වෙන්නේ (courses LAZY නිසා)

    // JOIN FETCH භාවිත කරලා N+1 Problem එක විසඳමු
    @Query("SELECT s FROM Student s JOIN FETCH s.courses")
    List<Student> findAllWithCoursesJoinFetch();

    // ID එක අනුව එක් Student කෙනෙක්ගේ සියලුම courses එක්ක fetch කරන්න
    @Query("SELECT s FROM Student s JOIN FETCH s.courses WHERE s.id = :studentId")
    Optional<Student> findStudentByIdWithCoursesJoinFetch(@org.springframework.data.repository.query.Param("studentId") Long studentId);
}

දැන් අපි Service layer එකෙන් මේක call කරන හැටි බලමු:


// StudentService.java
package com.techbadda.guide.service;

import com.techbadda.guide.entity.Course;
import com.techbadda.guide.entity.Student;
import com.techbadda.guide.repository.StudentRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class StudentService {

    private final StudentRepository studentRepository;

    public StudentService(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    @Transactional
    public void populateData() {
        Student student1 = new Student("Nimal Perera", "[email protected]");
        student1.addCourse(new Course("Software Engineering"));
        student1.addCourse(new Course("Database Management"));
        studentRepository.save(student1);

        Student student2 = new Student("Kamala Silva", "[email protected]");
        student2.addCourse(new Course("Web Development"));
        student2.addCourse(new Course("UI/UX Design"));
        studentRepository.save(student2);

        System.out.println("Dummy data populated!");
    }

    @Transactional(readOnly = true)
    public List<Student> getAllStudentsWithCourses() {
        System.out.println("Fetching all students with courses using JOIN FETCH...");
        List<Student> students = studentRepository.findAllWithCoursesJoinFetch();
        students.forEach(student -> {
            System.out.println("Student: " + student.getName() + ", Email: " + student.getEmail());
            student.getCourses().forEach(course -> System.out.println("  - Course: " + course.getTitle()));
        });
        return students;
    }

    // N+1 problem එක පෙන්වන්න පුළුවන් simple findAll
    // @Transactional(readOnly = true)
    // public List getAllStudentsLazyLoad() {
    //     System.out.println("Fetching all students with potential N+1 problem...");
    //     List students = studentRepository.findAll(); // This fetches students only
    //     students.forEach(student -> {
    //         System.out.println("Student: " + student.getName() + ", Email: " + student.getEmail());
    //         // Accessing courses will trigger N additional queries if not JOIN FETCHed
    //         student.getCourses().forEach(course -> System.out.println("  - Course: " + course.getTitle()));
    //     });
    //     return students;
    // }
}

ඔයාලා application.properties එකේ spring.jpa.show-sql=true සහ spring.jpa.properties.hibernate.format_sql=true දාලා මේක run කරලා බලන්න. findAllWithCoursesJoinFetch() method එක call කරනකොට එකම SQL query එකකින් student සහ ඒ studentට අදාල courses දෙකම fetch වෙන හැටි ඔයාලට console එකේදී බලාගන්න පුළුවන් වේවි. ඒත් getAllStudentsLazyLoad() (uncomment කරලා run කරනවා නම්) call කරනකොට වෙනම queries ගොඩක් run වෙන හැටිත් බලන්න පුළුවන්. ඒක තමයි N+1 problem එක!

මේ වගේ පොඩි පොඩි දේවල් වලින් application එකක performance එක විශාල වශයෙන් වැඩි කරගන්න පුළුවන්.


අවසන් වචන: Application එක lightning fast කරමු!

ඉතින් යාලුවනේ, අද අපි කතා කළා Spring Boot එක්ක Hibernate භාවිත කරනකොට JPA Queries Optimize කරන හැටි ගැන. N+1 problem එකෙන් පටන් අරන්, ඒකට තියෙන විසඳුම්, BatchSize, Entity Graphs, Pagination, Caching සහ Read-Only Transactions වගේ වැදගත් concepts කීපයක් ගැන අපි ගැඹුරින්ම බැලුවා.

මේ techniques හරියට භාවිත කරනවා නම්, ඔයාලගේ applications වල performance එක සැලකිය යුතු ලෙස වැඩි කරගන්න පුළුවන්. මතක තියාගන්න, හොඳ application එකක් කියන්නේ user friendly වෙන එක විතරක් නෙමෙයි, performant වෙන එකත් අත්‍යවශ්‍යයි.

මේ වගේ technical topics තව දැනගන්න ඕනේ නම්, නැත්නම් මේ ලිපිය ගැන අදහස්, ප්‍රශ්න තියෙනවා නම් පහළින් comment එකක් දාන්න. ඔයාලගේ අදහස් අපි ගොඩක් අගය කරනවා. තවත් මේ වගේම වැදගත් ලිපියකින් හමුවෙමු! Happy Coding!