Spring Boot සහ Neo4j: ග්‍රැෆ් ඩේටාබේස් (Graph Database) වල බලය, Sinhala Guide for Developers

Spring Boot සහ Neo4j: ග්‍රැෆ් ඩේටාබේස් (Graph Database) වල බලය, Sinhala Guide for Developers

ඉතින් කොහොමද කට්ටිය? මේ දවස් වල Software Development කරන අපිට අලුත් technologies ගැන ඉගෙන ගන්න එක අත්‍යවශ්‍ය දෙයක් වෙලා නියමයි නේද? විශේෂයෙන්ම Data Management කියන කෑල්ල, දවසින් දවස දියුණු වෙන විදිහට අපේ applications වලට ගැලපෙන Data Storage solution එකක් තෝරගන්න එකත් ලොකු අභියෝගයක්. Relational Databases (SQL) සහ NoSQL Databases ගැන අපි ගොඩක් දන්නවා. ඒත් මේ අතරේ තවත් විශේෂිත database වර්ගයක් තියෙනවා – ඒ තමයි Graph Databases.

අද අපි කතා කරන්න යන්නේ මේ Graph Databases අතරින් ජනප්‍රියම එකක් වෙන Neo4j ගැන. ඒ වගේම මේ Neo4j එක්ක අපේ ආදරණීය Spring Boot එක කොහොමද ගලපලා වැඩ කරන්නේ කියලා. අපි විශේෂයෙන්ම පොඩි Social Network එකක් හදලා ඒකේ Nodes සහ Relationships කොහොමද හසුරුවන්නේ කියලා බලමු. මේක ඔයාලට Graph Databases කියන්නේ මොනවද, ඒවා කොතැනටද ගැලපෙන්නේ, සහ Spring Boot එක්ක ඒවට පහසුවෙන් interact කරන්නේ කොහොමද කියලා තේරුම් ගන්න හොඳ අවස්ථාවක් වෙයි.

එහෙනම්, අපි පටන් ගමු නේද?

Spring Boot සහ Neo4j: ඇයි මේ කෝම්බිනේෂන් එක?

හරි, මුලින්ම බලමු ඇයි අපිට මේ දෙන්නව එකට ඕන වෙන්නේ කියලා. අපි දන්නවා Spring Boot කියන්නේ Java applications ඉක්මනට develop කරන්න තියෙන නියම framework එකක්. Setup කිරීම් අඩුයි, Dependency Management එක පහසුයි, production-ready applications හදන්න පුළුවන්. ඒකනේ අපි හැමෝම ඒකට ආස.

ඒ වගේම Neo4j කියන්නේ ලෝකයේ ජනප්‍රියම Graph Database එක. Relationship-heavy data වලට, ඒ කියන්නේ යම් Nodes අතර තියෙන connections හසුරුවන්න Neo4j ඉතාම powerful. Facebook, LinkedIn වගේ Social Networks, Recommendation Engines (Netflix), Fraud Detection systems වගේ තැන්වලට Graph Databases ඉතාම සුදුසුයි.

ඉතින්, Spring Boot එක්ක Neo4j එකතු වුණාම අපිට Spring Data Neo4j (SDN) කියන module එක හම්බෙනවා. මේකෙන් Neo4j එක්ක වැඩ කරන එක හරිම පහසු වෙනවා. Repository pattern එක, automatic object-graph mapping එක වගේ දේවල් නිසා අපිට boilerplate code ලියන එකෙන් ගැලවෙන්න පුළුවන්. ඒ නිසා, complex relationships තියෙන data models හසුරුවන්න ඕන කරන applications හදද්දී, මේ දෙන්නගේ combination එක නියම විසඳුමක්.

ග්‍රැෆ් ඩේටාබේස් (Graph Database) කියන්නේ මොකද්ද?

අපි Relational Databases වලදී දත්ත Table විදිහට හසුරුවනවා. NoSQL වලදී Key-Value, Document, Column Family වගේ විදිහට. Graph Databases වලදී data හසුරුවන්නේ Graph structure එකකට අනුවයි. මේ Graph එකේ ප්‍රධාන කොටස් දෙකක් තියෙනවා:

  1. Nodes (නෝඩ්ස්): මේවා තමයි අපේ data entities. උදාහරණයක් විදිහට, Social Network එකකදී පුද්ගලයෙක් (Person), පෝස්ට් එකක් (Post), කම්පැනියක් (Company) කියන්නේ Nodes.
  2. Relationships (සම්බන්ධතා): මේවා Nodes දෙකක් අතර තියෙන සම්බන්ධය පෙන්නුම් කරනවා. උදාහරණයක් විදිහට, පුද්ගලයෙක් තව පුද්ගලයෙක්ගේ මිතුරෙක් (FRIENDS_WITH), පුද්ගලයෙක් පෝස්ට් එකක් ලිව්වා (WROTE), පුද්ගලයෙක් කම්පැනියක වැඩ කරනවා (WORKS_FOR) කියන ඒවා Relationships.

මේ Nodes සහ Relationships වලට Properties (ගුණාංග) තියෙන්න පුළුවන්. උදාහරණයක් විදිහට, Person Node එකට name, age වගේ Properties තියෙන්න පුළුවන්. FRIENDS_WITH Relationship එකට since (කවදා ඉඳන්ද යාළුවෝ) වගේ Property එකක් තියෙන්න පුළුවන්.

මේක තේරුම් ගන්න ලේසිම විදිහ තමයි, අපි කතා කරන Social Network එකම උදාහරණයට ගන්න එක. අපි දැන් ඒක design කරමු!

අපේ Social Network එක Design කරමු

අපි හදන්න යන Social Network එකේදී අපි ප්‍රධාන Entities දෙකක් identify කරගමු:

  • Person (පුද්ගලයා): මේක Node එකක්. මේ Person Node එකට 'name', 'age', 'city' වගේ Properties තියෙන්න පුළුවන්.
  • FRIENDS_WITH (මිතුරෙක්): මේක Relationship එකක්. මේක Person Nodes දෙකක් අතර තියෙන සම්බන්ධය පෙන්නනවා. මේ Relationship එකට 'since' (මිතුරන් වූ දිනේ) වගේ Property එකක් තියෙන්න පුළුවන්.

උදාහරණයක් විදිහට:

  • Node 1: (Person {name: 'Amara', age: 30})
  • Node 2: (Person {name: 'Kamal', age: 32})
  • Relationship: (Amara)-[:FRIENDS_WITH {since: '2020-01-15'}]->(Kamal)

මේ වගේ Graph එකක් හදලා අපි දැන් බලමු මේක Spring Boot එක්ක කොහොමද code කරන්නේ කියලා.

Spring Data Neo4j සමඟ කෝඩ් කරමු

හරි, දැන් අපි අපේ Spring Boot project එක හදලා Neo4j එක්ක integrate කරමු. ඔයාලට පුළුවන් Spring Initializr (start.spring.io) පාවිච්චි කරලා project එකක් හදාගන්න. Dependencies විදිහට මේ ටික add කරගන්න:

  • Spring Web (REST API හදන්න)
  • Spring Data Neo4j
  • Lombok (boilerplate code අඩු කරන්න)

1. Dependencies Add කරගමු (pom.xml for Maven)

ඔයාලා Maven පාවිච්චි කරනවා නම්, pom.xml එකට මේ dependencies add කරන්න:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

ඒ වගේම application.properties (හෝ application.yml) ෆයිල් එකේ Neo4j connection details දෙන්න ඕන. ඔයාලා local Neo4j instance එකක් පාවිච්චි කරනවා නම්, default settings මෙහෙම වෙයි:


spring.neo4j.uri=bolt://localhost:7687
spring.neo4j.username=neo4j
spring.neo4j.password=your_password

your_password කියන තැන ඔයාලගේ Neo4j password එක දෙන්න. (Neo4j Desktop එකෙන් හෝ Docker වලින් Neo4j run කරනවා නම් password එක reset කරගන්න පුළුවන්).

2. Entity Classes හදමු

දැන් අපි අපේ Node සහ Relationship entities හදමු. Spring Data Neo4j වලදී Node එකක් නියෝජනය කරන්න @Node annotation එකත්, Relationship එකක් නියෝජනය කරන්න @RelationshipProperties annotation එකත් පාවිච්චි කරනවා.

Person.java:


import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Relationship;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.HashSet;
import java.util.Set;

@Node("Person") // This specifies that this class represents a 'Person' node in Neo4j
@Data // Lombok annotation for getters, setters, toString, equals, hashCode
@NoArgsConstructor // Lombok annotation for no-arg constructor
@AllArgsConstructor // Lombok annotation for all-arg constructor
public class Person {

    @Id @GeneratedValue // Neo4j generates ID automatically
    private Long id;

    private String name;
    private int age;
    private String city;

    // Relationships: Here we define the 'FRIENDS_WITH' relationship.
    // DIRECTION.OUTGOING means Person -> FRIENDS_WITH -> OtherPerson
    // DIRECTION.INCOMING would mean OtherPerson -> FRIENDS_WITH -> Person
    // type = "FRIENDS_WITH" specifies the relationship type
    @Relationship(type = "FRIENDS_WITH", direction = Relationship.Direction.OUTGOING)
    private Set<Friendship> friends = new HashSet<>();

    public Person(String name, int age, String city) {
        this.name = name;
        this.age = age;
        this.city = city;
    }

    // Helper method to add a friend
    public void addFriend(Person friend, String since) {
        if (this.friends == null) {
            this.friends = new HashSet<>();
        }
        this.friends.add(new Friendship(this, friend, since));
    }
}

Friendship.java (Relationship Entity):


import org.springframework.data.neo4j.core.schema.RelationshipId;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@RelationshipProperties // This specifies that this class represents a relationship
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Friendship {

    @RelationshipId // Neo4j generates ID for the relationship
    private Long id;

    // The source node of the relationship. It's usually not explicitly defined here
    // as the relationship is managed from the 'Person' entity's perspective.
    // private Person sourcePerson;

    @TargetNode // This annotation marks the target node of the relationship
    private Person targetPerson;

    private String since;

    // Constructor for creating new friendships
    public Friendship(Person sourcePerson, Person targetPerson, String since) {
        // The sourcePerson is implicitly managed by the @Relationship annotation in Person class
        this.targetPerson = targetPerson;
        this.since = since;
    }
}

සැ.යු.: Friendship class එකේදී අපි sourcePerson එක explicit විදිහට define කරන්නේ නෑ. ඒක Person class එකේ @Relationship annotation එකෙන් auto manage වෙනවා. @TargetNode එකෙන් අපි relationship එක connect වෙන අනිත් node එක identify කරනවා.

3. Repository Interface හදමු

අපි Relational Databases එක්ක වැඩ කරද්දී JPA Repositories පාවිච්චි කරනවා වගේම, Spring Data Neo4j වලදී Neo4jRepository එක පාවිච්චි කරනවා.

PersonRepository.java:


import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface PersonRepository extends Neo4jRepository<Person, Long> {

    // Derived query method: Spring Data automatically generates query based on method name
    List<Person> findByName(String name);

    // Custom Cypher query using @Query annotation
    @Query("MATCH (p:Person)-[:FRIENDS_WITH]->(f:Person)-[:FRIENDS_WITH]->(ff:Person) " +
           "WHERE p.name = $name AND NOT (p)-[:FRIENDS_WITH]->(ff) AND p <> ff " +
           "RETURN DISTINCT ff")
    List<Person> findFriendsOfFriends(String name);

    // Custom query to find mutual friends
    @Query("MATCH (p1:Person)-[:FRIENDS_WITH]->(mutualFriend:Person)-[:FRIENDS_WITH]->(p2:Person) " +
           "WHERE p1.name = $person1Name AND p2.name = $person2Name " +
           "RETURN DISTINCT mutualFriend")
    List<Person> findMutualFriends(String person1Name, String person2Name);

}

උඩ තියෙන example එකේ findByName කියන්නේ Derived Query Method එකක්. ඒක Spring Data එකෙන් Auto Generate වෙනවා. findFriendsOfFriends සහ findMutualFriends කියන්නේ @Query annotation එක පාවිච්චි කරලා ලියපු Cypher queries. Cypher කියන්නේ Neo4j වලට විශේෂිත query language එක.

4. Service Layer එකක් හදමු

දැන් අපි Controller එකකින් Request එකක් ආවම Repository එක හරහා Neo4j එක්ක Interact කරන්න Service layer එකක් හදමු.

PersonService.java:


import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class PersonService {

    private final PersonRepository personRepository;

    public PersonService(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Transactional
    public Person createPerson(Person person) {
        return personRepository.save(person);
    }

    @Transactional
    public Person addFriendship(String personName1, String personName2, String since) {
        Person person1 = personRepository.findByName(personName1).stream().findFirst()
                .orElseThrow(() -> new RuntimeException("Person " + personName1 + " not found"));
        Person person2 = personRepository.findByName(personName2).stream().findFirst()
                .orElseThrow(() -> new RuntimeException("Person " + personName2 + " not found"));

        person1.addFriend(person2, since);
        return personRepository.save(person1);
    }

    public List<Person> getAllPeople() {
        return personRepository.findAll();
    }

    public List<Person> getFriendsOfFriends(String name) {
        return personRepository.findFriendsOfFriends(name);
    }

    public List<Person> getMutualFriends(String person1Name, String person2Name) {
        return personRepository.findMutualFriends(person1Name, person2Name);
    }
}

@Transactional annotation එක මෙතන වැදගත්. ඒකෙන් කියන්නේ මේ method එක ඇතුලේ වෙන operations ටික එක transaction එකක් විදිහට run කරන්න කියලා. Graph operations වලදී මේක වැදගත් වෙනවා.

5. REST Controller එකක් හදමු

දැන් අපි මේ Service layer එකට access කරන්න REST API Endpoints ටිකක් හදමු.

PersonController.java:


import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/people")
public class PersonController {

    private final PersonService personService;

    public PersonController(PersonService personService) {
        this.personService = personService;
    }

    @PostMapping
    public ResponseEntity<Person> createPerson(@RequestBody Person person) {
        return ResponseEntity.ok(personService.createPerson(person));
    }

    @GetMapping
    public ResponseEntity<List<Person>> getAllPeople() {
        return ResponseEntity.ok(personService.getAllPeople());
    }

    @PostMapping("/add-friendship")
    public ResponseEntity<Person> addFriendship(@RequestBody Map<String, String> payload) {
        String person1Name = payload.get("person1Name");
        String person2Name = payload.get("person2Name");
        String since = payload.getOrDefault("since", "N/A");
        return ResponseEntity.ok(personService.addFriendship(person1Name, person2Name, since));
    }

    @GetMapping("/friends-of-friends/{name}")
    public ResponseEntity<List<Person>> getFriendsOfFriends(@PathVariable String name) {
        return ResponseEntity.ok(personService.getFriendsOfFriends(name));
    }

    @GetMapping("/mutual-friends")
    public ResponseEntity<List<Person>> getMutualFriends(@RequestParam String person1, @RequestParam String person2) {
        return ResponseEntity.ok(personService.getMutualFriends(person1, person2));
    }
}

දැන් ඔයාලට පුළුවන් මේ application එක run කරලා Postman හෝ curl වගේ tool එකක් පාවිච්චි කරලා test කරන්න:

  • Create People: POST /api/people with {"name": "Amara", "age": 30, "city": "Colombo"}
  • Create Friendships: POST /api/people/add-friendship with {"person1Name": "Amara", "person2Name": "Kamal", "since": "2020-01-15"}
  • Get Friends of Friends: GET /api/people/friends-of-friends/Amara
  • Get Mutual Friends: GET /api/people/mutual-friends?person1=Amara&person2=Kamal

මේවා හරහා data create කරලා Neo4j Browser (http://localhost:7474) එකට ගිහින් MATCH (n) RETURN n; හෝ MATCH (p:Person)-[r]->(f:Person) RETURN p,r,f; වගේ Cypher queries run කරලා ඔයාලා add කරපු data සහ relationships visualize කරන්න පුළුවන්. ඒක හරිම Cool!

ප්‍රයෝගික උදාහරණ සහ Tips

අපි මේ හදපු Social Network එක තව දුරටත් දියුණු කරන්න පුළුවන්. උදාහරණයක් විදිහට:

  • Recommendation Engine: Graph queries පාවිච්චි කරලා 'ඔබට මිතුරන් විය හැකි පුද්ගලයන්' (Friends You May Know) වගේ Recommendations දෙන්න පුළුවන්. ඒකට 'shortest path' queries හෝ 'common interests' වගේ දේවල් පාවිච්චි කරන්න පුළුවන්.
  • Data Migrations: existing Relational database එකක ඉඳන් Graph database එකකට data migrate කරනවා නම්, ඒකට tools සහ strategies ගොඩක් තියෙනවා.
  • Performance Tuning: විශාල Graph databases එක්ක වැඩ කරද්දී queries වල performance එක ඉතාම වැදගත්. Indexes හදලා, Cypher queries optimize කරලා performance වැඩි දියුණු කරන්න පුළුවන්.
  • Neo4j Aura: local database එකක් වෙනුවට cloud-based Neo4j instance එකක් (Neo4j Aura) පාවිච්චි කරන්නත් පුළුවන්. ඒකෙන් infrastructure management කරදර නැතුව development කරන්න පුළුවන්.

මතක තියාගන්න, Graph Databases හැම problem එකකටම solution එකක් නෙවෙයි. ඒත් relationships ගොඩක් තියෙන data models වලට නම් මේවා truly shine කරනවා. ඒ වගේම Spring Boot වගේ framework එකක් එක්ක වැඩ කරද්දී development experience එකත් නියමයි.

අවසන් වචන

ඉතින් අද අපි Spring Boot සහ Neo4j එකට පාවිච්චි කරලා සරල Social Network එකක data model එකක් හදලා, Nodes, Relationships create කරලා, ඒවා query කරන විදිහ ගැන ඉගෙන ගත්තා. මේක ඔයාලට Graph Databases කියන concept එක සහ Spring Data Neo4j වල power එක තේරුම් ගන්න හොඳ පදනමක් වෙන්න ඇති කියලා හිතනවා.

Graph Databases කියන්නේ හරිම fascinating domain එකක්. මේක ඔයාලගේ project වලට අලුත් විසඳුම් හොයන්න හොඳ ආරම්භයක් වෙයි. පුළුවන් නම් මේ article එකේ code snippets ටික ඔයාලගේ local machine එකේ try කරලා බලන්න. Neo4j Desktop install කරලා හෝ Docker වලින් run කරලා experiment කරන්න. එතකොට තවත් හොඳට තේරෙයි.

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

ආයෙත් මේ වගේ අලුත් දෙයක් අරගෙන එනකල්, Happy Coding! 😊