05 tháng 12 năm 2024 | Máy tính
Bài viết trước “Khám phá Neo4j” đã giới thiệu các khái niệm cơ bản của Neo4j và cách sử dụng sơ bộ. Bài viết này sẽ tập trung vào việc làm thế nào để sử dụng Spring Data Neo4j để truy cập cơ sở dữ liệu Neo4j. Spring Data Neo4j là một phần của dự án Spring Data, giúp đơn giản hóa việc tương tác với cơ sở dữ liệu đồ thị Neo4j. Ngoài khả năng thực hiện các thao tác CRUD thông qua Repository, Spring Data Neo4j còn hỗ trợ quản lý giao dịch, truy vấn Cypher và mô hình hóa dữ liệu đồ thị.
Tiếp theo, chúng ta sẽ trình bày ví dụ về cách sử dụng Spring Data Neo4j. Ví dụ này liên quan đến tình huống kinh doanh lưu trữ dữ liệu về mối quan hệ giữa “Diễn viên (Actor) - Tham gia (ACTED_IN) -> Phim (Movie)”.
Phiên bản Neo4j cài đặt cục bộ là 5.25.1
, và các phụ thuộc cùng phiên bản trong ví dụ này bao gồm:
JDK: BellSoft Liberica 17.0.7
Maven: 3.9.2
Spring Boot: 3.4.0
Spring Data Neo4j: 7.4.0 [Sam86 Club Choi Game Bài](/post/4011/)
Cấu trúc thư mục của ví dụ này như sau:
spring-data-neo4j-demo
├─ src
│ ├─ main
│ │ ├─ java
│ │ │ └─ com.example.demo
│ │ │ ├─ repository
│ │ │ │ ├─ MovieRepository.java
│ │ │ │ └─ ActorRepository.java
│ │ │ ├─ service
│ │ │ │ ├─ ActorMovieService.java
│ │ │ │ └─ impl
│ │ │ │ └─ ActorMovieServiceImpl.java
│ │ │ ├─ model
│ │ │ │ ├─ Actor.java
│ │ │ │ ├─ Movie.java
│ │ │ │ └─ Role.java
│ │ │ └─ DemoApplication.java
│ │ └─ resources
│ │ └─ application.properties
│ └─ test
│ └─ java
│ └─ com.example.demo
│ ├─ repository
│ │ ├─ MovieRepositoryTest.java
│ │ └─ ActorRepositoryTest.java
│ └─ service
│ └─ ActorMovieServiceTest.java
└─ pom.xml
Từ cấu trúc thư mục trên có thể thấy đây là một dự án Maven tiêu chuẩn. Thư mục src/main
chứa mã nguồn chính và tệp cấu hình, trong khi src/test
chứa các lớp kiểm thử đơn vị. Tất cả mã Java trong src/main/java
đều nằm trong gói com.example.demo
, trong đó DemoApplication
là điểm nhập của chương trình, model
chứa các lớp mô hình, repository
chứa các lớp kho lưu trữ, và service
chứa các lớp dịch vụ.
Thư mục src/test/java
cũng có cấu trúc gói giống như mã chính. Các lớp kiểm thử đơn vị trong repository
như MovieRepositoryTest
dùng để kiểm tra MovieRepository
, ActorRepositoryTest
dùng để kiểm tra ActorRepository
; và ActorMovieServiceTest
trong service
dùng để kiểm tra ActorMovieService
.
Sau khi giới thiệu cấu trúc dự án, hãy xem xét các phụ thuộc chính được sử dụng:
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Dễ thấy rằng dự án chỉ có rất ít phụ thuộc chính yếu, chủ yếu là spring-boot-starter-web
và spring-boot-starter-data-neo4j
. Để tránh phải viết phương thức Setters, Getters và hàm tạo cho các lớp Model, dự án sử dụng phụ thuộc lombok
. Cuối cùng, để viết kiểm thử đơn vị, dự án cũng phụ thuộc vào spring-boot-starter-test
.
Giới thiệu xong cấu trúc dự án và các phụ thuộc liên quan, bây giờ bắt đầu phân tích các tệp hoặc mã chính trong dự án.
1 Phân tích mã nguồn
Ví dụ trong bài viết này nhắm tới tình huống “Diễn viên (Actor) - Tham gia (ACTED_IN) -> Phim (Movie)”, với biểu đồ mô hình như sau. ![Biểu đồ mô hình “Diễn viên (Actor) - Tham gia (ACTED_IN) -> Phim (Movie)”]
1.1 Tệp cấu hình
Tệp cấu hình application.properties
có nội dung như sau:
# src/main/resources/application.properties
spring.neo4j.uri=bolt://localhost:7687/neo4j
spring.neo4j.authentication.username=neo4j
spring.neo4j.authentication.password=neo4j
Như vậy, chúng ta đã cấu hình địa chỉ truy cập Neo4j, tên người dùng và mật khẩu trong tệp cấu hình.
1.2 Lớp Model
Lớp Model trong Java được sử dụng để ánh xạ trực tiếp với các nút hoặc mối quan hệ trong Neo4j. Từ biểu đồ mô hình trên, “Diễn viên (Actor) - Tham gia (ACTED_IN) -> Phim (Movie)” bao gồm hai nút: Diễn viên và Phim, cùng với một mối quan hệ: Tham gia. Vì vậy, ví dụ này có ba lớp Model: Actor
, Movie
và Role
, lần lượt đại diện cho nút Diễn viên, nút Phim và mối quan hệ diễn viên đóng vai trong phim.
Lớp Actor
có nội dung như sau:
// src/main/java/com/example/demo/model/Actor.java
package com.example.demo.model;
import org.springframework.data.neo4j.core.schema.*;
import lombok.*;
@Data
@NoArgsConstructor
@Node
public class Actor {
@Id
@GeneratedValue
private Long id;
private String name;
private String nationality;
private int yearOfBirth;
public Actor(String name, String nationality, int yearOfBirth) {
this.name = name;
this.nationality = nationality;
this.yearOfBirth = yearOfBirth;
}
}
Lớp này được đánh dấu bằng chú thích @Node
, biểu thị nó tương ứng với nút Actor trong Neo4j. Để phân biệt từng cá thể trong Actor, mỗi thực thể Actor nên có khóa chính, vì vậy trường id
được đánh dấu bằng @Id
là khóa chính của Actor, và @GeneratedValue
biểu thị giá trị này được sinh tự động. Ngoài ra, nút Actor còn có ba thuộc tính khác: name
, nationality
và yearOfBirth
.
Lớp Movie
có nội dung như sau:
// src/main/java/com/example/demo/model/Movie.java
package com.example.demo.model;
import org.springframework.data.neo4j.core.schema.*;
import lombok.*;
import java.util.List;
@Data
@NoArgsConstructor
@Node
public class Movie {
@Id
@GeneratedValue
private Long id;
private String name;
private int releasedAt;
@Relationship(type="ACTED_IN", direction=Relationship.Direction.INCOMING)
private List<Role> rolesAndActors;
public Movie(String name, int releasedAt, List<Role> rolesAndActors) {
this.name = name;
this.releasedAt = releasedAt;
this.rolesAndActors = rolesAndActors;
}
}
Lớp này tương ứng với nút Movie trong Neo4j, ngoài việc có một trường khóa chính thì còn có hai thuộc tính name
và releasedAt
. Điểm mấu chốt là nó có một trường được đánh dấu bằng chú thích @Relationship
là rolesAndActors
, biểu thị đây là một thuộc tính mối quan hệ, không phải thuộc tính thông thường. Loại mối quan hệ là ACTED_IN
(tham gia), hướng là INCOMING
, biểu thị mối quan hệ đi vào (tức mũi tên chỉ vào Movie). Một bộ phim có thể được nhiều diễn viên tham gia, vì vậy rolesAndActors
là kiểu List<Role>
.
Lớp Role
có nội dung như sau:
// src/main/java/com/example/demo/model/Role.java
package com.example.demo.model;
import org.springframework.data.neo4j.core.schema.*;
import lombok.*;
@Data [winvip.club](/post/how-does-spring-data-operate-both-mysql-and-neo4j/)
@NoArgsConstructor
@RelationshipProperties
public class Role {
@RelationshipId
private Long id;
private String name;
@TargetNode
private Actor actor;
public Role(String name, Actor actor) {
this.name = name;
this.actor = actor;
}
}
Lớp này được đánh dấu bằng chú thích @RelationshipProperties
, biểu thị nó là một lớp thuộc tính mối quan hệ. Lớp này cũng cần một khóa chính, vì vậy có một trường id
được đánh dấu bằng @RelationshipId
. Ngoài ra còn có một thuộc tính name
, biểu thị tên vai diễn mà diễn viên đảm nhận trong bộ phim. Và một trường actor
được đánh dấu bằng @TargetNode
, biểu thị đầu kia của mối quan hệ là một Actor.
Ba lớp Model trên đã hoàn thành, từ đó mô hình “Diễn viên (Actor) - Tham gia (ACTED_IN) -> Phim (Movie)” đã được thiết lập.
1.3 Giao diện Repository
Sau khi định nghĩa xong lớp Model, bước tiếp theo là định nghĩa giao diện Repository để truy vấn Neo4j.
Chúng ta biết rằng Spring Data Repository thống nhất cách truy cập các loại cơ sở dữ liệu khác nhau (chẳng hạn như MySQL, Oracle… cơ sở dữ liệu quan hệ, MongoDB… cơ sở dữ liệu phi quan hệ, Neo4j… cơ sở dữ liệu đồ thị). Chúng ta chỉ cần định nghĩa một giao diện Repository, kế thừa từ một giao diện cha thì sẽ có ngay các chức năng CRUD cơ bản. Ngoài ra, chúng ta cũng có thể thêm các phương thức truy vấn tùy chỉnh theo quy tắc đặt tên.
Ví dụ này có hai giao diện Repository: ActorRepository
và MovieRepository
, lần lượt dùng để truy vấn Actor và Movie.
Giao diện ActorRepository
có nội dung như sau:
// src/main/java/com/example/demo/repository/ActorRepository.java
package com.example.demo.repository;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;
import java.util.List;
public interface ActorRepository extends Neo4jRepository<Actor, Long>, CypherdslConditionExecutor<Actor> {
@Query("""
MATCH (a:Actor)-[:ACTED_IN]->(m:Movie)
WHERE m.name = $name
RETURN a.name
""")
List<String> findActorNamesByMovieName(String name);
@Query("""
MATCH (a:Actor)-[:ACTED_IN]->(m:Movie)
WHERE m.name = $name
RETURN COALESCE(AVG(datetime().year - a.yearOfBirth), 0)
""")
double findAverageAgeOfActorsByMovieName(String name);
@Query("""
MATCH (a1:Actor {name: $actor1})
MATCH (a2:Actor {name: $actor2})
MATCH p = shortestPath((a1)-[*..10]-(a2))
RETURN p
""")
List<Path> findShortestPathBetweenActors(String actor1, String actor2);
@Query("""
MATCH (a:Actor)
WHERE a.name = $name
SET a.yearOfBirth = $yearOfBirth
""")
void updateYearOfBirthByName(String name, int yearOfBirth);
}
Giao diện này kế thừa hai giao diện cha: Neo4jRepository
và CypherdslConditionExecutor
. Neo4jRepository
ngoài việc cung cấp các chức năng CRUD cơ bản còn hỗ trợ phân trang và sắp xếp. Trong khi đó, CypherDslConditionExecutor
hỗ trợ truy vấn Cypher DSL, tức là cho phép thực hiện các truy vấn phức tạp bằng cách lập trình.
Ngoài ra, chúng ta còn sử dụng chú thích @Query
để viết một loạt các truy vấn Cypher tùy chỉnh (và phương thức cập nhật), lần lượt dùng để: tìm danh sách tên diễn viên tham gia bộ phim theo tên phim (findActorNamesByMovieName()
), tìm tuổi trung bình của các diễn viên tham gia bộ phim theo tên phim (findAverageAgeOfActorsByMovieName()
), tìm đường ngắn nhất giữa hai diễn viên (findShortestPathBetweenActors()
), và cập nhật năm sinh theo tên diễn viên (updateYearOfBirthByName()
).
Giao diện MovieRepository
có nội dung như sau:
// src/main/java/com/example/demo/repository/MovieRepository.java
package com.example.demo.repository;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor;
import java.util.List;
public interface MovieRepository extends Neo4jRepository<Movie, Long>, CypherdslConditionExecutor<Movie> {
List<Movie> findByName(String name);
@Query("""
MATCH (m:Movie)<-[:ACTED_IN]-(a:Actor)
WHERE a.name = $name
RETURN m.name
""")
List<String> findMovieNamesByActorName(String name);
}
Giao diện này cũng kế thừa hai giao diện cha: Neo4jRepository
và CypherdslConditionExecutor
. Ngoài ra còn thêm một phương thức đặt tên theo quy tắc (findByName()
) và một truy vấn tùy chỉnh (findMovieNamesByActorName()
), lần lượt dùng để: tìm Movie theo tên và tìm tên các bộ phim mà diễn viên tham gia theo tên diễn viên.
Sau khi giới thiệu hai giao diện Repository
này, chúng ta sẽ tạo một giao diện ActorMovieService
trong gói service
, rồi triển khai giao diện này trong ActorMovieServiceImpl
để khám phá các tính năng khác của Spring Data Neo4j.
1.4 Giao diện ActorMovieService và triển khai
Giao diện ActorMovieService
có nội dung như sau:
// src/main/java/com/example/demo/service/ActorMovieService.java
package com.example.demo.service;
import java.util.List;
public interface ActorMovieService {
List<Movie> findMoviesByActorName(String name);
List<Actor> findActorsByNamePrefix(String prefix);
List<Actor> findActorsByNamePrefixWithQueryByExample(String prefix);
void updateMovie(Movie movie);
}
Giao diện này định nghĩa bốn phương thức, lần lượt dùng để: tìm các bộ phim mà diễn viên tham gia theo tên diễn viên (findMoviesByActorName()
), tìm diễn viên theo tiền tố tên (findActorsByNamePrefix()
), tìm diễn viên theo tiền tố tên nhưng sử dụng cách tiếp cận Query by Example (findActorsByNamePrefixWithQueryByExample()
), và cập nhật Movie (updateMovie()
).
Triển khai giao diện ActorMovieService
trong ActorMovieServiceImpl
có nội dung như sau:
// src/main/java/com/example/demo/service/impl/ActorMovieServiceImpl.java
package com.example.demo.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class ActorMovieServiceImpl implements ActorMovieService {
@Autowired
private ActorRepository actorRepository;
@Autowired
private MovieRepository movieRepository;
@Autowired
private Neo4jTemplate neo4jTemplate;
@Override
public List<Movie> findMoviesByActorName(String name) {
String cypher = """
MATCH (a:Actor)-[:ACTED_IN]->(m:Movie)
WHERE a.name = $name
RETURN m
""";
Map<String, Object> params = new HashMap<>();
params.put("name", name);
return neo4jTemplate.findAll(cypher, params, Movie.class);
}
@Override
public List<Actor> findActorsByNamePrefix(String prefix) {
Node actor = Cypher.node("Actor").named("actor");
Property name = actor.property("name");
Property yearOfBirth = actor.property("yearOfBirth");
Condition condition = name.startsWith(Cypher.anonParameter(prefix));
return actorRepository.findAll(condition, yearOfBirth.descending()).stream().collect(Collectors.toList());
}
@Override
public List<Actor> findActorsByNamePrefixWithQueryByExample(String prefix) {
Actor exampleActor = new Actor();
exampleActor.setName(prefix);
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("yearOfBirth")
.withStringMatcher(ExampleMatcher.StringMatcher.STARTING);
return actorRepository.findAll(Example.of(exampleActor, matcher), Sort.by("yearOfBirth").descending());
}
@Transactional
@Override
public void updateMovie(Movie movie) {
movieRepository.findById(movie.getId())
.map(m -> {
m.setName(movie.getName());
if (movie.getReleasedAt() > 0) {
m.setReleasedAt(movie.getReleasedAt());
}
return movieRepository.save(m);
})
.orElseThrow(() -> new RuntimeException("Phim không tìm thấy"));
}
}
Để ý rằng phương thức findMoviesByActorName()
sử dụng việc tự viết câu lệnh Cypher, thiết lập tham số và gọi Neo4jTemplate
để thực hiện; findActorsByNamePrefix()
sử dụng cách tiếp cận Cypher DSL để thực hiện truy vấn và sắp xếp; findActorsByNamePrefixWithQueryByExample()
có chức năng tương tự nhưng sử dụng cách tiếp cận Query by Example; và phương thức cuối cùng updateMovie()
sử dụng chú thích @Transactional
để hỗ trợ giao dịch (cập nhật sẽ bị hủy nếu ném ngoại lệ).
2 Kiểm thử đơn vị
Tiếp theo là viết các lớp kiểm thử đơn vị để kiểm tra Repository và Service đã đề cập ở trên.
Kiểm thử đơn vị MovieRepositoryTest
dành cho MovieRepository
có nội dung như sau:
// src/test/java/com/example/demo/repository/MovieRepositoryTest.java
package com.example.demo.repository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class MovieRepositoryTest {
@Autowired
private MovieRepository movieRepository;
@Test
public void testSaveAll() {
long movieCount = movieRepository.count();
// Khởi tạo dữ liệu
if (0 == movieCount) {
Actor actor1 = new Actor("Ngô Kinh", "Trung Quốc", 1974);
Actor actor2 = new Actor("Lư Tĩnh Shan", "Trung Quốc", 1985);
Actor actor3 = new Actor("Gã Dịu", "Trung Quốc", 1957);
List<Movie> movies = List.of(
new Movie("Chiến Lang II", 2017, List.of(new Role("Lạnh Phong", actor1), new Role("Rachel", actor2))),
new Movie("Thái Cực Tông Sư", 1998, List.of(new Role("Dương Dật Quân", actor1))),
new Movie("Hành Tinh Đất II", 2023, List.of(new Role("Lưu Bồi Cường", actor1))),
new Movie("Tôi Và Quê Hương Của Tôi", 2020, List.of(new Role("EMMA MEIER", actor2), new Role("Trương Bắc Kinh", actor3)))
);
movieRepository.saveAll(movies);
}
}
@Test
public void testFindByName() {
List<Movie> movies = movieRepository.findByName("Chiến Lang II");
System.out.println(movies);
}
@Test
public void testFindMovieNamesByActorName() {
List<String> movieNames = movieRepository.findMovieNamesByActorName("Ngô Kinh");
System.out.println(movieNames);
}
}
Phương thức testSaveAll()
gọi movieRepository.saveAll()
để lưu trữ một nhóm Movie kèm thông tin mối quan hệ. Điều này tương đương với câu lệnh Cypher sau:
CREATE
(a1:Actor {name: "Ngô Kinh", nationality: "Trung Quốc", yearOfBirth: 1974}),
(a2:Actor {name: "Lư Tĩnh Shan", nationality: "Trung Quốc", yearOfBirth: 1985}),
(a3:Actor {name: "Gã Dịu", nationality: "Trung Quốc", yearOfBirth: 1957}),
(m1:Movie {name: "Chiến Lang II", releasedAt: 2017}),
(m2:Movie {name: "Thái Cực Tông Sư", releasedAt: 1998}),
(m3:Movie {name: "Hành Tinh Đất II", releasedAt: 2023}),
(m4:Movie {name: "Tôi Và Quê Hương Của Tôi", releasedAt: 2020}),
(a1)-[:ACTED_IN {role: "Lạnh Phong"}]->(m1),
(a1)-[:ACTED_IN {role: "Dương Dật Quân"}]->(m2),
(a1)-[:ACTED_IN {role: "Lưu Bồi Cường"}]->(m3),
(a2)-[:ACTED_IN {role: "Rachel"}]->(m1),
(a2)-[:ACTED_IN {role: "EMMA MEIER"}]->(m4),
(a3)-[:ACTED_IN {role: "Trương Bắc Kinh"}]->(m4);
Sau khi thực thi phương pháp lưu trữ trên, sử dụng câu lệnh Cypher sau để truy vấn toàn bộ mối quan hệ “Diễn viên - Tham gia -> Phim”:
MATCH (a:Actor)-[r:ACTED_IN]->(m:Movie)
RETURN a, r, m
Kết quả thu được như sau: ![Biểu đồ mối quan hệ “Diễn viên - Tham gia -> Phim”]
Các phương thức khác gọi movieRepository.findByName()
và movieRepository.findMovieNamesByActorName()
, kết quả xuất ra đúng như mong đợi.
Các lớp kiểm thử đơn vị khác như ActorRepositoryTest
và ActorMovieServiceTest
sẽ không được mở rộng tại đây.
3 Kết luận
Tóm lại, chúng ta đã khám phá cách sử dụng Spring Data Neo4j để truy cập cơ sở dữ liệu Neo4j, cảm nhận được sự tiện lợi của việc tích hợp với khung Spring Boot, chỉ cần thêm một Starter là đủ. Về mặt sử dụng, Spring Data Neo4j tuân theo tư tưởng thiết kế thống nhất của Spring Data. Chúng ta có thể sử dụng chú thích để ánh xạ lớp Java với thực thể cơ sở dữ liệu Neo4j; có thể kế thừa giao diện Repository để thực hiện các chức năng tăng-xóa-sửa-truy vấn cơ bản, và sử dụng chú thích @Query
để tự viết câu lệnh Cypher; cũng có thể sử dụng Neo4jTemplate
để tự viết truy vấn và thao tác với cơ sở dữ liệu Neo4j. Ngoài ra, Spring Data Neo4j còn cung cấp hai cách truy vấn bổ sung là Query by Example và Cypher DSL để truy vấn cơ sở dữ liệu Neo4j.
Mã nguồn đầy đủ của ví dụ này đã được gửi lên GitHub, mời bạn quan tâm hoặc Fork.
[1] Spring: Tài liệu tham khảo Spring Data Neo4j - [2] Spring: Truy cập dữ liệu Neo4j với REST -
#Spring #Java #Neo4j