Flyway 数据库版本控制和迁移工具详细使用指南

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.6k人参与

1. Flyway 是什么?

Flyway 是一个开源的数据库版本控制和迁移工具,用于管理数据库 schema 的变更。它帮助开发者以版本化、可重复、可追踪的方式管理数据库结构和数据的演进。

2. Flyway 的作用

核心功能

  • 版本控制:为数据库变更提供版本号管理
  • 自动迁移:应用未执行的数据库变更脚本
  • 一致性保证:确保所有环境(开发、测试、生产)的数据库结构一致
  • 回滚支持:支持撤销数据库变更(需手动编写回滚脚本)
  • 团队协作:多人开发时避免数据库结构冲突

解决的问题

  • 手动执行 SQL 脚本容易出错和遗漏
  • 不同环境数据库结构不一致
  • 数据库变更无法追踪和版本化
  • 部署时数据库升级复杂

3. Flyway 的特点

主要特性

  • 简单易用:基于约定优于配置的原则
  • 支持多种数据库:MySQL、PostgreSQL、Oracle、SQL Server 等 20+ 种数据库
  • 多种脚本格式:支持 SQL 和 Java 代码两种迁移方式
  • 自动版本跟踪:自动创建 flyway_schema_history 表记录执行历史
  • 事务支持:每个迁移脚本在一个事务中执行
  • 校验机制:防止已执行的脚本被意外修改

脚本命名规范

  • 版本化迁移V1__Initial.sqlV2__Add_user_table.sql
  • 可重复迁移R__View_user_summary.sql
  • 撤消迁移U1__Undo_initial.sql(仅限 Flyway Teams 版本)

4. 在 Spring Boot 3 项目中使用 Flyway

4.1 项目依赖配置

Maven 配置 (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>flyway-demo</artifactId>
    <version>1.0.0</version>
    <name>flyway-demo</name>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Boot Data JPA Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Flyway 核心依赖 -->
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        
        <!-- Flyway MySQL 支持 -->
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-mysql</artifactId>
        </dependency>
        
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Gradle 配置 (build.gradle)

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '1.0.0'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.mysql:mysql-connector-j'
    implementation 'org.flywaydb:flyway-core'
    implementation 'org.flywaydb:flyway-mysql'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

4.2 应用配置

application.yml 配置

# 应用基本信息配置
spring:
  application:
    name: flyway-demo
  
  # 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/flyway_demo?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: root123
    driver-class-name: com.mysql.cj.jdbc.Driver
    
  # JPA 配置
  jpa:
    hibernate:
      # 禁用 Hibernate 自动建表,由 Flyway 管理
      ddl-auto: none
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        
  # Flyway 配置
  flyway:
    # 启用 Flyway(默认已启用)
    enabled: true
    # 数据库编码
    encoding: UTF-8
    # SQL 脚本位置(默认: classpath:db/migration)
    locations: classpath:db/migration
    # 表名(默认: flyway_schema_history)
    table: flyway_schema_history
    # 是否在迁移时进行校验
    validate-on-migrate: true
    # 基线版本(用于已有数据库的首次集成)
    baseline-version: 1
    # 是否在非空数据库上创建基线
    baseline-on-migrate: false
    # 是否清理 schema(生产环境慎用)
    clean-disabled: true
    # 支持的数据库类型
    database: mysql

# 服务器配置
server:
  port: 8080

# 日志配置
logging:
  level:
    org.flywaydb: DEBUG
    org.springframework.jdbc: DEBUG

application.properties 配置(备选)

# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/flyway_demo?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=root123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA 配置
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Flyway 配置
spring.flyway.enabled=true
spring.flyway.encoding=UTF-8
spring.flyway.locations=classpath:db/migration
spring.flyway.table=flyway_schema_history
spring.flyway.validate-on-migrate=true
spring.flyway.baseline-version=1
spring.flyway.baseline-on-migrate=false
spring.flyway.clean-disabled=true

# 服务器端口
server.port=8080

4.3 创建数据库

在 MySQL 中创建数据库:

-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS flyway_demo 
CHARACTER SET utf8mb4 
COLLATE utf8mb4_unicode_ci;

-- 使用数据库
USE flyway_demo;

4.4 编写迁移脚本

src/main/resources/db/migration/ 目录下创建 SQL 迁移脚本:

V1__Create_users_table.sql

-- 版本 1: 创建用户表
-- 文件命名规范: V<版本号>__<描述>.sql
-- 注意: 版本号和描述之间是两个下划线 (__)

-- 创建用户表
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID,主键自增',
    username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名,唯一约束',
    email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱,唯一约束',
    password VARCHAR(255) NOT NULL COMMENT '密码(加密存储)',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    is_active BOOLEAN DEFAULT TRUE COMMENT '是否激活'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 添加索引以提高查询性能
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);

V2__Create_products_table.sql

-- 版本 2: 创建产品表

-- 创建产品表
CREATE TABLE products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '产品ID,主键自增',
    name VARCHAR(100) NOT NULL COMMENT '产品名称',
    description TEXT COMMENT '产品描述',
    price DECIMAL(10, 2) NOT NULL COMMENT '产品价格',
    stock_quantity INT DEFAULT 0 COMMENT '库存数量',
    category VARCHAR(50) COMMENT '产品分类',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    is_available BOOLEAN DEFAULT TRUE COMMENT '是否可用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品表';

-- 添加索引
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_products_created_at ON products(created_at);

V3__Add_user_profile_table.sql

-- 版本 3: 创建用户资料表并建立外键关系

-- 创建用户资料表
CREATE TABLE user_profiles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '资料ID,主键自增',
    user_id BIGINT NOT NULL COMMENT '关联的用户ID',
    first_name VARCHAR(50) COMMENT '名字',
    last_name VARCHAR(50) COMMENT '姓氏',
    phone VARCHAR(20) COMMENT '电话号码',
    address TEXT COMMENT '地址',
    birth_date DATE COMMENT '出生日期',
    avatar_url VARCHAR(255) COMMENT '头像URL',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    CONSTRAINT fk_user_profile_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户资料表';

-- 添加唯一约束,确保一个用户只有一个资料
ALTER TABLE user_profiles ADD CONSTRAINT uk_user_profile_user_id UNIQUE (user_id);

-- 添加索引
CREATE INDEX idx_user_profiles_user_id ON user_profiles(user_id);

V4__Insert_initial_data.sql

-- 版本 4: 插入初始数据

-- 插入初始用户数据
INSERT INTO users (username, email, password, is_active) VALUES
('admin', 'admin@example.com', '$2a$10$8a7d5b1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e', TRUE),
('john_doe', 'john@example.com', '$2a$10$8a7d5b1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e', TRUE),
('jane_smith', 'jane@example.com', '$2a$10$8a7d5b1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e3f4c6a9b2d1e', TRUE);

-- 插入初始产品数据
INSERT INTO products (name, description, price, stock_quantity, category, is_available) VALUES
('iPhone 15', '最新款苹果手机', 999.99, 100, 'Electronics', TRUE),
('MacBook Pro', '专业级笔记本电脑', 2499.99, 50, 'Electronics', TRUE),
('Coffee Mug', '陶瓷咖啡杯', 15.99, 200, 'Home', TRUE),
('Running Shoes', '专业跑步鞋', 129.99, 75, 'Sports', TRUE);

4.5 创建实体类

User.java

package com.example.flywaydemo.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;

/**
 * 用户实体类
 * 对应数据库中的 users 表
 */
@Entity
@Table(name = "users")
public class User {
    
    /**
     * 用户ID,主键
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 用户名,唯一约束
     */
    @Column(nullable = false, unique = true, length = 50)
    private String username;
    
    /**
     * 邮箱,唯一约束
     */
    @Column(nullable = false, unique = true, length = 100)
    private String email;
    
    /**
     * 密码(已加密)
     */
    @Column(nullable = false)
    private String password;
    
    /**
     * 创建时间
     */
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    /**
     * 更新时间
     */
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
    
    /**
     * 是否激活
     */
    @Column(name = "is_active")
    private Boolean active = true;
    
    // 构造函数
    public User() {}
    
    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }
    
    // Getter 和 Setter 方法
    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 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 getActive() {
        return active;
    }
    
    public void setActive(Boolean active) {
        this.active = active;
    }
}

Product.java

package com.example.flywaydemo.entity;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 产品实体类
 * 对应数据库中的 products 表
 */
@Entity
@Table(name = "products")
public class Product {
    
    /**
     * 产品ID,主键
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 产品名称
     */
    @Column(nullable = false, length = 100)
    private String name;
    
    /**
     * 产品描述
     */
    @Column(columnDefinition = "TEXT")
    private String description;
    
    /**
     * 产品价格
     */
    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;
    
    /**
     * 库存数量
     */
    @Column(name = "stock_quantity")
    private Integer stockQuantity = 0;
    
    /**
     * 产品分类
     */
    @Column(length = 50)
    private String category;
    
    /**
     * 创建时间
     */
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    /**
     * 更新时间
     */
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
    
    /**
     * 是否可用
     */
    @Column(name = "is_available")
    private Boolean available = true;
    
    // 构造函数
    public Product() {}
    
    public Product(String name, String description, BigDecimal price, Integer stockQuantity, String category) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.stockQuantity = stockQuantity;
        this.category = category;
    }
    
    // Getter 和 Setter 方法
    public Long getId() {
        return id;
    }
    
    public void setId(Long id) {
        this.id = id;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getDescription() {
        return description;
    }
    
    public void setDescription(String description) {
        this.description = description;
    }
    
    public BigDecimal getPrice() {
        return price;
    }
    
    public void setPrice(BigDecimal price) {
        this.price = price;
    }
    
    public Integer getStockQuantity() {
        return stockQuantity;
    }
    
    public void setStockQuantity(Integer stockQuantity) {
        this.stockQuantity = stockQuantity;
    }
    
    public String getCategory() {
        return category;
    }
    
    public void setCategory(String category) {
        this.category = category;
    }
    
    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 getAvailable() {
        return available;
    }
    
    public void setAvailable(Boolean available) {
        this.available = available;
    }
}

4.6 创建 Repository 接口

UserRepository.java

package com.example.flywaydemo.repository;

import com.example.flywaydemo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * 用户数据访问接口
 * 继承 JpaRepository 获得基本的 CRUD 操作
 */
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    /**
     * 根据用户名查找用户
     * @param username 用户名
     * @return 用户实体
     */
    User findByUsername(String username);
    
    /**
     * 根据邮箱查找用户
     * @param email 邮箱地址
     * @return 用户实体
     */
    User findByEmail(String email);
}

ProductRepository.java

package com.example.flywaydemo.repository;

import com.example.flywaydemo.entity.Product;
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.math.BigDecimal;
import java.util.List;

/**
 * 产品数据访问接口
 */
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    
    /**
     * 根据分类查找产品
     * @param category 产品分类
     * @return 产品列表
     */
    List<Product> findByCategory(String category);
    
    /**
     * 查找价格低于指定值的产品
     * @param maxPrice 最高价格
     * @return 产品列表
     */
    List<Product> findByPriceLessThan(BigDecimal maxPrice);
    
    /**
     * 自定义查询:查找可用的产品
     * @return 可用产品列表
     */
    @Query("SELECT p FROM Product p WHERE p.available = true")
    List<Product> findAvailableProducts();
    
    /**
     * 自定义查询:根据价格范围查找产品
     * @param minPrice 最低价格
     * @param maxPrice 最高价格
     * @return 产品列表
     */
    @Query("SELECT p FROM Product p WHERE p.price BETWEEN :minPrice AND :maxPrice")
    List<Product> findByPriceRange(@Param("minPrice") BigDecimal minPrice, 
                                  @Param("maxPrice") BigDecimal maxPrice);
}

4.7 创建 Service 层

UserService.java

package com.example.flywaydemo.service;

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

import java.util.List;
import java.util.Optional;

/**
 * 用户业务逻辑服务类
 */
@Service
public class UserService {
    
    /**
     * 注入用户数据访问接口
     */
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 获取所有用户
     * @return 用户列表
     */
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    
    /**
     * 根据ID获取用户
     * @param id 用户ID
     * @return 用户实体(Optional包装)
     */
    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }
    
    /**
     * 根据用户名获取用户
     * @param username 用户名
     * @return 用户实体
     */
    public User getUserByUsername(String username) {
        return userRepository.findByUsername(username);
    }
    
    /**
     * 保存用户
     * @param user 用户实体
     * @return 保存后的用户实体
     */
    public User saveUser(User user) {
        return userRepository.save(user);
    }
    
    /**
     * 删除用户
     * @param id 用户ID
     */
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

ProductService.java

package com.example.flywaydemo.service;

import com.example.flywaydemo.entity.Product;
import com.example.flywaydemo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

/**
 * 产品业务逻辑服务类
 */
@Service
public class ProductService {
    
    /**
     * 注入产品数据访问接口
     */
    @Autowired
    private ProductRepository productRepository;
    
    /**
     * 获取所有产品
     * @return 产品列表
     */
    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }
    
    /**
     * 获取所有可用产品
     * @return 可用产品列表
     */
    public List<Product> getAvailableProducts() {
        return productRepository.findAvailableProducts();
    }
    
    /**
     * 根据ID获取产品
     * @param id 产品ID
     * @return 产品实体(Optional包装)
     */
    public Optional<Product> getProductById(Long id) {
        return productRepository.findById(id);
    }
    
    /**
     * 根据分类获取产品
     * @param category 产品分类
     * @return 产品列表
     */
    public List<Product> getProductsByCategory(String category) {
        return productRepository.findByCategory(category);
    }
    
    /**
     * 根据价格范围获取产品
     * @param minPrice 最低价格
     * @param maxPrice 最高价格
     * @return 产品列表
     */
    public List<Product> getProductsByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
        return productRepository.findByPriceRange(minPrice, maxPrice);
    }
    
    /**
     * 保存产品
     * @param product 产品实体
     * @return 保存后的产品实体
     */
    public Product saveProduct(Product product) {
        return productRepository.save(product);
    }
    
    /**
     * 删除产品
     * @param id 产品ID
     */
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
}

4.8 创建 Controller 层

UserController.java

package com.example.flywaydemo.controller;

import com.example.flywaydemo.entity.User;
import com.example.flywaydemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

/**
 * 用户控制器
 * 提供 RESTful API 接口
 */
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    /**
     * 注入用户服务
     */
    @Autowired
    private UserService userService;
    
    /**
     * 获取所有用户
     * GET /api/users
     * @return 用户列表
     */
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }
    
    /**
     * 根据ID获取用户
     * GET /api/users/{id}
     * @param id 用户ID
     * @return 用户实体
     */
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        Optional<User> user = userService.getUserById(id);
        return user.map(ResponseEntity::ok)
                  .orElse(ResponseEntity.notFound().build());
    }
    
    /**
     * 根据用户名获取用户
     * GET /api/users/username/{username}
     * @param username 用户名
     * @return 用户实体
     */
    @GetMapping("/username/{username}")
    public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
        User user = userService.getUserByUsername(username);
        if (user != null) {
            return ResponseEntity.ok(user);
        }
        return ResponseEntity.notFound().build();
    }
    
    /**
     * 创建或更新用户
     * POST /api/users
     * @param user 用户实体
     * @return 保存后的用户实体
     */
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User savedUser = userService.saveUser(user);
        return ResponseEntity.ok(savedUser);
    }
    
    /**
     * 删除用户
     * DELETE /api/users/{id}
     * @param id 用户ID
     * @return 删除成功响应
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

ProductController.java

package com.example.flywaydemo.controller;

import com.example.flywaydemo.entity.Product;
import com.example.flywaydemo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

/**
 * 产品控制器
 * 提供 RESTful API 接口
 */
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    /**
     * 注入产品服务
     */
    @Autowired
    private ProductService productService;
    
    /**
     * 获取所有产品
     * GET /api/products
     * @return 产品列表
     */
    @GetMapping
    public ResponseEntity<List<Product>> getAllProducts() {
        List<Product> products = productService.getAllProducts();
        return ResponseEntity.ok(products);
    }
    
    /**
     * 获取所有可用产品
     * GET /api/products/available
     * @return 可用产品列表
     */
    @GetMapping("/available")
    public ResponseEntity<List<Product>> getAvailableProducts() {
        List<Product> products = productService.getAvailableProducts();
        return ResponseEntity.ok(products);
    }
    
    /**
     * 根据ID获取产品
     * GET /api/products/{id}
     * @param id 产品ID
     * @return 产品实体
     */
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        Optional<Product> product = productService.getProductById(id);
        return product.map(ResponseEntity::ok)
                     .orElse(ResponseEntity.notFound().build());
    }
    
    /**
     * 根据分类获取产品
     * GET /api/products/category/{category}
     * @param category 产品分类
     * @return 产品列表
     */
    @GetMapping("/category/{category}")
    public ResponseEntity<List<Product>> getProductsByCategory(@PathVariable String category) {
        List<Product> products = productService.getProductsByCategory(category);
        return ResponseEntity.ok(products);
    }
    
    /**
     * 根据价格范围获取产品
     * GET /api/products/price-range?min={min}&max={max}
     * @param minPrice 最低价格
     * @param maxPrice 最高价格
     * @return 产品列表
     */
    @GetMapping("/price-range")
    public ResponseEntity<List<Product>> getProductsByPriceRange(
            @RequestParam BigDecimal minPrice,
            @RequestParam BigDecimal maxPrice) {
        List<Product> products = productService.getProductsByPriceRange(minPrice, maxPrice);
        return ResponseEntity.ok(products);
    }
    
    /**
     * 创建或更新产品
     * POST /api/products
     * @param product 产品实体
     * @return 保存后的产品实体
     */
    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody Product product) {
        Product savedProduct = productService.saveProduct(product);
        return ResponseEntity.ok(savedProduct);
    }
    
    /**
     * 删除产品
     * DELETE /api/products/{id}
     * @param id 产品ID
     * @return 删除成功响应
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
        return ResponseEntity.noContent().build();
    }
}

4.9 创建主启动类

FlywayDemoApplication.java

package com.example.flywaydemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Spring Boot 应用主启动类
 * 
 * 应用启动时,Flyway 会自动执行以下操作:
 * 1. 检查数据库中是否存在 flyway_schema_history 表
 * 2. 如果不存在,则创建该表用于记录迁移历史
 * 3. 扫描 classpath:db/migration 目录下的所有迁移脚本
 * 4. 按照版本号顺序执行未执行的迁移脚本
 * 5. 在 flyway_schema_history 表中记录已执行的脚本信息
 */
@SpringBootApplication
public class FlywayDemoApplication {
    
    /**
     * 应用程序入口点
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        SpringApplication.run(FlywayDemoApplication.class, args);
    }
}

5. 运行和测试

5.1 启动应用

# 使用 Maven 启动
./mvnw spring-boot:run

# 或者构建后运行
./mvnw clean package
java -jar target/flyway-demo-1.0.0.jar

5.2 观察 Flyway 执行日志

启动时会看到类似以下日志:

INFO  o.f.c.i.license.VersionPrinter - Flyway Community Edition 10.0.0 by Redgate
INFO  o.f.c.i.database.base.DatabaseType - Database: jdbc:mysql://localhost:3306/flyway_demo (MySQL 8.0)
INFO  o.f.c.i.s.JdbcTableSchemaHistory - Creating Schema History table `flyway_demo`.`flyway_schema_history` ...
INFO  o.f.c.i.c.DbMigrate - Current version of schema `flyway_demo`: << Empty Schema >>
INFO  o.f.c.i.c.DbMigrate - Migrating schema `flyway_demo` to version "1 - Create users table"
INFO  o.f.c.i.c.DbMigrate - Migrating schema `flyway_demo` to version "2 - Create products table"
INFO  o.f.c.i.c.DbMigrate - Migrating schema `flyway_demo` to version "3 - Add user profile table"
INFO  o.f.c.i.c.DbMigrate - Migrating schema `flyway_demo` to version "4 - Insert initial data"
INFO  o.f.c.i.c.DbMigrate - Successfully applied 4 migrations to schema `flyway_demo`

5.3 验证数据库结构

检查数据库中的表:

-- 查看创建的表
SHOW TABLES;

-- 查看 Flyway 历史记录表
SELECT * FROM flyway_schema_history;

-- 查看用户表数据
SELECT * FROM users;

-- 查看产品表数据
SELECT * FROM products;

5.4 测试 API 接口

# 获取所有用户
curl http://localhost:8080/api/users

# 获取所有产品
curl http://localhost:8080/api/products

# 根据分类获取产品
curl http://localhost:8080/api/products/category/Electronics

# 根据价格范围获取产品
curl "http://localhost:8080/api/products/price-range?min=100&max=1000"

6. 高级配置和最佳实践

6.1 多环境配置

application-dev.yml(开发环境)

spring:
  flyway:
    enabled: true
    clean-disabled: false  # 开发环境允许 clean 操作

application-prod.yml(生产环境)

spring:
  flyway:
    enabled: true
    clean-disabled: true   # 生产环境禁止 clean 操作
    validate-on-migrate: true

6.2 Java 迁移脚本示例

V5__JavaMigration.java

package db.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.Statement;

/**
 * Java 迁移脚本示例
 * 适用于复杂的数据库操作,如数据转换、条件逻辑等
 */
public class V5__JavaMigration extends BaseJavaMigration {
    
    /**
     * 执行迁移逻辑
     * @param context Flyway 上下文,包含数据库连接等信息
     * @throws Exception 迁移过程中可能出现的异常
     */
    @Override
    public void migrate(Context context) throws Exception {
        // 获取数据库连接
        try (Statement statement = context.getConnection().createStatement()) {
            // 执行复杂的 SQL 操作
            statement.execute("ALTER TABLE users ADD COLUMN last_login TIMESTAMP NULL");
            statement.execute("UPDATE users SET last_login = created_at WHERE last_login IS NULL");
        }
    }
}

6.3 回滚脚本(Flyway Teams 版本)

U1__Undo_create_users_table.sql

-- 撤消版本 1 的变更
DROP TABLE IF EXISTS users;

6.4 常用 Flyway 命令

// 在代码中手动控制 Flyway
@Autowired
private Flyway flyway;

// 执行迁移
flyway.migrate();

// 验证迁移
flyway.validate();

// 清理数据库(慎用!)
flyway.clean();

// 获取当前版本信息
flyway.info();

7. 常见问题和解决方案

7.1 已有数据库集成 Flyway

spring:
  flyway:
    # 为已有数据库创建基线
    baseline-on-migrate: true
    # 基线版本号
    baseline-version: 1

7.2 脚本校验失败

  • 原因:已执行的脚本被修改
  • 解决方案
    • 恢复脚本到原始状态
    • 或者禁用校验:spring.flyway.validate-on-migrate=false(不推荐)

7.3 生产环境安全考虑

  • 禁用 clean 操作
  • 启用脚本校验
  • 严格控制数据库用户权限
  • 迁移脚本经过充分测试

总结

Flyway 是一个强大而简单的数据库版本控制工具,通过本文的详细示例,你应该能够:

  1. 理解 Flyway 的核心概念和工作原理
  2. 在 Spring Boot 3 项目中正确配置和使用 Flyway
  3. 编写规范的 SQL 迁移脚本
  4. 构建完整的 CRUD 应用并与 Flyway 集成
  5. 处理常见的生产环境问题

Flyway 的关键优势在于简单性可靠性,它让数据库变更变得像代码版本控制一样简单和可追踪。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值