Tích Hợp Database trong Spring Boot với JPA & PostgreSQL

Giới Thiệu
Tích hợp database là một khía cạnh quan trọng của hầu hết các ứng dụng Spring Boot. Spring Data JPA kết hợp với Hibernate ORM cung cấp một cách mạnh mẽ và tinh tế để tương tác với cơ sở dữ liệu quan hệ mà không cần viết mã SQL boilerplate.
Trong hướng dẫn toàn diện này, chúng ta sẽ học:
- Cài đặt Spring Data JPA với PostgreSQL
- Entity mapping và relationships
- Repository patterns và custom queries
- Database migrations với Flyway
- Best practices cho ứng dụng production
Yêu cầu: Tutorial này xây dựng dựa trên Bắt Đầu với Spring Boot. Hãy đảm bảo bạn đã cài đặt Java và Spring Boot trước khi tiếp tục.
Tại Sao Dùng Spring Data JPA?
Spring Data JPA đơn giản hóa việc truy cập database bằng cách:
- Giảm boilerplate: Không cần viết CRUD operations thủ công
- Type safety: Kiểm tra compile-time cho queries
- Tự động implementation: Repository interfaces được implement tự động
- Query methods mạnh mẽ: Tạo queries từ tên method
- Tích hợp: Tích hợp liền mạch với transaction management của Spring
Cài Đặt Database
1. Cài PostgreSQL
macOS (Homebrew):
brew install postgresql@15
brew services start postgresql@15Ubuntu/Debian:
sudo apt update
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresqlWindows: Tải và cài đặt từ postgresql.org
2. Tạo Database
# Kết nối PostgreSQL
psql postgres
# Tạo database và user
CREATE DATABASE springboot_demo;
CREATE USER demo_user WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE springboot_demo TO demo_user;
# Thoát
\q3. Thêm Dependencies
Cập nhật pom.xml:
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Flyway cho migrations (tùy chọn nhưng nên dùng) -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
</dependencies>Với Gradle (build.gradle):
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.flywaydb:flyway-core'
}4. Cấu Hình Kết Nối Database
Sửa src/main/resources/application.properties:
# Database Connection
spring.datasource.url=jdbc:postgresql://localhost:5432/springboot_demo
spring.datasource.username=demo_user
spring.datasource.password=secure_password
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA/Hibernate Properties
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Flyway Migration
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migrationCác tùy chọn cấu hình quan trọng:
ddl-auto=validate: Chỉ validate schema, không tự tạo (khuyên dùng cho production)ddl-auto=create: Drop và tạo lại schema khi khởi động (chỉ dùng development)ddl-auto=update: Tự động cập nhật schema (không khuyên dùng production)show-sql=true: Log SQL statements (tắt ở production)
Tạo Entity Đầu Tiên
Hãy tạo entity User để đại diện cho users trong database.
1. Tạo Entity Class
src/main/java/com/example/demo/entity/User.java:
package com.example.demo.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50)
@Column(unique = true, nullable = false)
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
@Column(unique = true, nullable = false)
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8)
@Column(nullable = false)
private String password;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(nullable = false)
private Boolean active = true;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Constructors
public User() {}
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
// Getters và Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
}Các Annotation Chính:
@Entity: Đánh dấu class là JPA entity@Table: Chỉ định tên table@Id: Trường primary key@GeneratedValue: Chiến lược auto-increment@Column: Tùy chỉnh column (tên, constraints)@CreationTimestamp: Tự động set thời gian tạo@UpdateTimestamp: Tự động cập nhật thời gian sửa
Tạo Repository
Spring Data JPA repositories cung cấp các database operations mà không cần viết implementation code.
src/main/java/com/example/demo/repository/UserRepository.java:
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Derived query methods (tự động implement)
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
List<User> findByActiveTrue();
List<User> findByFirstNameContainingIgnoreCase(String firstName);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
// Custom JPQL query
@Query("SELECT u FROM User u WHERE u.createdAt >= :date")
List<User> findUsersCreatedAfter(@Param("date") LocalDateTime date);
// Native SQL query
@Query(value = "SELECT * FROM users WHERE active = true AND created_at >= :date",
nativeQuery = true)
List<User> findActiveUsersCreatedAfter(@Param("date") LocalDateTime date);
// Count queries
long countByActiveTrue();
}Quy Ước Đặt Tên Query Method:
findBy...: Lấy entitiesexistsBy...: Kiểm tra tồn tại (trả về boolean)countBy...: Đếm entities khớpdeleteBy...: Xóa entities khớp
Từ khóa:
And,Or: Kết hợp điều kiệnBetween: Queries theo rangeLessThan,GreaterThan: So sánhLike,Containing: Khớp chuỗiIgnoreCase: Không phân biệt hoa thườngOrderBy: Sắp xếp
Tạo Service Layer
src/main/java/com/example/demo/service/UserService.java:
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(User user) {
// Kiểm tra username đã tồn tại
if (userRepository.existsByUsername(user.getUsername())) {
throw new IllegalArgumentException("Username already exists");
}
// Kiểm tra email đã tồn tại
if (userRepository.existsByEmail(user.getEmail())) {
throw new IllegalArgumentException("Email already exists");
}
// Trong production, hash password trước khi lưu
// user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
@Transactional(readOnly = true)
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
@Transactional(readOnly = true)
public Optional<User> getUserByUsername(String username) {
return userRepository.findByUsername(username);
}
@Transactional(readOnly = true)
public List<User> getAllActiveUsers() {
return userRepository.findByActiveTrue();
}
@Transactional(readOnly = true)
public List<User> searchUsers(String name) {
return userRepository.findByFirstNameContainingIgnoreCase(name);
}
public User updateUser(Long id, User updatedUser) {
User user = userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
user.setFirstName(updatedUser.getFirstName());
user.setLastName(updatedUser.getLastName());
user.setEmail(updatedUser.getEmail());
return userRepository.save(user);
}
public void deactivateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
user.setActive(false);
userRepository.save(user);
}
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@Transactional(readOnly = true)
public long countActiveUsers() {
return userRepository.countByActiveTrue();
}
}Quản Lý Transaction:
@Transactional: Tự động xử lý transactionreadOnly = true: Tối ưu operations chỉ đọc- Transactions tự động rollback khi có exceptions
Tạo REST API Endpoints
src/main/java/com/example/demo/controller/UserController.java:
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<User>> getAllActiveUsers() {
List<User> users = userService.getAllActiveUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/search")
public ResponseEntity<List<User>> searchUsers(@RequestParam String name) {
List<User> users = userService.searchUsers(name);
return ResponseEntity.ok(users);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id,
@Valid @RequestBody User user) {
User updatedUser = userService.updateUser(id, user);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/count")
public ResponseEntity<Long> countActiveUsers() {
long count = userService.countActiveUsers();
return ResponseEntity.ok(count);
}
}Database Migrations với Flyway
Flyway quản lý các phiên bản schema và migrations một cách an toàn.
1. Tạo Migration Files
Tạo thư mục: src/main/resources/db/migration/
V1__Create_users_table.sql:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_active ON users(active);V2__Add_user_roles.sql:
CREATE TABLE roles (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description VARCHAR(255)
);
CREATE TABLE user_roles (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);
INSERT INTO roles (name, description) VALUES
('USER', 'Regular user'),
('ADMIN', 'Administrator');2. Quy Ước Đặt Tên Migration
Format: V{version}__{description}.sql
V1__Initial_schema.sqlV2__Add_indexes.sqlV3__Add_user_roles.sql
Flyway thực thi migrations theo thứ tự và theo dõi các migrations đã áp dụng trong bảng flyway_schema_history.
Entity Relationships
One-to-Many Relationship
Post.java:
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User author;
@CreationTimestamp
private LocalDateTime createdAt;
// Getters và setters
}Cập nhật User.java:
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Post> posts = new ArrayList<>();Many-to-Many Relationship
@Entity
@Table(name = "users")
public class User {
// ... các fields hiện tại
@ManyToMany
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "roles")
private Set<User> users = new HashSet<>();
}Fetch Types:
LAZY: Chỉ load relationship khi được truy cập (mặc định cho collections)EAGER: Load relationship ngay lập tức (có thể gây N+1 query problems)
Testing Database Operations
src/test/java/com/example/demo/repository/UserRepositoryTest.java:
@SpringBootTest
@Transactional
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void testCreateAndFindUser() {
User user = new User("testuser", "[email protected]", "password123");
user.setFirstName("Test");
user.setLastName("User");
User savedUser = userRepository.save(user);
assertNotNull(savedUser.getId());
Optional<User> foundUser = userRepository.findByUsername("testuser");
assertTrue(foundUser.isPresent());
assertEquals("[email protected]", foundUser.get().getEmail());
}
@Test
void testFindActiveUsers() {
userRepository.save(new User("user1", "[email protected]", "pass"));
User inactiveUser = new User("user2", "[email protected]", "pass");
inactiveUser.setActive(false);
userRepository.save(inactiveUser);
List<User> activeUsers = userRepository.findByActiveTrue();
assertEquals(1, activeUsers.size());
assertEquals("user1", activeUsers.get(0).getUsername());
}
}Best Practices
1. Dùng DTOs cho API Responses
Không expose entities trực tiếp:
public class UserDTO {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;
// Không có password field!
// Constructor, getters, setters
}2. Xử Lý N+1 Query Problems
Dùng @EntityGraph hoặc JOIN FETCH:
@Query("SELECT u FROM User u LEFT JOIN FETCH u.posts WHERE u.id = :id")
Optional<User> findByIdWithPosts(@Param("id") Long id);3. Dùng Projections cho Read Operations
public interface UserSummary {
Long getId();
String getUsername();
String getEmail();
}
List<UserSummary> findAllBy();4. Connection Pooling
Cấu hình HikariCP trong application.properties:
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=200005. Không Bao Giờ Lưu Plain Passwords
Dùng PasswordEncoder của Spring Security:
@Autowired
private PasswordEncoder passwordEncoder;
user.setPassword(passwordEncoder.encode(rawPassword));6. Dùng Indexing Hợp Lý
Thêm indexes cho các columns thường xuyên query:
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);7. Pagination cho Kết Quả Lớn
Page<User> findByActiveTrue(Pageable pageable);
// Sử dụng
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<User> users = userRepository.findByActiveTrue(pageable);Các Vấn Đề Thường Gặp và Giải Pháp
Vấn đề: LazyInitializationException
Nguyên nhân: Truy cập lazy-loaded relationship bên ngoài transaction.
Giải pháp: Dùng @Transactional hoặc fetch eagerly khi cần.
Vấn đề: N+1 Query Problem
Nguyên nhân: Một query cho parent, N queries cho children.
Giải pháp: Dùng JOIN FETCH hoặc @EntityGraph.
Vấn đề: Database Lock Timeouts
Nguyên nhân: Transactions chạy quá lâu.
Giải pháp: Giữ transactions ngắn, dùng optimistic locking.
Kết Luận
Spring Data JPA với Hibernate cung cấp abstraction mạnh mẽ cho database operations trong Spring Boot applications. Những điểm chính:
- Dùng Spring Data JPA repositories cho CRUD operations tự động
- Tận dụng Flyway cho database migrations
- Tuân theo best practices: dùng DTOs, xử lý lazy loading, implement pagination
- Test kỹ database operations
- Monitor và tối ưu query performance
Trong tutorial tiếp theo, chúng ta sẽ học về tích hợp Spring Security để bảo mật các endpoints này với authentication và authorization.
Tài Liệu Tham Khảo
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.