05 tháng 5 năm 2025 - Máy tính
Bài viết này sẽ trình bày chi tiết về cách sử dụng Spring Data để truy cập đồng thời vào hai cơ sở dữ liệu MySQL và Neo4j. Nội dung bao gồm cấu hình nhiều nguồn dữ liệu trong Spring Boot, quản lý giao dịch cho cả hai hệ thống, cũng như cách sử dụng các nhóm Repository khác nhau.
Để làm cho dự án minh họa gần với thực tế hơn, chúng ta sẽ đặt ra một kịch bản cụ thể: việc di chuyển dữ liệu từ MySQL sang Neo4j. Về mặt kỹ thuật, chúng ta sẽ sử dụng hai nhóm Repository để đọc và ghi dữ liệu, cũng như chuyển đổi bảng từ cơ sở dữ liệu quan hệ thành nút (node) và mối liên kết (relationship) trong Neo4j.
Trước khi đi sâu vào cấu trúc của dự án và các đoạn mã chính, hãy xem qua chức năng mà dự án này có thể thực hiện:
1. Hiển thị Chức năng
Dự án này thực hiện việc di chuyển dữ liệu từ ba bảng trong MySQL sang Neo4j, nơi chúng trở thành các node và relationship.
Ba bảng trong MySQL là:
- actor (diễn viên)
- movie (phim)
- actor_movie (mối liên hệ giữa diễn viên và phim)
Câu lệnh tạo bảng và chèn dữ liệu như sau:
CREATE TABLE actor (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
nationality VARCHAR(100) NOT NULL,
year_of_birth INT NOT NULL
);
CREATE TABLE movie (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
released_at INT NOT NULL
);
CREATE TABLE actor_movie (
actor_id BIGINT NOT NULL,
movie_id BIGINT NOT NULL,
role VARCHAR(100) NOT NULL,
PRIMARY KEY (actor_id, movie_id)
);
INSERT INTO actor (name, nationality, year_of_birth)
VALUES ('Ngô Kinh', 'Trung Quốc', 1974),
('Lư Tĩnh Sương', 'Trung Quốc', 1985);
INSERT INTO movie (name, released_at)
VALUES ('Chiến Lang II', 2017),
('Thái Cực Tông Sư', 1998),
('Hành Tinh Drift II', 2023),
('Tôi Và Quê Hương Tôi', 2020);
INSERT INTO actor_movie (actor_id, movie_id, role)
VALUES (1, 1, 'Lãnh Phong'),
(1, 2, 'Dương Dật Khôn'),
(1, 3, 'Lưu Bồi Cường'),
(2, 1, 'Rachel'),
(2, 4, 'EMMA MEIER');
Sau khi di chuyển dữ liệu, các node và relationship trong Neo4j sẽ trông như sau: 
Tiếp theo, chúng ta sẽ phân tích cấu trúc và các đoạn mã quan trọng của dự án này.
g88.vin 2. Cấu trúc Dự án và Phân tích Mã Nguồn
Dự án mẫu này được quản lý bởi Maven và sử dụng Spring Boot. Các phiên bản phụ thuộc chính như sau:
Java: Liberica JDK 17.0.7
Maven: 3.9.2
Spring Boot: 3.4.5
2.1. Cấu trúc Dự án và Phụ thuộc
Cấu trúc của dự án như sau:
spring-data-jpa-and-neo4j-demo
├─ src
│ ├─ main
│ │ ├─ java
│ │ │ └─ com.example.demo
│ │ │ ├─ config
│ │ │ │ ├─ MySQLConfig.java
│ │ │ │ └─ Neo4jConfig.java
│ │ │ ├─ repository
│ │ │ │ ├─ graph
│ │ │ │ │ ├─ GraphActorRepository.java
│ │ │ │ │ └─ GraphMovieRepository.java
│ │ │ │ └─ relational
│ │ │ │ │ ├─ ActorRepository.java
│ │ │ │ │ ├─ MovieRepository.java
│ │ │ │ │ └─ ActorMovieRepository.java
│ │ │ ├─ service
│ │ │ │ ├─ MigrationService.java
│ │ │ │ └─ impl
│ │ │ │ └─ MigrationServiceImpl.java
│ │ │ ├─ model
│ │ │ │ ├─ graph
│ │ │ │ │ ├─ GraphActor.java
│ │ │ │ │ └─ GraphMovie.java
│ │ │ │ └─ relational
│ │ │ │ │ ├─ Actor.java
│ │ │ │ │ ├─ Movie.java
│ │ │ │ │ └─ ActorMovie.java
│ │ │ └─ DemoApplication.java
│ │ └─ resources
│ │ └─ application.yaml
│ └─ test
│ └─ java
│ └─ com.example.demo
│ └─ service
│ └─ MigrationServiceTest.java
└─ pom.xml
Như có thể thấy, đây là một dự án chuẩn Maven, với DemoApplication.java
đóng vai trò là lớp khởi chạy và application.yaml
là tệp cấu hình. Gói config
chứa các lớp cấu hình, chẳng hạn như MySQLConfig.java
và Neo4jConfig.java
, được dùng để thiết lập thông tin kết nối và quản lý giao dịch cho MySQL và Neo4j.
Gói repository
chứa các giao diện Repository để tương tác với cơ sở dữ liệu. Trong đó, gói con relational
chứa các Repository dành cho MySQL, còn gói con graph
chứa các Repository dành cho Neo4j.
Gói model
chứa các lớp mô hình (Model). Gói con relational
chứa các Model tương ứng với bảng MySQL, trong khi gói con graph
chứa các Model tương ứng với các node trong Neo4j.
Ngoài ra, gói service
chứa các lớp dịch vụ, bao gồm MigrationService.java
và triển khai của nó, thực hiện nhiệm vụ di chuyển dữ liệu từ MySQL sang Neo4j.
Dưới đây là danh sách các phụ thuộc cần thiết trong 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-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.2.0</version>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Chúng ta có thể thấy rằng dự án này chủ yếu dựa vào hai phụ thuộc chính: spring-boot-starter-data-jpa
(dùng để truy cập MySQL) và spring-boot-starter-data-neo4j
(dùng để truy cập Neo4j). Ngoài ra, lombok
giúp đơn giản hóa việc viết Getters và Setters, mysql-connector-j
là trình điều khiển cho MySQL, và spring-boot-starter-test
hỗ trợ kiểm thử đơn vị.
2.2. Cấu hình Dự án
Tệp cấu hình application.yaml
có nội dung như sau:
spring:
datasource:
jdbc-url: jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
jpa:
show-sql: true
neo4j:
uri: bolt://localhost:7687
authentication:
username: neo4j
password: neo4j
logging:
level:
org.neo4j.ogm: DEBUG
org.springframework.data.neo4j: DEBUG
Như vậy, chúng ta đã cấu hình hai nguồn dữ liệu: spring.datasource
dành cho thông tin kết nối MySQL, và spring.neo4j
dành cho thông tin kết nối Neo4j. Bên cạnh đó, chúng ta cũng bật chế độ in câu lệnh SQL và Cypher của Neo4j.
Sau khi tìm hiểu cấu trúc, phụ thuộc và cấu hình, bây giờ chúng ta sẽ phân tích các đoạn mã quan trọng.
2.3. Lớp Config
Để hỗ trợ thao tác đồng thời trên MySQL và Neo4j trong Spring Boot, lớp cấu hình đóng vai trò then chốt.
Dưới đây là mã nguồn của MySQLConfig.java
:
package com.example.demo.config;
// ...
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "com.example.demo.repository.relational",
entityManagerFactoryRef = "mysqlEntityManagerFactory",
transactionManagerRef = "mysqlTransactionManager"
)
public class MySQLConfig {
@Bean(name = "mysqlDataSource")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource mysqlDataSource() [888bets](https://www.kongken.com) {
return DataSourceBuilder.create().build();
}
@Bean(name = "mysqlEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean mysqlEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("mysqlDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.example.demo.model.relational")
.persistenceUnit("mysql")
.build();
}
@Bean(name = "mysqlTransactionManager")
public PlatformTransactionManager mysqlTransactionManager(
@Qualifier("mysqlEntityManagerFactory") EntityManagerFactory factory) {
return new JpaTransactionManager(factory);
}
}
Như có thể thấy, trong lớp này, chúng ta chỉ định vị trí của các Repository cho MySQL, thông tin kết nối trong tệp cấu hình, và cấu hình quản lý thực thể và giao dịch cho MySQL.
Tiếp theo là mã nguồn của Neo4jConfig.java
:
package [Sam86 Club Choi Game Bài](/post/4011/) com.example.demo.config;
// ...
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories(
basePackages = "com.example.demo.repository.graph",
transactionManagerRef = "neo4jTransactionManager"
)
public class Neo4jConfig {
@Bean(name = "neo4jTransactionManager")
public PlatformTransactionManager transactionManager(Driver driver) {
return Neo4jTransactionManager.with(driver).build();
}
}
Trong lớp cấu hình này, chúng ta chỉ định vị trí của các Repository cho Neo4j và cấu hình quản lý giao dịch cho Neo4j.
2.4. Lớp Model
Lớp Model được sử dụng để ánh xạ bảng trong MySQL hoặc node trong Neo4j.
Dưới đây là mã nguồn của Actor.java
, tương ứng với bảng actor
trong MySQL:
package com.example.demo.model.relational;
// ...
@Data
@Entity(name = "actor")
public class Actor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long actorId;
private String name;
private String nationality;
private Integer yearOfBirth;
}
Dưới đây là mã nguồn của GraphActor.java
, tương ứng với node Actor
trong Neo4j:
package com.example.demo.model.graph;
// ...
@Data
@Node("Actor")
public class GraphActor {
@Id
@GeneratedValue
private Long id;
private Long actorId;
private String name;
private String nationality;
private Integer yearOfBirth;
}
Các Model khác trong gói relational
hoặc graph
tương tự như hai ví dụ trên, vì vậy chúng tôi sẽ không liệt kê hết ở đây.
2.5. Giao diện Repository
Repository được sử dụng để tương tác trực tiếp với cơ sở dữ liệu.
Dưới đây là mã nguồn của ActorRepository.java
, dùng để thực hiện các thao tác CRUD trên bảng actor
trong MySQL:
package com.example.demo.repository.relational;
// ...
public interface ActorRepository extends JpaRepository<Actor, Long> {}
Dưới đây là mã nguồn của GraphActorRepository.java
, dùng để thực hiện các thao tác CRUD trên node Actor
trong Neo4j:
package com.example.demo.repository.graph;
// ...
public interface GraphActorRepository extends Neo4jRepository<GraphActor, Long> {
@Transactional("neo4jTransactionManager")
@Query("""
UNWIND $actors AS actor
MERGE (a:Actor {actorId: actor.actorId})
ON CREATE SET a = actor
ON MATCH SET a += actor
""")
void batchInsertOrUpdate(List<Map<String, Object>> actors);
}
Như có thể thấy, tương tự như Repository JPA thông thường, Repository Neo4j cũng hỗ trợ việc viết các truy vấn tùy chỉnh. Lý do chúng ta viết phương thức này là vì cách tiếp cận Cypher tùy chỉnh này hiệu quả hơn so với cách tiếp cận mặc định.
Các Repository khác trong gói relational
hoặc graph
tương tự như hai ví dụ trên.
2.6. Lớp Service
Mã nguồn của lớp triển khai MigrationService
như sau, sử dụng các Model và Repository đã đề cập trước đó để thực hiện việc di chuyển dữ liệu từ MySQL sang Neo4j:
package com.example.demo.service.impl;
// ...
@Service
public class MigrationServiceImpl implements MigrationService {
@Autowired
private ActorRepository actorRepository;
@Autowired
private MovieRepository movieRepository;
@Autowired
private ActorMovieRepository actorMovieRepository;
@Autowired
private GraphActorRepository graphActorRepository;
@Autowired
private GraphMovieRepository graphMovieRepository;
@Override
public void migrateActorsAndMovies() {
// Di chuyển tất cả các diễn viên
migrateAllActors();
// Di chuyển tất cả các phim
migrateAllMovies();
// Xóa tất cả mối liên hệ ACTED_IN
graphMovieRepository.deleteAllActedInRelations();
// Tái tạo mối liên hệ ACTED_IN
List<Map<String, Object>> actedInRelations = getAllActedInRelations();
graphMovieRepository.batchInsertOrUpdateActedInRelations(actedInRelations);
}
private void migrateAllActors() {
List<Actor> actors = actorRepository.findAll();
List<Map<String, Object>> graphActors = actors.stream()
.map(this::assembleActor)
.toList();
graphActorRepository.batchInsertOrUpdate(graphActors);
}
private void migrateAllMovies() {
List<Movie> movies = movieRepository.findAll();
List<Map<String, Object>> graphMovies = movies.stream()
.map(this::assembleMovie)
.toList();
graphMovieRepository.batchInsertOrUpdate(graphMovies);
}
private List<Map<String, Object>> getAllActedInRelations() {
List<ActorMovie> actorMovies = actorMovieRepository.findAll();
return actorMovies.stream()
.map(this::assembleActedIn)
.toList();
}
private Map<String, Object> assembleActor(Actor actor) {
GraphActor graphActor = new GraphActor();
BeanUtils.copyProperties(actor, graphActor);
graphActor.setId(null);
return ObjectToMapUtil.toMap(graphActor);
}
private Map<String, Object> assembleMovie(Movie movie) {
GraphMovie graphMovie = new GraphMovie();
BeanUtils.copyProperties(movie, graphMovie);
graphMovie.setId(null);
return ObjectToMapUtil.toMap(graphMovie);
}
private Map<String, Object> assembleActedIn(ActorMovie actorMovie) {
return Map.of(
"actorId", actorMovie.getId().getActorId(),
"movieId", actorMovie.getId().getMovieId(),
"role", actorMovie.getRole()
);
}
}
Như vậy, lớp triển khai này đọc dữ liệu từ MySQL và ghi dữ liệu vào Neo4j, hoàn thành quá trình chuyển đổi mô hình và di chuyển dữ liệu giữa hai cơ sở dữ liệu.
Lưu ý rằng chúng ta sử dụng một công cụ chuyển đổi đối tượng Java thành kiểu Map (ObjectToMapUtil.java
), vì Repository của Neo4j hiện tại chưa hỗ trợ tốt việc truyền trực tiếp một danh sách đối tượng List<Actor>
.
Cuối cùng, sau khi gọi lớp triển khai này trong lớp kiểm thử đơn vị, chúng ta sẽ nhận được kết quả như đã trình bày ở phần đầu bài viết.
3. Kết luận
Như đã thảo luận, chúng ta đã minh họa cách sử dụng Spring Data để truy cập đồng thời vào MySQL và Neo4j, với mục tiêu di chuyển dữ liệu từ MySQL sang Neo4j.
Mã nguồn đầy đủ của dự án mẫu đã được gửi lên GitHub, mời bạn quan tâm hoặc Fork.
[1] Spring: Spring Data JPA - [2] Spring: Tài liệu Tham khảo Spring Data Neo4j -
#Spring #Java #MySQL #Neo4j