初始化 DataSource
infra.jdbc.datasource.init 包提供了对初始化现有 DataSource 的支持。嵌入式数据库支持提供了为应用程序创建和初始化 DataSource 的一种选择。但是,您有时可能需要初始化运行在某处服务器上的实例。
使用 Infra XML 初始化数据库
如果您想初始化数据库并且可以提供对 DataSource bean 的引用,则可以使用 infra-jdbc 命名空间中的 initialize-database 标签:
<jdbc:initialize-database data-source="dataSource">
<jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
<jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>
前面的示例针对数据库运行两个指定的脚本。第一个脚本创建模式,第二个脚本使用测试数据集填充表。脚本位置也可以是带有通配符的模式,采用 Infra 中用于资源的通常 Ant 样式(例如,classpath*:/com/foo/**/sql/*-data.sql)。如果您使用模式,则脚本将按其 URL 或文件名的字典顺序运行。
数据库初始化器的默认行为是无条件运行提供的脚本。这可能并不总是您想要的——例如,如果您针对已经包含测试数据的数据库运行脚本。通过遵循先创建表然后插入数据的常见模式(如前所示),可以减少意外删除数据的可能性。如果表已存在,则第一步失败。
但是,为了更好地控制现有数据的创建和删除,XML 命名空间提供了一些其他选项。第一个是打开和关闭初始化的标志。您可以根据环境设置此标志(例如从系统属性或环境 bean 中提取布尔值)。以下示例从系统属性获取值:
<jdbc:initialize-database data-source="dataSource"
enabled="#{systemProperties.INITIALIZE_DATABASE}"> (1)
<jdbc:script location="..."/>
</jdbc:initialize-database>
| 1 | 从名为 INITIALIZE_DATABASE 的系统属性获取 enabled 的值。 |
控制现有数据发生情况的第二个选项是对故障更加宽容。为此,您可以控制初始化器忽略它从脚本运行的 SQL 中的某些错误的能力,如下例所示:
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<jdbc:script location="..."/>
</jdbc:initialize-database>
在前面的示例中,我们说我们预计有时脚本会针对空数据库运行,因此脚本中有一些 DROP 语句会失败。因此,失败的 SQL DROP 语句将被忽略,但其他失败将导致异常。如果您的 SQL 方言不支持 DROP … IF EXISTS(或类似内容),但您想在重新创建之前无条件删除所有测试数据,这很有用。在这种情况下,第一个脚本通常是一组 DROP 语句,后跟一组 CREATE 语句。
ignore-failures 选项可以设置为 NONE(默认值)、DROPS(忽略失败的删除)或 ALL(忽略所有失败)。
如果脚本中根本不存在 ; 字符,则每个语句应由 ; 或新行分隔。您可以全局控制或按脚本控制,如下例所示:
<jdbc:initialize-database data-source="dataSource" separator="@@"> (1)
<jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/> (2)
<jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/>
<jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>
| 1 | 将分隔符脚本设置为 @@。 |
| 2 | 将 db-schema.sql 的分隔符设置为 ;。 |
在此示例中,两个 test-data 脚本使用 @@ 作为语句分隔符,只有 db-schema.sql 使用 ;。此配置指定默认分隔符为 @@,并覆盖 db-schema 脚本的默认值。
如果您需要比从 XML 命名空间获得的更多控制,您可以直接使用 DataSourceInitializer 并将其定义为应用程序中的组件。
初始化依赖于数据库的其他组件
一大类应用程序(那些在 Infra 上下文启动之前不使用数据库的应用程序)可以使用数据库初始化器,而不会出现进一步的并发症。如果您的应用程序不是其中之一,您可能需要阅读本节的其余部分。
数据库初始化器依赖于 DataSource 实例,并运行其初始化回调中提供的脚本(类似于 XML bean 定义中的 init-method、组件中的 @PostConstruct 方法或实现 InitializingBean 的组件中的 afterPropertiesSet() 方法)。如果其他 bean 依赖于相同的数据源并在初始化回调中使用该数据源,则可能会出现问题,因为数据尚未初始化。这方面的一个常见示例是在应用程序启动时急切初始化并从数据库加载数据的缓存。
要解决此问题,您有两个选择:将缓存初始化策略更改为较晚的阶段,或者确保首先初始化数据库初始化器。
如果应用程序在您的控制之下,而不是其他情况,那么更改缓存初始化策略可能很容易。关于如何实现这一点的一些建议包括:
-
使缓存在首次使用时延迟初始化,这可以缩短应用程序启动时间。
-
让您的缓存或初始化缓存的单独组件实现
Lifecycle或SmartLifecycle。当应用程序上下文启动时,您可以通过设置其autoStartup标志来自动启动SmartLifecycle,并且可以通过在封闭上下文上调用ConfigurableApplicationContext.start()来手动启动Lifecycle。 -
使用 Infra
ApplicationEvent或类似的自定义观察者机制来触发缓存初始化。ContextRefreshedEvent总是在上下文准备好使用时(所有 bean 初始化之后)由上下文发布,因此这通常是一个有用的钩子(这就是SmartLifecycle默认工作的方式)。
确保首先初始化数据库初始化器也很容易。关于如何实现这一点的一些建议包括:
-
依赖 Infra
BeanFactory的默认行为,即按注册顺序初始化 bean。您可以通过采用 XML 配置中一组<import/>元素的常见做法来轻松安排这一点,这些元素对您的应用程序模块进行排序,并确保首先列出数据库和数据库初始化。 -
将
DataSource和使用它的业务组件分开,并通过将它们放在单独的ApplicationContext实例中来控制它们的启动顺序(例如,父上下文包含DataSource,子上下文包含业务组件)。这种结构在 Infra Web 应用程序中很常见,但可以更普遍地应用。