Skip to content

Spring Boot 数据库初始化指南 🚀

概述

在现代应用开发中,数据库初始化是一个至关重要的环节。想象一下,如果每次部署新应用时都需要手动创建表结构、插入基础数据,那将是多么繁琐和容易出错的过程!Spring Boot 为我们提供了多种优雅的数据库初始化方案,让这个过程变得自动化、可靠且易于管理。

NOTE

数据库初始化不仅仅是创建表结构,还包括插入初始数据、执行数据迁移等操作。选择合适的初始化策略对项目的长期维护至关重要。

为什么需要数据库初始化?

在没有自动化数据库初始化之前,开发团队通常面临以下痛点:

  • 环境不一致:开发、测试、生产环境的数据库结构可能存在差异
  • 部署复杂:每次部署都需要手动执行 SQL 脚本
  • 版本管理困难:数据库结构变更难以追踪和回滚
  • 团队协作问题:新成员加入项目时,环境搭建复杂

Spring Boot 的数据库初始化机制完美解决了这些问题! 🎉

初始化方式对比

方式一:使用 Hibernate 自动初始化

核心配置

Hibernate 提供了 ddl-auto 属性来控制数据库初始化行为:

kotlin
spring:
  jpa:
    hibernate:
      ddl-auto: create-drop  # 每次启动重新创建表
    show-sql: true          # 显示 SQL 语句
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
kotlin
spring:
  jpa:
    hibernate:
      ddl-auto: validate     # 仅验证表结构,不修改
    show-sql: false
  datasource:
    url: jdbc:mysql://localhost:3306/myapp
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

DDL-Auto 选项详解

选项说明适用场景
none不执行任何操作生产环境,使用专业迁移工具
validate验证表结构是否匹配生产环境验证
update更新表结构(不删除)开发环境 ⚠️
create每次启动创建表测试环境
create-drop启动创建,关闭删除单元测试

WARNING

在生产环境中,永远不要使用 createcreate-dropupdate!这可能导致数据丢失。

实体类示例

kotlin
@Entity
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    
    @Column(nullable = false, unique = true)
    val username: String,
    
    @Column(nullable = false)
    val email: String,
    
    @CreationTimestamp
    val createdAt: LocalDateTime = LocalDateTime.now()
)

@Entity
@Table(name = "posts")
data class Post(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    
    @Column(nullable = false)
    val title: String,
    
    @Column(columnDefinition = "TEXT")
    val content: String,
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    val author: User
)

初始数据导入

当使用 createcreate-drop 时,可以在 src/main/resources/import.sql 中添加初始数据:

sql
-- import.sql
INSERT INTO users (username, email) VALUES ('admin', '[email protected]');
INSERT INTO users (username, email) VALUES ('user1', '[email protected]');
INSERT INTO posts (title, content, user_id) VALUES ('Welcome', 'Welcome to our blog!', 1);

TIP

import.sql 是 Hibernate 特有的功能,与 Spring Boot 无关。它只在表结构从零创建时执行。

方式二:使用 SQL 脚本初始化

基本配置

Spring Boot 会自动寻找并执行以下脚本:

kotlin
// application.yml
spring:
  sql:
    init:
      mode: always                    # 总是执行初始化
      schema-locations:              # 表结构脚本位置
        - classpath:schema.sql
        - classpath:schema-${spring.sql.init.platform}.sql
      data-locations:                # 数据脚本位置  
        - classpath:data.sql
        - classpath:data-${spring.sql.init.platform}.sql
      platform: mysql               # 数据库平台
      continue-on-error: false      # 遇到错误是否继续

脚本文件结构

src/main/resources/
├── schema.sql              # 通用表结构
├── schema-mysql.sql        # MySQL 特定表结构
├── schema-postgresql.sql   # PostgreSQL 特定表结构
├── data.sql               # 通用初始数据
├── data-mysql.sql         # MySQL 特定数据
└── data-postgresql.sql    # PostgreSQL 特定数据

脚本示例

sql
-- 用户表
CREATE TABLE IF NOT EXISTS users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 文章表
CREATE TABLE IF NOT EXISTS posts (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    content TEXT,
    user_id BIGINT NOT NULL,
    status VARCHAR(20) DEFAULT 'DRAFT',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- 创建索引
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status);
sql
-- 插入管理员用户
INSERT INTO users (username, email, password) VALUES 
('admin', '[email protected]', '$2a$10$...');

-- 插入示例文章
INSERT INTO posts (title, content, user_id, status) VALUES 
('欢迎使用我们的博客系统', '这是一篇欢迎文章...', 1, 'PUBLISHED'),
('Spring Boot 入门指南', 'Spring Boot 是一个...', 1, 'DRAFT');

与 JPA 结合使用

当同时使用 JPA 和 SQL 脚本时,需要注意执行顺序:

kotlin
// application.yml
spring:
  jpa:
    defer-datasource-initialization: true  # 延迟数据源初始化 [!code highlight]
    hibernate:
      ddl-auto: create
  sql:
    init:
      mode: always

IMPORTANT

设置 defer-datasource-initialization: true 确保 SQL 脚本在 Hibernate 创建表结构之后执行。

方式三:使用 Flyway 数据库迁移

为什么选择 Flyway?

Flyway 是一个专业的数据库迁移工具,它解决了以下问题:

  • 版本控制:每个迁移都有版本号,可追踪数据库变更历史
  • 增量更新:只执行未应用的迁移,避免重复执行
  • 回滚支持:支持数据库版本回滚(商业版)
  • 多环境支持:不同环境可以有不同的迁移策略

添加依赖

kotlin
// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.flywaydb:flyway-core")
    implementation("org.flywaydb:flyway-mysql") // 根据数据库选择
}

配置 Flyway

kotlin
// application.yml
spring:
  flyway:
    enabled: true
    locations: 
      - classpath:db/migration
      - classpath:db/migration/{vendor}  # 数据库特定迁移
    baseline-on-migrate: true           # 在非空数据库上启用基线
    validate-on-migrate: true           # 验证迁移脚本
    out-of-order: false                # 不允许乱序执行

迁移脚本命名规范

src/main/resources/db/migration/
├── V1__Create_user_table.sql
├── V2__Create_post_table.sql  
├── V3__Add_user_avatar_column.sql
├── V4__Insert_initial_data.sql
└── V5__Create_comment_table.sql

NOTE

Flyway 迁移脚本命名规则:V{版本号}__{描述}.sql,版本号用下划线分隔(如 V2_1)。

迁移脚本示例

sql
-- 创建用户表
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    status VARCHAR(20) DEFAULT 'ACTIVE',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 创建索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);
sql
-- 创建文章表
CREATE TABLE posts (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    slug VARCHAR(200) NOT NULL UNIQUE,
    content TEXT,
    excerpt VARCHAR(500),
    user_id BIGINT NOT NULL,
    status VARCHAR(20) DEFAULT 'DRAFT',
    published_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id)
);

-- 创建索引
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_published_at ON posts(published_at);
sql
-- 为用户表添加头像字段
ALTER TABLE users 
ADD COLUMN avatar_url VARCHAR(255) NULL AFTER email;

-- 为现有用户设置默认头像
UPDATE users 
SET avatar_url = 'https://example.com/default-avatar.png' 
WHERE avatar_url IS NULL;

Java 迁移示例

除了 SQL 脚本,Flyway 还支持 Java 迁移:

kotlin
@Component
class V4__InsertInitialData : JavaMigration {
    
    override fun getVersion(): MigrationVersion = MigrationVersion.fromVersion("4")
    
    override fun getDescription(): String = "Insert initial data"
    
    override fun migrate(context: Context) {
        val jdbcTemplate = JdbcTemplate(context.connection)
        
        // 插入管理员用户
        jdbcTemplate.update("""
            INSERT INTO users (username, email, password, status) 
            VALUES (?, ?, ?, ?)
        """.trimIndent(), 
            "admin", 
            "[email protected]", 
            passwordEncoder.encode("admin123"),
            "ACTIVE"
        )
        
        // 插入示例文章
        jdbcTemplate.update("""
            INSERT INTO posts (title, slug, content, user_id, status, published_at) 
            VALUES (?, ?, ?, ?, ?, ?)
        """.trimIndent(),
            "欢迎使用博客系统",
            "welcome-to-blog",
            "这是我们博客系统的第一篇文章...",
            1,
            "PUBLISHED",
            Timestamp.from(Instant.now())
        )
    }
}

环境特定迁移

kotlin
spring:
  flyway:
    locations: 
      - classpath:db/migration
      - classpath:db/migration/dev  # 开发环境特定迁移
sql
-- 开发环境测试数据
INSERT INTO users (username, email, password, status) VALUES 
('testuser1', '[email protected]', '$2a$10$...', 'ACTIVE'),
('testuser2', '[email protected]', '$2a$10$...', 'ACTIVE');

INSERT INTO posts (title, slug, content, user_id, status, published_at) VALUES 
('测试文章1', 'test-post-1', '这是测试文章内容...', 2, 'PUBLISHED', NOW()),
('测试文章2', 'test-post-2', '这是另一篇测试文章...', 3, 'DRAFT', NULL);

方式四:使用 Liquibase 数据库迁移

Liquibase vs Flyway

特性FlywayLiquibase
脚本格式SQL、JavaXML、YAML、JSON、SQL
学习曲线简单中等
回滚支持商业版开源版支持
数据库抽象有限强大
条件执行有限丰富

添加依赖

kotlin
// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.liquibase:liquibase-core")
}

配置 Liquibase

kotlin
// application.yml
spring:
  liquibase:
    change-log: classpath:db/changelog/db.changelog-master.yaml
    contexts: dev,prod                 # 执行上下文
    default-schema: myapp             # 默认模式
    drop-first: false                 # 不要在生产环境设为 true

主变更日志

yaml
# db/changelog/db.changelog-master.yaml
databaseChangeLog:
  - include:
      file: db/changelog/v1.0/db.changelog-v1.0.yaml
  - include:
      file: db/changelog/v1.1/db.changelog-v1.1.yaml
  - include:
      file: db/changelog/v2.0/db.changelog-v2.0.yaml

变更集示例

yaml
databaseChangeLog:
  - changeSet:
      id: create-users-table
      author: developer
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: BIGINT
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: username
                  type: VARCHAR(50)
                  constraints:
                    nullable: false
                    unique: true
              - column:
                  name: email
                  type: VARCHAR(100)
                  constraints:
                    nullable: false
                    unique: true
              - column:
                  name: password
                  type: VARCHAR(255)
                  constraints:
                    nullable: false
              - column:
                  name: created_at
                  type: TIMESTAMP
                  defaultValueComputed: CURRENT_TIMESTAMP

  - changeSet:
      id: create-posts-table
      author: developer
      changes:
        - createTable:
            tableName: posts
            columns:
              - column:
                  name: id
                  type: BIGINT
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: title
                  type: VARCHAR(200)
                  constraints:
                    nullable: false
              - column:
                  name: content
                  type: TEXT
              - column:
                  name: user_id
                  type: BIGINT
                  constraints:
                    nullable: false
                    foreignKeyName: fk_posts_user
                    references: users(id)
yaml
databaseChangeLog:
  - changeSet:
      id: add-user-avatar-column
      author: developer
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: avatar_url
                  type: VARCHAR(255)
                  afterColumn: email

  - changeSet:
      id: insert-initial-admin
      author: developer
      context: prod
      changes:
        - insert:
            tableName: users
            columns:
              - column:
                  name: username
                  value: admin
              - column:
                  name: email
                  value: [email protected]
              - column:
                  name: password
                  value: $2a$10$encrypted_password_here

条件执行和回滚

yaml
databaseChangeLog:
  - changeSet:
      id: add-index-if-not-exists
      author: developer
      preConditions:
        - not:
            - indexExists:
                indexName: idx_posts_title
      changes:
        - createIndex:
            indexName: idx_posts_title
            tableName: posts
            columns:
              - column:
                  name: title
      rollback:
        - dropIndex:
            indexName: idx_posts_title
            tableName: posts

  - changeSet:
      id: update-user-status
      author: developer
      context: migration
      changes:
        - update:
            tableName: users
            columns:
              - column:
                  name: status
                  value: ACTIVE
            where: status IS NULL

测试环境的特殊处理

Flyway 测试迁移

src/test/resources/db/migration/
└── V9999__test-data.sql    # 高版本号确保最后执行
sql
-- V9999__test-data.sql
-- 测试用户数据
INSERT INTO users (username, email, password, status) VALUES 
('testuser', '[email protected]', 'test123', 'ACTIVE');

-- 测试文章数据  
INSERT INTO posts (title, slug, content, user_id, status) VALUES 
('测试文章', 'test-article', '这是测试内容', 1, 'PUBLISHED');

Liquibase 测试配置

yaml
spring:
  liquibase:
    change-log: classpath:db/changelog/db.changelog-test.yaml
    contexts: test
yaml
databaseChangeLog:
  - include:
      file: classpath:db/changelog/db.changelog-master.yaml
  - changeSet:
      runOrder: last
      id: insert-test-data
      author: test
      context: test
      changes:
        - insert:
            tableName: users
            columns:
              - column:
                  name: username
                  value: testuser
              - column:
                  name: email  
                  value: [email protected]
kotlin
@SpringBootTest
@ActiveProfiles("test")  
@Transactional
class UserServiceTest {
    
    @Autowired
    private lateinit var userService: UserService
    
    @Test
    fun `should find test user`() {
        val user = userService.findByUsername("testuser")
        assertThat(user).isNotNull
        assertThat(user?.email).isEqualTo("[email protected]")
    }
}

依赖管理和启动顺序

Spring Boot 自动检测数据库初始化器并管理依赖关系:

自定义依赖检测

kotlin
@Component
class CustomDatabaseInitializer : DatabaseInitializer {
    
    override fun initializeDatabase() {
        // 自定义初始化逻辑
        log.info("执行自定义数据库初始化...")
    }
}

@Service
@DependsOnDatabaseInitialization
class UserService(
    private val userRepository: UserRepository
) {
    
    @PostConstruct
    fun init() {
        // 这个方法会在数据库初始化完成后执行
        log.info("UserService 初始化完成,数据库已就绪")
    }
}

最佳实践建议

1. 选择合适的初始化策略

推荐策略

  • 开发环境:Hibernate DDL + SQL 脚本,快速迭代
  • 测试环境:Flyway/Liquibase,确保环境一致性
  • 生产环境:专业迁移工具(Flyway/Liquibase),严格版本控制

2. 避免混合使用

WARNING

不要同时使用多种初始化方式!选择一种并坚持使用。

3. 版本控制策略

kotlin
// 推荐的版本号策略
V1_0_0__Initial_schema.sql          // 主版本
V1_0_1__Add_user_avatar.sql         // 小版本  
V1_1_0__Add_comment_feature.sql     // 功能版本
V2_0_0__Major_refactoring.sql       // 重大重构

4. 环境隔离

kotlin
// application-dev.yml
spring:
  jpa:
    hibernate:
      ddl-auto: create-drop
  sql:
    init:
      mode: always

// application-prod.yml  
spring:
  jpa:
    hibernate:
      ddl-auto: validate  # [!code highlight]
  flyway:
    enabled: true
    validate-on-migrate: true

5. 备份和回滚策略

IMPORTANT

在生产环境执行数据库迁移前,务必进行数据备份!

bash
# 生产部署前的检查清单
 数据库备份已完成
 迁移脚本已在测试环境验证  
 回滚方案已准备
 监控和告警已配置

总结

Spring Boot 的数据库初始化机制为我们提供了从简单到复杂的多种选择:

  1. Hibernate DDL:适合快速原型和开发环境
  2. SQL 脚本:简单直接,适合小型项目
  3. Flyway:专业迁移工具,适合大多数项目
  4. Liquibase:功能丰富,适合复杂场景

选择合适的策略,建立规范的流程,你的数据库管理将变得轻松而可靠! ✨

TIP

记住:好的数据库初始化策略不仅能提高开发效率,更能保障生产环境的稳定性。投入时间建立完善的数据库版本管理体系,绝对是值得的!