Appearance
Spring 内嵌数据库支持:轻量级开发与测试的利器 🚀
什么是内嵌数据库?为什么需要它?
想象一下,你正在开发一个 Spring Boot 应用,需要进行数据库相关的开发和测试。传统做法是安装一个完整的数据库服务器(如 MySQL、PostgreSQL),但这会带来很多麻烦:
- 🔧 环境配置复杂:每个开发者都需要安装和配置数据库
- ⏰ 启动时间长:数据库服务器启动需要时间
- 🧪 测试数据污染:测试之间可能相互影响
- 📦 部署依赖重:需要额外的数据库服务器资源
NOTE
内嵌数据库就像是一个"便携式数据库",它直接运行在你的应用进程中,无需外部数据库服务器,完美解决了上述痛点。
Spring 内嵌数据库的核心价值
Spring Framework 通过 org.springframework.jdbc.datasource.embedded
包提供了强大的内嵌数据库支持,原生支持三种主流内嵌数据库:
- HSQL:历史悠久,稳定可靠
- H2:性能优秀,功能丰富(推荐)
- Derby:Apache 出品,企业级支持
核心优势一览
开发阶段的最佳伙伴
- ✅ 轻量级:无需外部数据库服务器
- ✅ 快速启动:几毫秒内完成数据库初始化
- ✅ 易于配置:几行代码即可搞定
- ✅ 测试友好:每个测试都可以有独立的数据库实例
- ✅ 快速迭代:SQL 结构变更无需复杂的迁移过程
创建内嵌数据库的三种方式
方式一:Java 配置(推荐)
kotlin
@Configuration
class DataSourceConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.addScripts("schema.sql", "test-data.sql")
.build()
}
}
java
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.addScripts("schema.sql", "test-data.sql")
.build();
}
}
方式二:XML 配置
xml
<jdbc:embedded-database id="dataSource" generate-name="true" type="H2">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>
方式三:直接构建(测试场景)
kotlin
class DatabaseTest {
private lateinit var database: EmbeddedDatabase
@BeforeEach
fun setUp() {
database = EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.addDefaultScripts() // 自动加载 schema.sql 和 data.sql
.build()
}
@AfterEach
fun tearDown() {
database.shutdown()
}
}
IMPORTANT
记住在测试结束后调用 database.shutdown()
来释放资源,避免内存泄漏。
内嵌数据库类型选择指南
H2 数据库(强烈推荐)🌟
kotlin
@Bean
fun h2DataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.build()
}
为什么选择 H2?
- 🚀 性能卓越:内存模式下读写速度极快
- 🔧 功能丰富:支持多种 SQL 标准
- 🌐 Web 控制台:提供便捷的数据库管理界面
- 📱 兼容性好:与主流数据库语法高度兼容
HSQL 数据库
kotlin
@Bean
fun hsqlDataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:schema.sql")
.build()
}
HSQL 特点:
- 📚 历史悠久:Spring 默认选择,稳定可靠
- 🎯 简单直接:适合简单的测试场景
Derby 数据库
kotlin
@Bean
fun derbyDataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.DERBY)
.addScript("classpath:schema.sql")
.build()
}
Derby 特点:
- 🏢 企业级:Apache 出品,适合企业环境
- 🔒 事务支持:完整的 ACID 事务支持
实战场景:电商订单系统测试
让我们通过一个具体的电商订单系统来看看内嵌数据库的威力:
数据库结构定义
schema.sql - 数据库表结构
sql
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 商品表
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0
);
-- 订单表
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
测试数据准备
test-data.sql - 测试数据
sql
-- 插入测试用户
INSERT INTO users (username, email) VALUES
('john_doe', '[email protected]'),
('jane_smith', '[email protected]');
-- 插入测试商品
INSERT INTO products (name, price, stock) VALUES
('iPhone 15', 999.99, 10),
('MacBook Pro', 2499.99, 5),
('AirPods', 199.99, 20);
-- 插入测试订单
INSERT INTO orders (user_id, total_amount, status) VALUES
(1, 999.99, 'COMPLETED'),
(2, 2499.99, 'PENDING');
订单服务实现
kotlin
@Service
class OrderService(
private val jdbcTemplate: JdbcTemplate
) {
fun createOrder(userId: Long, productId: Long, quantity: Int): Order {
// 检查库存
val stock = jdbcTemplate.queryForObject(
"SELECT stock FROM products WHERE id = ?",
Int::class.java,
productId
) ?: throw ProductNotFoundException("Product not found")
if (stock < quantity) {
throw InsufficientStockException("Not enough stock")
}
// 获取商品价格
val price = jdbcTemplate.queryForObject(
"SELECT price FROM products WHERE id = ?",
BigDecimal::class.java,
productId
) ?: throw ProductNotFoundException("Product not found")
val totalAmount = price.multiply(BigDecimal.valueOf(quantity.toLong()))
// 创建订单
val keyHolder = GeneratedKeyHolder()
jdbcTemplate.update({ connection ->
val ps = connection.prepareStatement(
"INSERT INTO orders (user_id, total_amount, status) VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
)
ps.setLong(1, userId)
ps.setBigDecimal(2, totalAmount)
ps.setString(3, "PENDING")
ps
}, keyHolder)
val orderId = keyHolder.key?.toLong()
?: throw RuntimeException("Failed to create order")
// 更新库存
jdbcTemplate.update(
"UPDATE products SET stock = stock - ? WHERE id = ?",
quantity, productId
)
return Order(orderId, userId, totalAmount, "PENDING")
}
fun findOrdersByUserId(userId: Long): List<Order> {
return jdbcTemplate.query(
"SELECT * FROM orders WHERE user_id = ?",
{ rs, _ ->
Order(
id = rs.getLong("id"),
userId = rs.getLong("user_id"),
totalAmount = rs.getBigDecimal("total_amount"),
status = rs.getString("status")
)
},
userId
)
}
}
data class Order(
val id: Long,
val userId: Long,
val totalAmount: BigDecimal,
val status: String
)
完整的集成测试
kotlin
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class OrderServiceIntegrationTest {
@Autowired
private lateinit var orderService: OrderService
@Autowired
private lateinit var jdbcTemplate: JdbcTemplate
@Test
fun `should create order successfully when stock is sufficient`() {
// Given: 有足够的库存
val userId = 1L
val productId = 1L // iPhone 15, stock = 10
val quantity = 2
// When: 创建订单
val order = orderService.createOrder(userId, productId, quantity)
// Then: 订单创建成功
assertThat(order.userId).isEqualTo(userId)
assertThat(order.totalAmount).isEqualTo(BigDecimal("1999.98"))
assertThat(order.status).isEqualTo("PENDING")
// And: 库存正确减少
val remainingStock = jdbcTemplate.queryForObject(
"SELECT stock FROM products WHERE id = ?",
Int::class.java,
productId
)
assertThat(remainingStock).isEqualTo(8) // 10 - 2 = 8
}
@Test
fun `should throw exception when stock is insufficient`() {
// Given: 库存不足
val userId = 1L
val productId = 2L // MacBook Pro, stock = 5
val quantity = 10 // 请求数量超过库存
// When & Then: 应该抛出库存不足异常
assertThrows<InsufficientStockException> {
orderService.createOrder(userId, productId, quantity)
}
}
@Test
fun `should find orders by user id`() {
// Given: 用户已有订单
val userId = 1L
// When: 查询用户订单
val orders = orderService.findOrdersByUserId(userId)
// Then: 返回正确的订单列表
assertThat(orders).hasSize(1)
assertThat(orders[0].totalAmount).isEqualTo(BigDecimal("999.99"))
assertThat(orders[0].status).isEqualTo("COMPLETED")
}
}
内嵌数据库的测试最佳实践
1. 数据库隔离策略
2. 解决数据库名称冲突
WARNING
如果不使用唯一名称生成,多个测试可能会尝试创建同名数据库,导致冲突!
kotlin
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
// 没有设置唯一名称,可能导致冲突
.build()
}
kotlin
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.build()
}
3. 测试模板模式
kotlin
abstract class DatabaseTestTemplate {
protected lateinit var database: EmbeddedDatabase
protected lateinit var jdbcTemplate: JdbcTemplate
@BeforeEach
fun setUpDatabase() {
database = EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build()
jdbcTemplate = JdbcTemplate(database)
}
@AfterEach
fun tearDownDatabase() {
database.shutdown()
}
}
// 具体测试类继承模板
class UserRepositoryTest : DatabaseTestTemplate() {
@Test
fun `should find user by username`() {
// 使用继承的 jdbcTemplate 进行测试
val user = jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE username = ?",
{ rs, _ -> User(rs.getLong("id"), rs.getString("username")) },
"john_doe"
)
assertThat(user?.username).isEqualTo("john_doe")
}
}
高级定制:自定义数据库配置
有时候你需要对内嵌数据库进行更精细的控制,比如使用自定义驱动或连接参数:
kotlin
@Configuration
class CustomDataSourceConfig {
@Bean
fun customDataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setDatabaseConfigurer(
EmbeddedDatabaseConfigurers.customizeConfigurer(
EmbeddedDatabaseType.H2,
this::customizeH2
)
)
.addScript("schema.sql")
.build()
}
private fun customizeH2(defaultConfigurer: EmbeddedDatabaseConfigurer): EmbeddedDatabaseConfigurer {
return object : EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) {
override fun configureConnectionProperties(
properties: ConnectionProperties,
databaseName: String
) {
super.configureConnectionProperties(properties, databaseName)
// 自定义连接属性
properties.setDriverClass(CustomH2Driver::class.java)
properties.setUrl("jdbc:h2:mem:$databaseName;MODE=MySQL")
}
}
}
}
扩展内嵌数据库支持
Spring 提供了两种扩展方式:
1. 支持新的数据库类型
kotlin
class SQLiteEmbeddedDatabaseConfigurer : EmbeddedDatabaseConfigurer {
override fun configureConnectionProperties(
properties: ConnectionProperties,
databaseName: String
) {
properties.setDriverClass(SQLiteDriver::class.java)
properties.setUrl("jdbc:sqlite:memory:$databaseName")
properties.setUsername("")
properties.setPassword("")
}
override fun shutdown(dataSource: DataSource, databaseName: String) {
// SQLite 特定的关闭逻辑
}
}
2. 支持连接池
kotlin
class PooledDataSourceFactory : DataSourceFactory {
override fun getConnectionProperties(): ConnectionProperties {
return ConnectionProperties()
}
override fun createDataSource(
properties: ConnectionProperties,
databaseName: String
): DataSource {
val pooledDataSource = HikariDataSource()
pooledDataSource.jdbcUrl = properties.url
pooledDataSource.username = properties.username
pooledDataSource.password = properties.password
pooledDataSource.maximumPoolSize = 10
return pooledDataSource
}
}
总结与最佳实践 🎯
内嵌数据库是 Spring 生态中的一颗明珠,它完美解决了开发和测试阶段的数据库痛点:
核心价值回顾
- 开发效率提升:无需复杂的数据库环境搭建
- 测试质量保证:每个测试都有独立、干净的数据库环境
- 部署简化:减少外部依赖,降低部署复杂度
- 快速迭代:数据库结构变更无需复杂的迁移过程
最佳实践清单
开发阶段
- ✅ 优先选择 H2 数据库,性能和功能兼备
- ✅ 始终使用
generateUniqueName(true)
避免命名冲突 - ✅ 将 SQL 脚本文件放在
src/main/resources
下便于管理 - ✅ 使用有意义的脚本命名:
schema.sql
、data.sql
、test-data.sql
测试阶段
- ✅ 为每个测试类创建独立的数据库实例
- ✅ 在
@AfterEach
中及时关闭数据库释放资源 - ✅ 使用测试模板模式减少重复代码
- ✅ 结合 Spring TestContext Framework 实现数据库共享
注意事项
- ⚠️ 内嵌数据库仅适用于开发和测试,生产环境请使用专业数据库
- ⚠️ 大量数据的性能测试应该使用真实数据库环境
- ⚠️ 注意内存使用,避免在内存中存储过多测试数据
通过合理使用 Spring 的内嵌数据库支持,你可以构建出更加健壮、高效的应用程序,让开发和测试变得更加愉快! 🚀