DEV Community

Cyrus Tse
Cyrus Tse

Posted on

Spring Boot 4 入門教學 - Part 6 (JDBC 資料庫連接)

第六章:JDBC 資料庫連接

6.1 前言:資料庫連接的基礎

在這一章節中,我們將學習如何使用 Spring Boot 連接到 PostgreSQL 資料庫,並執行基本的資料庫操作。資料庫連接是大多數 Web 應用程式的核心功能之一,Spring Boot 提供了多種方式來簡化這個過程。

我們將使用 Spring 的 JdbcTemplate 來進行資料庫操作,這是一個輕量級且靈活的工具,適合學習和理解 JDBC 的運作原理。

6.2 添加 PostgreSQL 依賴

首先,我們需要在 pom.xml 中添加 PostgreSQL 驅動依賴。如果你使用 Spring Initializr 時已經添加了「PostgreSQL Driver」,則不需要手動添加。

<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot Starter JDBC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <!-- PostgreSQL Driver -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- 其他依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

6.3 配置資料來源

在 application.properties 中配置 PostgreSQL 連接:

# src/main/resources/application.properties

# PostgreSQL 資料庫連接配置
spring.datasource.url=jdbc:postgresql://localhost:5432/myapp_db
spring.datasource.username=myapp_user
spring.datasource.password=your_password
spring.datasource.driver-class-name=org.postgresql.Driver

# 連接池配置(HikariCP)
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000

# JPA/Hibernate 配置(如果你使用 JPA)
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

# 伺服器端口
server.port=8080

# 日誌配置
logging.level.com.zaxxer.hikari=INFO
logging.level.org.springframework.jdbc=DEBUG
Enter fullscreen mode Exit fullscreen mode

或者使用 application.yml 格式:

# src/main/resources/application.yml

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/myapp_db
    username: myapp_user
    password: your_password
    driver-class-name: org.postgresql.Driver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.PostgreSQLDialect

server:
  port: 8080

logging:
  level:
    com.zaxxer.hikari: INFO
    org.springframework.jdbc: DEBUG
Enter fullscreen mode Exit fullscreen mode

6.4 創建資料表

登入 PostgreSQL 並創建用戶表:

-- 登入 PostgreSQL
psql -U myapp_user -d myapp_db

-- 創建 users 表
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    full_name VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    active BOOLEAN DEFAULT TRUE
);

-- 創建索引
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);

-- 插入測試數據
INSERT INTO users (username, email, password, full_name, active) VALUES
('john', 'john@example.com', 'password123', 'John Doe', TRUE),
('jane', 'jane@example.com', 'password456', 'Jane Smith', TRUE),
('bob', 'bob@example.com', 'password789', 'Bob Johnson', FALSE);

-- 查詢數據
SELECT * FROM users;
Enter fullscreen mode Exit fullscreen mode

6.5 使用 JdbcTemplate 操作資料庫

讓我們創建一個完整的 User 實體和 Repository 類:

package com.example.myfirstapp.entity;

import java.time.LocalDateTime;

/**
 * User 實體類
 * 對應資料庫中的 users 表
 */
public class User {

    private Long id;
    private String username;
    private String email;
    private String password;
    private String fullName;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private Boolean active;

    // 構造函數
    public User() {}

    public User(Long id, String username, String email, String password,
                String fullName, LocalDateTime createdAt, LocalDateTime updatedAt, Boolean active) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.password = password;
        this.fullName = fullName;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.active = active;
    }

    // Getters and 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 getFullName() { return fullName; }
    public void setFullName(String fullName) { this.fullName = fullName; }

    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }

    public LocalDateTime getUpdatedAt() { return updatedAt; }
    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }

    public Boolean isActive() { return active; }
    public void setActive(Boolean active) { this.active = active; }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", email='" + email + '\'' +
                ", fullName='" + fullName + '\'' +
                ", active=" + active +
                '}';
    }
}
Enter fullscreen mode Exit fullscreen mode
package com.example.myfirstapp.repository;

import com.example.myfirstapp.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
public class UserRepository {

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * 查詢所有用戶
     */
    public List<User> findAll() {
        String sql = "SELECT id, username, email, password, full_name, " +
                     "created_at, updated_at, active FROM users ORDER BY id";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
    }

    /**
     * 根據 ID 查詢用戶
     */
    public Optional<User> findById(Long id) {
        String sql = "SELECT id, username, email, password, full_name, " +
                     "created_at, updated_at, active FROM users WHERE id = ?";
        try {
            User user = jdbcTemplate.queryForObject(sql,
                new BeanPropertyRowMapper<>(User.class), id);
            return Optional.ofNullable(user);
        } catch (Exception e) {
            return Optional.empty();
        }
    }

    /**
     * 根據用戶名查詢用戶
     */
    public Optional<User> findByUsername(String username) {
        String sql = "SELECT id, username, email, password, full_name, " +
                     "created_at, updated_at, active FROM users WHERE username = ?";
        try {
            User user = jdbcTemplate.queryForObject(sql,
                new BeanPropertyRowMapper<>(User.class), username);
            return Optional.ofNullable(user);
        } catch (Exception e) {
            return Optional.empty();
        }
    }

    /**
     * 根據電子郵件查詢用戶
     */
    public Optional<User> findByEmail(String email) {
        String sql = "SELECT id, username, email, password, full_name, " +
                     "created_at, updated_at, active FROM users WHERE email = ?";
        try {
            User user = jdbcTemplate.queryForObject(sql,
                new BeanPropertyRowMapper<>(User.class), email);
            return Optional.ofNullable(user);
        } catch (Exception e) {
            return Optional.empty();
        }
    }

    /**
     * 檢查用戶名是否存在
     */
    public boolean existsByUsername(String username) {
        String sql = "SELECT COUNT(*) FROM users WHERE username = ?";
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class, username);
        return count != null && count > 0;
    }

    /**
     * 檢查電子郵件是否存在
     */
    public boolean existsByEmail(String email) {
        String sql = "SELECT COUNT(*) FROM users WHERE email = ?";
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class, email);
        return count != null && count > 0;
    }

    /**
     * 插入新用戶
     */
    public User save(User user) {
        if (user.getId() == null) {
            // 插入新用戶
            String sql = "INSERT INTO users (username, email, password, full_name, " +
                         "created_at, updated_at, active) VALUES (?, ?, ?, ?, ?, ?, ?)";
            LocalDateTime now = LocalDateTime.now();
            jdbcTemplate.update(sql,
                user.getUsername(),
                user.getEmail(),
                user.getPassword(),
                user.getFullName(),
                now,
                now,
                user.isActive() != null ? user.isActive() : true
            );

            // 獲取生成的 ID
            String currSql = "SELECT currval(pg_get_serial_sequence('users', 'id'))";
            Long id = jdbcTemplate.queryForObject(currSql, Long.class);
            user.setId(id);
            user.setCreatedAt(now);
            user.setUpdatedAt(now);
        } else {
            // 更新現有用戶
            String sql = "UPDATE users SET username = ?, email = ?, password = ?, " +
                         "full_name = ?, updated_at = ?, active = ? WHERE id = ?";
            jdbcTemplate.update(sql,
                user.getUsername(),
                user.getEmail(),
                user.getPassword(),
                user.getFullName(),
                LocalDateTime.now(),
                user.isActive(),
                user.getId()
            );
            user.setUpdatedAt(LocalDateTime.now());
        }
        return user;
    }

    /**
     * 刪除用戶
     */
    public void deleteById(Long id) {
        String sql = "DELETE FROM users WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }

    /**
     * 根據名稱搜索用戶
     */
    public List<User> searchByName(String name) {
        String sql = "SELECT id, username, email, password, full_name, " +
                     "created_at, updated_at, active FROM users " +
                     "WHERE username ILIKE ? OR full_name ILIKE ? OR email ILIKE ? " +
                     "ORDER BY id";
        String searchPattern = "%" + name + "%";
        return jdbcTemplate.query(sql,
            new BeanPropertyRowMapper<>(User.class),
            searchPattern, searchPattern, searchPattern);
    }

    /**
     * 統計用戶數量
     */
    public int count() {
        String sql = "SELECT COUNT(*) FROM users";
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
        return count != null ? count : 0;
    }

    /**
     * 查詢活躍用戶數量
     */
    public int countActive() {
        String sql = "SELECT COUNT(*) FROM users WHERE active = TRUE";
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
        return count != null ? count : 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

6.6 Service 層實作

package com.example.myfirstapp.service;

import com.example.myfirstapp.config.ResourceNotFoundException;
import com.example.myfirstapp.entity.User;
import com.example.myfirstapp.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public List<User> findAll() {
        return userRepository.findAll();
    }

    public User findById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User", id));
    }

    public User findByUsername(String username) {
        return userRepository.findByUsername(username)
            .orElseThrow(() -> new ResourceNotFoundException("User not found: " + username));
    }

    public List<User> searchByName(String name) {
        return userRepository.searchByName(name);
    }

    public User save(User user) {
        return userRepository.save(user);
    }

    public void deleteById(Long id) {
        if (!userRepository.findById(id).isPresent()) {
            throw new ResourceNotFoundException("User", id);
        }
        userRepository.deleteById(id);
    }

    public boolean existsByUsername(String username) {
        return userRepository.existsByUsername(username);
    }

    public boolean existsByEmail(String email) {
        return userRepository.existsByEmail(email);
    }

    public int count() {
        return userRepository.count();
    }

    public int countActive() {
        return userRepository.countActive();
    }
}
Enter fullscreen mode Exit fullscreen mode

6.7 完整的 Controller 實作

package com.example.myfirstapp.controller;

import com.example.myfirstapp.dto.ApiResponse;
import com.example.myfirstapp.dto.CreateUserRequest;
import com.example.myfirstapp.dto.UserResponse;
import com.example.myfirstapp.entity.User;
import com.example.myfirstapp.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public ResponseEntity<ApiResponse<List<UserResponse>>> getAllUsers() {
        List<User> users = userService.findAll();
        List<UserResponse> responses = users.stream()
            .map(this::convertToResponse)
            .collect(Collectors.toList());
        return ResponseEntity.ok(ApiResponse.success(responses));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<UserResponse>> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(ApiResponse.success(convertToResponse(user)));
    }

    @PostMapping
    public ResponseEntity<ApiResponse<UserResponse>> createUser(
            @Valid @RequestBody CreateUserRequest request) {

        // 檢查用戶名是否存在
        if (userService.existsByUsername(request.getUsername())) {
            return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error("用戶名已存在"));
        }

        // 檢查電子郵件是否存在
        if (userService.existsByEmail(request.getEmail())) {
            return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error("電子郵件已存在"));
        }

        // 創建用戶
        User user = new User();
        user.setUsername(request.getUsername());
        user.setEmail(request.getEmail());
        user.setPassword(request.getPassword());
        user.setFullName(request.getFullName());
        user.setActive(true);

        User saved = userService.save(user);
        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(ApiResponse.success(convertToResponse(saved)));
    }

    @PutMapping("/{id}")
    public ResponseEntity<ApiResponse<UserResponse>> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody CreateUserRequest request) {

        User existing = userService.findById(id);

        // 更新用戶信息
        existing.setUsername(request.getUsername());
        existing.setEmail(request.getEmail());
        existing.setPassword(request.getPassword());
        existing.setFullName(request.getFullName());

        User updated = userService.save(existing);
        return ResponseEntity.ok(ApiResponse.success(convertToResponse(updated)));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
        return ResponseEntity.ok(ApiResponse.success(null));
    }

    @GetMapping("/search")
    public ResponseEntity<ApiResponse<List<UserResponse>>> searchUsers(
            @RequestParam(name = "name", required = false) String name) {
        List<User> users;
        if (name != null && !name.isEmpty()) {
            users = userService.searchByName(name);
        } else {
            users = userService.findAll();
        }
        List<UserResponse> responses = users.stream()
            .map(this::convertToResponse)
            .collect(Collectors.toList());
        return ResponseEntity.ok(ApiResponse.success(responses));
    }

    @GetMapping("/count")
    public ResponseEntity<ApiResponse<Integer>> getUserCount() {
        return ResponseEntity.ok(ApiResponse.success(userService.count()));
    }

    @GetMapping("/count/active")
    public ResponseEntity<ApiResponse<Integer>> getActiveUserCount() {
        return ResponseEntity.ok(ApiResponse.success(userService.countActive()));
    }

    private UserResponse convertToResponse(User user) {
        UserResponse response = new UserResponse();
        response.setId(user.getId());
        response.setUsername(user.getUsername());
        response.setEmail(user.getEmail());
        response.setFullName(user.getFullName());
        response.setCreatedAt(user.getCreatedAt());
        response.setStatus(user.isActive() ? "ACTIVE" : "INACTIVE");
        return response;
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)