1. 概述
Spring Boot 極大地簡化了數據庫變更的管理。如果未採用默認配置,它將會在我們的包中搜索實體並自動創建相應的表。
但有時我們需要對數據庫變更進行更精細的控制。這時,我們可以使用 Spring 中的 data.sql 和 schema.sql 文件。
2. 數據文件 data.sql 文件
讓我們在此假設我們正在使用 JPA,並在項目中定義一個簡單的 Country 實體:
@Entity
public class Country {
@Id
@GeneratedValue(strategy = IDENTITY)
private Integer id;
@Column(nullable = false)
private String name;
//...
}如果運行我們的應用程序,Spring Boot 將為我們創建一個空表,但不會填充任何內容。
一種簡單的方法是創建一個名為 data.sql 的文件:
INSERT INTO country (name) VALUES ('India');
INSERT INTO country (name) VALUES ('Brazil');
INSERT INTO country (name) VALUES ('USA');
INSERT INTO country (name) VALUES ('Italy');默認情況下,data.sql 腳本會在 Hibernate 初始化之前執行。我們需要 Hibernate 在創建表並插入數據之前完成初始化。為了實現這一點,我們需要延遲數據源的初始化。我們將使用下面的屬性來實現:
spring.jpa.defer-datasource-initialization=true當我們使用此文件作為類路徑運行項目時,Spring 會將其拾取並使用它來填充 country 表。
請注意,對於任何基於腳本的初始化,即通過 data.sql 或創建模式 schema.sql (我們將稍後學習) 插入數據,我們需要設置以下屬性:
spring.sql.init.mode=always對於嵌入式數據庫,如H2,默認情況下設置為始終。
3. schema.sql 文件
有時,我們不希望依賴默認的模式創建機制。
在這種情況下,我們可以創建自定義的 schema.sql 文件:
create table USERS(
ID int not null AUTO_INCREMENT,
NAME varchar(100) not null,
STATUS int,
PRIMARY KEY ( ID )
);Spring 將會拾取該文件並將其用於創建模式。
當我們使用包含該文件的類路徑運行項目時,雖然 Users 表未作為項目中的實體存在,但 Spring 仍然通過讀取 schema.sql 文件創建了 Users 表,存在於數據庫中。
請注意,如果我們在基於腳本的初始化中進行操作,即通過 schema.sql 和 data.sql 以及 Hibernate 初始化,那麼同時使用它們可能會導致一些問題。
為了解決這個問題,我們可以通過 Hibernate 禁用 DDL 命令的執行,Hibernate 用於表創建/更新:
spring.jpa.hibernate.ddl-auto=none
這將確保僅使用 schema.sql 執行基於腳本的模式生成。
如果仍然希望同時使用 Hibernate 自動模式生成與基於腳本的模式創建和數據填充,則需要使用:
spring.jpa.defer-datasource-initialization=true這將確保在執行 Hibernate 模式創建後,還會讀取 schema.sql 進行任何額外的模式更改,並進一步執行 data.sql 以填充數據庫。
如前一節所述,基於腳本的初始化默認僅對嵌入式數據庫執行。要始終使用腳本初始化數據庫,我們需要使用:
spring.sql.init.mode=always請參考官方 Spring 文檔關於 使用 SQL 腳本初始化數據庫 的內容。
4. 使用Hibernate控制數據庫創建
Spring 提供一個專門用於 Hibernate 的 JPA 屬性,用於生成 DDL:spring.jpa.hibernate.ddl-auto。
標準的 Hibernate 屬性值有 create, update, create-drop, validate 和 none。
- create – Hibernate 首先刪除現有的表,然後創建新的表。
- update – 基於映射(註解或 XML)的對象模型與現有模式進行比較,然後 Hibernate 根據差異更新模式。即使不再需要現有表或列,它也不會刪除這些表或列。
- create-drop – 類似於 create,但增加了 Hibernate 在所有操作完成後會刪除數據庫的特性;通常用於單元測試。
- validate – Hibernate 只驗證表和列是否存在;否則,它會拋出異常。
- none – 此值有效地關閉了 DDL 生成。
Spring Boot 內部默認此參數值為 create-drop,如果沒有檢測到 schema manager,否則為所有其他情況下的 none。
我們必須小心地設置此值或使用其他機制來初始化數據庫。
5. 自定義數據庫模式創建
默認情況下,Spring Boot 會自動創建嵌入式 DataSource 的模式。
如果需要控制或自定義此行為,我們可以使用 spring.sql.init.mode 屬性。該屬性可以取三個值:
- always – 始終初始化數據庫
- embedded – 如果使用嵌入式數據庫,則始終初始化。如果未指定屬性值,則此選項為默認值。
- never – 從未初始化數據庫
值得注意的是,如果我們在使用非嵌入式數據庫,例如 MySQL 或 PostgreSQL,並且希望初始化其模式,則必須將此屬性設置為 always。
此屬性在 Spring Boot 2.5.0 版本中引入的;如果使用 Spring Boot 的舊版本,則需要使用 spring.datasource.initialization-mode。
6. 使用 @Sql 註解
Spring 還提供了 @Sql 註解——一種聲明式的方式來初始化和填充我們的測試模式。
以下是 @Sql 註解的屬性:
- config – 用於 SQL 腳本的本地配置。我們在下一部分中將詳細討論它。
- executionPhase – 還可以指定執行 SQL 腳本的時間。
- statements – 可以聲明用於執行的 inline SQL 語句。
- scripts – 可以聲明 SQL 腳本文件的路徑以進行執行。這相當於 value 屬性。
@Sql 註解可以用於類級別或方法級別。
6.1. 在類級別使用 <em @Sql> 註解
可以使用 > 註解在類級別聲明,以為測試填充數據。
下面展示如何使用 > 註解創建新表並加載初始數據,以供集成測試使用:
@Sql({"/employees_schema.sql", "/import_employees.sql"})
public class SpringBootInitialLoadIntegrationTest {
@Autowired
private EmployeeRepository employeeRepository;
@Test
public void testLoadDataForTestClass() {
assertEquals(3, employeeRepository.findAll().size());
}
}在上述代碼中,我們定義了兩個在測試方法之前執行的 SQL 腳本。@Sql 聲明利用了默認的 BEFORE_TEST_METHOD 執行階段。
Spring 6.1 和 Spring Boot 3.2.0 版本引入了對 executionPhase 參數的類級別支持,並提供了 BEFORE_TEST_CLASS 和 AFTER_TEST_CLASS 常量,用於確定腳本是否在測試類之前或之後運行。
讓我們更新 SpringBootInitialLoadIntegrationTest 類並顯式定義執行階段:
@Sql(scripts = {"/employees_schema.sql", "/import_employees.sql"}, executionPhase = BEFORE_TEST_CLASS)
public class SpringBootInitialLoadIntegrationTest {
// ...
}在這裏,我們通過將 executionPhase 的值設置為 BEFORE_TEST_CLASS 來運行 SQL 腳本,在測試類之前。
此外,AFTER_TEST_CLASS 執行階段有助於在測試類之後加載 SQL 腳本。這在我們需要在測試後清除數據庫時可能很有用。
@Sql(scripts = {"/delete_employees_data.sql"}, executionPhase = AFTER_TEST_CLASS)
public class SpringBootInitialLoadIntegrationTest {
// ...
}值得注意的是,這種配置不能被方法級別的腳本和語句覆蓋。相反,該腳本將在執行方法級別的腳本和語句的同時執行。
6.2. 在方法級別使用 @Sql 註解
我們將通過註解該方法來加載特定測試用例所需的額外數據:
@Test
@Sql({"/import_senior_employees.sql"})
public void testLoadDataForTestCase() {
assertEquals(5, employeeRepository.findAll().size());
}在此,SQL腳本在測試方法執行之前被執行。
再次,我們可以使用 BEFORE_TEST_METHOD 或 AFTER_TEST_METHOD 常量在方法級別明確定義執行階段:
@Test
@Sql(scripts = {"/import_senior_employees.sql"}, executionPhase = BEFORE_TEST_METHOD)
public void testLoadDataForTestCase() {
assertEquals(5, employeeRepository.findAll().size());
}AFTER_TEST_METHOD執行階段有助於在測試方法執行後加載SQL腳本。例如,我們可以利用它在測試方法執行後刪除數據庫表。
默認情況下,在方法級別聲明的@Sql註解會覆蓋類級別聲明的@Sql聲明。在這種情況下,方法級別的@Sql聲明優先於類級別定義的SQL:
@Sql(scripts = {"/employees_schema.sql", "/import_employees.sql"})
public class SpringBootInitialLoadIntegrationTest {
@Autowired
private EmployeeRepository employeeRepository;
@Test
@Sql(scripts = {"/import_senior_employees.sql"})
public void testLoadDataForTestClass() {
assertEquals(5, employeeRepository.findAll().size());
}
}這裏僅執行 import_seioner_employees.sql,僅在運行測試時才會執行。
但是,我們可以通過使用 @SqlMergeMode 聲明進一步配置此行為,該聲明有助於將方法級別的 @Sql 聲明與類級別的 @Sql 聲明合併。
7. @SqlConfig
我們可以通過使用@SqlConfig 註解</em title="SqlConfig">來配置我們解析和運行 SQL 腳本的方式。
@SqlConfig 可以聲明在類級別,作為全局配置。或者我們可以使用它來配置特定的@Sql 註解。
讓我們來看一個示例,其中我們還指定了 SQL 腳本的編碼以及執行腳本的事務模式:
@Test
@Sql(scripts = {"/import_senior_employees.sql"},
config = @SqlConfig(encoding = "utf-8", transactionMode = TransactionMode.ISOLATED))
public void testLoadDataForTestCase() {
assertEquals(5, employeeRepository.findAll().size());
}讓我們來看一下屬性 @SqlConfig 的各種屬性:
blockCommentStartDelimiter– 用於標識 SQL 腳本文件中註釋的起始分隔符blockCommentEndDelimiter– 用於表示 SQL 腳本文件中註釋的結束分隔符commentPrefix– 用於標識 SQL 腳本文件中單行註釋的前綴dataSource– 指定腳本和語句將要運行的javax.sql.DataSourcebean 的名稱encoding– SQL 腳本文件的編碼;默認值為平台編碼errorMode– 在運行腳本時遇到錯誤時使用的模式separator– 用於分隔單個語句的字符串;默認值為 “–“transactionManager– 指定將用於事務的PlatformTransactionManagerbean 的名稱transactionMode– 在事務模式下執行腳本時使用的模式
8.
Java 8 及更高版本允許使用重複註解。我們可以利用此功能來處理 註解。對於 Java 7 及以下版本,存在一個容器註解—— 。
使用 註解,我們將聲明多個 註解:
@SqlGroup({
@Sql(scripts = "/employees_schema.sql",
config = @SqlConfig(transactionMode = TransactionMode.ISOLATED)),
@Sql("/import_employees.sql")})
public class SpringBootSqlGroupAnnotationIntegrationTest {
@Autowired
private EmployeeRepository employeeRepository;
@Test
public void testLoadDataForTestCase() {
assertEquals(3, employeeRepository.findAll().size());
}
}9. 結論
在本文中,我們瞭解到如何利用 <em>schema.sql</em> 和 <em>data.sql</em> 文件來設置初始模式並填充數據。
我們還探討了如何使用 <em>@Sql</em>、<em>@SqlConfig</em> 和 <em> @SqlGroup </em> 註解來加載測試數據。
請注意,這種方法更適合基本和簡單的場景,任何高級數據庫處理都需要更高級和精細的工具,例如 Liquibase 或 Flyway。