1. Flyway 是什么?
Flyway 是一个开源的数据库版本控制和迁移工具,用于管理数据库 schema 的变更。它帮助开发者以版本化、可重复、可追踪的方式管理数据库结构和数据的演进。
2. Flyway 的作用
核心功能
- 版本控制:为数据库变更提供版本号管理
- 自动迁移:应用未执行的数据库变更脚本
- 一致性保证:确保所有环境(开发、测试、生产)的数据库结构一致
- 回滚支持:支持撤销数据库变更(需手动编写回滚脚本)
- 团队协作:多人开发时避免数据库结构冲突
解决的问题
- 手动执行 SQL 脚本容易出错和遗漏
- 不同环境数据库结构不一致
- 数据库变更无法追踪和版本化
- 部署时数据库升级复杂
3. Flyway 的特点
主要特性
- 简单易用:基于约定优于配置的原则
- 支持多种数据库:MySQL、PostgreSQL、Oracle、SQL Server 等 20+ 种数据库
- 多种脚本格式:支持 SQL 和 Java 代码两种迁移方式
- 自动版本跟踪:自动创建
flyway_schema_history表记录执行历史 - 事务支持:每个迁移脚本在一个事务中执行
- 校验机制:防止已执行的脚本被意外修改
脚本命名规范
- 版本化迁移:
V1__Initial.sql、V2__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 是一个强大而简单的数据库版本控制工具,通过本文的详细示例,你应该能够:
- 理解 Flyway 的核心概念和工作原理
- 在 Spring Boot 3 项目中正确配置和使用 Flyway
- 编写规范的 SQL 迁移脚本
- 构建完整的 CRUD 应用并与 Flyway 集成
- 处理常见的生产环境问题
Flyway 的关键优势在于简单性和可靠性,它让数据库变更变得像代码版本控制一样简单和可追踪。

2万+

被折叠的 条评论
为什么被折叠?



