Skip to content

Spring Boot SQL 数据库集成详解 🗄️

概述

在现代企业级应用开发中,数据库操作是不可或缺的核心功能。Spring Boot 为 SQL 数据库提供了全方位的支持,从最基础的 JDBC 直接访问到完整的对象关系映射(ORM)技术,让开发者能够根据项目需求选择最合适的数据访问方案。

IMPORTANT

Spring Boot 的数据库支持遵循"约定优于配置"的原则,通过自动配置大大简化了数据库集成的复杂度,让开发者能够专注于业务逻辑的实现。

数据源配置 (DataSource Configuration)

核心概念与价值

数据源(DataSource)是 Java 应用与数据库之间的桥梁,它提供了标准化的数据库连接管理方式。在没有 Spring Boot 自动配置之前,开发者需要手动配置连接池、管理连接生命周期、处理连接异常等复杂工作。

TIP

想象一下数据源就像是一个"水龙头管理系统":它不仅要确保有水(数据库连接)可用,还要控制水流大小(连接池大小)、水质(连接有效性)和节约用水(连接复用)。

嵌入式数据库支持

对于开发和测试环境,Spring Boot 提供了开箱即用的嵌入式数据库支持:

kotlin
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("com.h2database:h2") 
    // 或者使用其他嵌入式数据库
    // runtimeOnly("org.hsqldb:hsqldb")
    // runtimeOnly("org.apache.derby:derby")
}
xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope> 
</dependency>

NOTE

嵌入式数据库会在应用启动时自动创建,关闭时自动销毁。如果在测试中需要每个测试上下文使用独立的数据库,可以设置 spring.datasource.generate-unique-name=true

生产环境数据库配置

对于生产环境,我们需要配置真实的数据库连接:

yaml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/myapp
    username: ${DB_USER:dbuser}
    password: ${DB_PASSWORD:dbpass}
    driver-class-name: com.mysql.cj.jdbc.Driver
    
    # HikariCP 连接池配置
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000
      max-lifetime: 1200000
properties
# 基础连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/myapp
spring.datasource.username=${DB_USER:dbuser}
spring.datasource.password=${DB_PASSWORD:dbpass}

# 连接池配置
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5

连接池选择策略

Spring Boot 按以下优先级自动选择连接池:

TIP

HikariCP 因其卓越的性能和并发处理能力被 Spring Boot 优先选择。如果你使用 spring-boot-starter-jdbcspring-boot-starter-data-jpa,会自动包含 HikariCP 依赖。

JDBC 模板使用

JdbcTemplate:简化的数据库操作

JdbcTemplate 是 Spring 对原生 JDBC 的优雅封装,它解决了传统 JDBC 编程中的诸多痛点:

传统 JDBC 的痛点

  • 大量的样板代码(连接管理、异常处理、资源释放)
  • 手动处理 ResultSet 和参数设置
  • 复杂的异常处理和资源泄露风险
kotlin
@Service
class UserService(
    private val jdbcTemplate: JdbcTemplate
) {
    
    fun findUserById(id: Long): User? {
        return try {
            jdbcTemplate.queryForObject( 
                "SELECT id, name, email FROM users WHERE id = ?",
                { rs, _ -> 
                    User(
                        id = rs.getLong("id"),
                        name = rs.getString("name"),
                        email = rs.getString("email")
                    )
                },
                id
            )
        } catch (e: EmptyResultDataAccessException) {
            null
        }
    }
    
    fun createUser(user: User): Int {
        return jdbcTemplate.update( 
            "INSERT INTO users (name, email) VALUES (?, ?)",
            user.name, user.email
        )
    }
    
    fun findUsersByDepartment(department: String): List<User> {
        return jdbcTemplate.query( 
            "SELECT id, name, email FROM users WHERE department = ?",
            { rs, _ -> 
                User(
                    id = rs.getLong("id"),
                    name = rs.getString("name"),
                    email = rs.getString("email")
                )
            },
            department
        )
    }
}

JdbcClient:现代化的数据库客户端

Spring 6.1 引入的 JdbcClient 提供了更加流畅和现代化的 API:

kotlin
@Service
class ModernUserService(
    private val jdbcClient: JdbcClient
) {
    
    fun findUserById(id: Long): User? {
        return jdbcClient
            .sql("SELECT id, name, email FROM users WHERE id = :id") 
            .param("id", id) 
            .query { rs, _ -> 
                User(
                    id = rs.getLong("id"),
                    name = rs.getString("name"),
                    email = rs.getString("email")
                )
            }
            .optional() 
            .orElse(null)
    }
    
    fun createUser(user: User): Int {
        return jdbcClient
            .sql("INSERT INTO users (name, email) VALUES (:name, :email)") 
            .param("name", user.name)
            .param("email", user.email)
            .update() 
    }
    
    fun batchCreateUsers(users: List<User>): IntArray {
        return jdbcClient
            .sql("INSERT INTO users (name, email) VALUES (:name, :email)")
            .params(users.map { mapOf("name" to it.name, "email" to it.email) }) 
            .batchUpdate() 
    }
}

TIP

JdbcClient 相比 JdbcTemplate 的优势:

  • 更流畅的链式 API
  • 内置的命名参数支持
  • 更好的类型安全性
  • 简化的批量操作

JPA 和 Spring Data JPA

理解 JPA 的价值

JPA(Java Persistence API)解决了对象-关系映射(ORM)的标准化问题。在没有 ORM 的时代,开发者需要手写大量的 SQL 语句和对象转换代码。

实体类定义

kotlin
@Entity
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    val id: Long? = null,
    
    @Column(nullable = false, length = 100)
    val name: String,
    
    @Column(nullable = false, unique = true) 
    val email: String,
    
    @Enumerated(EnumType.STRING) 
    val status: UserStatus = UserStatus.ACTIVE,
    
    @CreationTimestamp
    val createdAt: LocalDateTime? = null,
    
    @UpdateTimestamp
    val updatedAt: LocalDateTime? = null,
    
    // 一对多关系
    @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) 
    val orders: List<Order> = emptyList()
) {
    // JPA 要求无参构造函数
    constructor() : this(name = "", email = "")
}

enum class UserStatus {
    ACTIVE, INACTIVE, SUSPENDED
}

Spring Data JPA 仓库

Spring Data JPA 通过方法名约定自动生成查询,大大减少了样板代码:

kotlin
interface UserRepository : JpaRepository<User, Long> {
    
    // 根据方法名自动生成查询
    fun findByEmail(email: String): User? 
    
    fun findByNameContainingIgnoreCase(name: String): List<User> 
    
    fun findByStatusAndCreatedAtAfter(
        status: UserStatus, 
        date: LocalDateTime
    ): List<User> 
    
    // 自定义查询
    @Query("SELECT u FROM User u WHERE u.email LIKE %:domain%") 
    fun findByEmailDomain(@Param("domain") domain: String): List<User>
    
    // 原生 SQL 查询
    @Query(
        value = "SELECT * FROM users WHERE created_at > :date ORDER BY created_at DESC LIMIT :limit",
        nativeQuery = true
    )
    fun findRecentUsers(@Param("date") date: LocalDateTime, @Param("limit") limit: Int): List<User>
    
    // 分页查询
    fun findByStatus(status: UserStatus, pageable: Pageable): Page<User> 
}

服务层实现

kotlin
@Service
@Transactional
class UserService(
    private val userRepository: UserRepository
) {
    
    fun createUser(createRequest: CreateUserRequest): User {
        // 检查邮箱唯一性
        userRepository.findByEmail(createRequest.email)?.let {
            throw IllegalArgumentException("Email already exists") 
        }
        
        val user = User(
            name = createRequest.name,
            email = createRequest.email
        )
        
        return userRepository.save(user) 
    }
    
    @Transactional(readOnly = true) 
    fun findActiveUsers(page: Int, size: Int): Page<User> {
        val pageable = PageRequest.of(page, size, Sort.by("createdAt").descending())
        return userRepository.findByStatus(UserStatus.ACTIVE, pageable)
    }
    
    fun updateUserStatus(userId: Long, status: UserStatus): User {
        val user = userRepository.findById(userId)
            .orElseThrow { EntityNotFoundException("User not found") } 
            
        return userRepository.save(user.copy(status = status)) 
    }
    
    fun searchUsers(keyword: String): List<User> {
        return userRepository.findByNameContainingIgnoreCase(keyword)
    }
}

IMPORTANT

@Transactional 注解确保数据一致性:

  • 默认情况下,运行时异常会触发回滚
  • readOnly = true 可以优化只读操作的性能
  • 可以通过 rollbackFornoRollbackFor 自定义回滚策略

数据库初始化配置

yaml
spring:
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true
        
  # 数据库初始化
  sql:
    init:
      mode: always
      schema-locations: classpath:schema.sql
      data-locations: classpath:data.sql
properties
# 开发环境配置
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.h2.console.enabled=true

WARNING

生产环境中应该使用 ddl-auto=validateddl-auto=none,避免意外的表结构变更。

H2 数据库控制台

H2 数据库提供了便捷的 Web 控制台,特别适合开发和调试:

kotlin
@Configuration
@Profile("dev") 
class H2ConsoleConfig {
    
    @Bean
    @ConditionalOnProperty(name = ["spring.h2.console.enabled"], havingValue = "true")
    fun h2ConsoleProperties(): H2ConsoleProperties {
        return H2ConsoleProperties().apply {
            path = "/h2-console"
            settings.isWebAllowOthers = false // 安全设置
        }
    }
}

安全配置

如果应用使用了 Spring Security,需要特殊配置以允许访问 H2 控制台:

kotlin
@Configuration
@Profile("dev")
class DevSecurityConfig {
    
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    fun h2ConsoleSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .securityMatcher(PathRequest.toH2Console()) 
            .authorizeHttpRequests { auth -> 
                auth.anyRequest().permitAll() 
            }
            .csrf { csrf -> csrf.disable() } 
            .headers { headers -> 
                headers.frameOptions { frame -> 
                    frame.sameOrigin() 
                }
            }
            .build()
    }
}

CAUTION

H2 控制台仅应在开发环境中启用,生产环境中禁用 CSRF 保护和允许框架嵌入存在安全风险。

响应式数据库访问 (R2DBC)

R2DBC 的价值主张

R2DBC(Reactive Relational Database Connectivity)为关系数据库带来了响应式编程的能力,解决了传统 JDBC 阻塞 I/O 的性能瓶颈。

R2DBC 配置

yaml
spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/myapp
    username: ${DB_USER:dbuser}
    password: ${DB_PASSWORD:dbpass}
    pool:
      initial-size: 10
      max-size: 20
      max-idle-time: 30m

使用 DatabaseClient

kotlin
@Service
class ReactiveUserService(
    private val databaseClient: DatabaseClient
) {
    
    fun findUserById(id: Long): Mono<User> {
        return databaseClient
            .sql("SELECT id, name, email FROM users WHERE id = :id")
            .bind("id", id) 
            .map { row -> 
                User(
                    id = row.get("id", Long::class.java)!!,
                    name = row.get("name", String::class.java)!!,
                    email = row.get("email", String::class.java)!!
                )
            }
            .one() 
    }
    
    fun findAllUsers(): Flux<User> {
        return databaseClient
            .sql("SELECT id, name, email FROM users ORDER BY created_at DESC")
            .map { row -> 
                User(
                    id = row.get("id", Long::class.java)!!,
                    name = row.get("name", String::class.java)!!,
                    email = row.get("email", String::class.java)!!
                )
            }
            .all() 
    }
    
    fun createUser(user: User): Mono<User> {
        return databaseClient
            .sql("INSERT INTO users (name, email) VALUES (:name, :email)")
            .bind("name", user.name)
            .bind("email", user.email)
            .filter { statement -> statement.returnGeneratedValues("id") } 
            .map { row -> 
                user.copy(id = row.get("id", Long::class.java))
            }
            .one()
    }
}

Spring Data R2DBC 仓库

kotlin
interface ReactiveUserRepository : ReactiveCrudRepository<User, Long> {
    
    fun findByEmail(email: String): Mono<User> 
    
    fun findByNameContainingIgnoreCase(name: String): Flux<User> 
    
    @Query("SELECT * FROM users WHERE created_at > :date ORDER BY created_at DESC")
    fun findRecentUsers(date: LocalDateTime): Flux<User> 
    
    fun findByStatus(status: UserStatus): Flux<User>
}

响应式服务实现

kotlin
@Service
class ReactiveUserService(
    private val userRepository: ReactiveUserRepository
) {
    
    fun createUser(createRequest: CreateUserRequest): Mono<User> {
        return userRepository.findByEmail(createRequest.email)
            .flatMap<User> { 
                Mono.error(IllegalArgumentException("Email already exists")) 
            }
            .switchIfEmpty( 
                Mono.defer {
                    val user = User(
                        name = createRequest.name,
                        email = createRequest.email
                    )
                    userRepository.save(user)
                }
            )
    }
    
    fun findUsersByKeyword(keyword: String): Flux<User> {
        return userRepository.findByNameContainingIgnoreCase(keyword)
            .take(50) // 限制结果数量
            .timeout(Duration.ofSeconds(5)) // 设置超时
    }
    
    fun getUserStatistics(): Mono<UserStatistics> {
        val activeUsers = userRepository.countByStatus(UserStatus.ACTIVE)
        val totalUsers = userRepository.count()
        
        return Mono.zip(activeUsers, totalUsers) { active, total ->
            UserStatistics(
                totalUsers = total,
                activeUsers = active,
                inactiveUsers = total - active
            )
        }
    }
}

最佳实践与性能优化

1. 连接池配置优化

yaml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000 # 5分钟
      max-lifetime: 1200000 # 20分钟
      connection-timeout: 30000 # 30秒
      leak-detection-threshold: 60000 # 1分钟 #

TIP

连接池大小的经验公式:connections = ((core_count * 2) + effective_spindle_count)

2. JPA 查询优化

kotlin
@Entity
class User {
    // 使用索引优化查询
    @Column(name = "email")
    @Index(name = "idx_user_email") 
    val email: String,
    
    // 懒加载关联对象
    @OneToMany(fetch = FetchType.LAZY) 
    val orders: List<Order> = emptyList()
}

@Repository
interface UserRepository : JpaRepository<User, Long> {
    
    // 使用 JOIN FETCH 避免 N+1 问题
    @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id") 
    fun findByIdWithOrders(@Param("id") id: Long): User?
    
    // 投影查询,只获取需要的字段
    @Query("SELECT new com.example.dto.UserSummary(u.id, u.name, u.email) FROM User u") 
    fun findAllSummaries(): List<UserSummary>
}

3. 事务管理最佳实践

kotlin
@Service
@Transactional
class OrderService(
    private val orderRepository: OrderRepository,
    private val inventoryService: InventoryService,
    private val paymentService: PaymentService
) {
    
    // 只读事务优化
    @Transactional(readOnly = true) 
    fun findOrderHistory(userId: Long): List<Order> {
        return orderRepository.findByUserIdOrderByCreatedAtDesc(userId)
    }
    
    // 细粒度事务控制
    @Transactional(rollbackFor = [BusinessException::class]) 
    fun processOrder(orderRequest: OrderRequest): Order {
        // 检查库存
        inventoryService.checkAvailability(orderRequest.items)
        
        // 创建订单
        val order = orderRepository.save(
            Order.from(orderRequest)
        )
        
        // 处理支付(可能抛出 BusinessException)
        paymentService.processPayment(order.totalAmount)
        
        return order
    }
    
    // 编程式事务控制
    @Transactional(propagation = Propagation.REQUIRES_NEW) 
    fun logOrderEvent(orderId: Long, event: String) {
        // 即使主事务回滚,日志也会被保存
        orderEventRepository.save(OrderEvent(orderId, event))
    }
}

总结

Spring Boot 的 SQL 数据库支持为开发者提供了从简单到复杂的完整解决方案:

  • JdbcTemplate/JdbcClient:适合简单查询和对 SQL 有完全控制需求的场景
  • JPA/Spring Data JPA:适合复杂的对象关系映射和快速开发
  • R2DBC:适合高并发、响应式的应用场景

NOTE

选择合适的技术栈应该基于项目的具体需求:

  • 团队技术栈熟悉度
  • 性能要求
  • 数据模型复杂度
  • 并发访问模式

通过合理的配置和最佳实践,Spring Boot 的数据库支持能够满足从简单的 CRUD 操作到复杂的企业级应用的各种需求。记住,技术选择没有银弹,关键是要根据具体场景做出明智的决策。 🎯