0. 为啥感觉升级了 win11 之后,电脑像是刚买回来的,很快
这篇加餐完全是一个意外:时隔两年半,再看 Springboot-quartz-starter
集成实现的时候,不知道为啥我的h2 在应用启动的时候,不能自动创建quartz相关的schema。后面看了 springboot 的文档,据说是可以做到的,AI也是这么说的。
没办法,只能看 QuartzAutoConfiguration
源码了。于是乎,就有了这么个好活
没办法,就当是一个支线任务了
1. AbstractScriptDatabaseInitializer
下面是熟悉的,阉割后的 源码
package org.springframework.boot.sql.init;
/**
* Base class for an {@link InitializingBean} that performs SQL database initialization
* using schema (DDL) and data (DML) scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public abstract class AbstractScriptDatabaseInitializer implements ResourceLoaderAware, InitializingBean {
// 构造入参配置
private final DatabaseInitializationSettings settings;
private volatile ResourceLoader resourceLoader;
@Override
public void afterPropertiesSet() throws Exception {
// 初始化后,就执行逻辑了
initializeDatabase();
}
/**
* Initializes the database by applying schema and data scripts.
* @return {@code true} if one or more scripts were applied to the database, otherwise
* {@code false}
*/
public boolean initializeDatabase() {
ScriptLocationResolver locationResolver = new ScriptLocationResolver(this.resourceLoader);
// 先后执行 schema, data 的脚本
boolean initialized = applySchemaScripts(locationResolver);
return applyDataScripts(locationResolver) || initialized;
}
// 真正执行脚本前,会走这个判断,决定是否要执行脚本
private boolean isEnabled() {
if (this.settings.getMode() == DatabaseInitializationMode.NEVER) {
return false;
}
return this.settings.getMode() == DatabaseInitializationMode.ALWAYS || isEmbeddedDatabase();
}
/**
* Returns whether the database that is to be initialized is embedded.
* @return {@code true} if the database is embedded, otherwise {@code false}
* @since 2.5.1
*/
protected boolean isEmbeddedDatabase() {
throw new IllegalStateException(
"Database initialization mode is '" + this.settings.getMode() + "' and database type is unknown");
}
private boolean applySchemaScripts(ScriptLocationResolver locationResolver) {
return applyScripts(this.settings.getSchemaLocations(), "schema", locationResolver);
}
private boolean applyDataScripts(ScriptLocationResolver locationResolver) {
return applyScripts(this.settings.getDataLocations(), "data", locationResolver);
}
private boolean applyScripts(List<String> locations, String type, ScriptLocationResolver locationResolver) {
List<Resource> scripts = getScripts(locations, type, locationResolver);
if (!scripts.isEmpty() && isEnabled()) {
runScripts(scripts);
return true;
}
return false;
}
// 根据配置的 路径的字符串 -> spring.Resource 类型
private List<Resource> getScripts(List<String> locations, String type, ScriptLocationResolver locationResolver) {
if (CollectionUtils.isEmpty(locations)) {
return Collections.emptyList();
}
List<Resource> resources = new ArrayList<>();
for (String location : locations) {
for (Resource resource : doGetResources(location, locationResolver)) {
if (resource.exists()) {
resources.add(resource);
}
}
}
return resources;
}
private List<Resource> doGetResources(String location, ScriptLocationResolver locationResolver) {
return locationResolver.resolve(location);
}
private void runScripts(List<Resource> resources) {
runScripts(resources, this.settings.isContinueOnError(), this.settings.getSeparator(),
this.settings.getEncoding());
}
protected abstract void runScripts(List<Resource> resources, boolean continueOnError, String separator,
Charset encoding);
private static class ScriptLocationResolver {
private final ResourcePatternResolver resourcePatternResolver;
private List<Resource> resolve(String location) throws IOException {
// ...
}
}
}
再看几个它的实现类,加载上配置类,基本上,可以知道它的使用方法了
2. 吾のDemo
始于测试类
package org.pajamas.spring.boot;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.pajamas.example.starter.core.entity.AlbumEntity;
import org.pajamas.example.starter.core.repo.AlbumRepo;
import org.pajamas.example.test.AbstractApplicationTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.TestPropertySource;
import java.util.List;
/**
* @author william
* @since 2024/5/30
*/
@DisplayName("what the interesting component")
@TestPropertySource(properties = {
"spring.application.name=service-example-test",
// 屏蔽 liquibase 的干扰
"spring.liquibase.enabled=false"
})
@Import(ExampleDatabaseInitializer.class)
public class DatabaseInitializerTest extends AbstractApplicationTest {
// 其实就,一个 jpa 实体类的 repository
@Autowired
AlbumRepo repo;
// @Disabled
@DisplayName("execute DDL, DML automatically, as App startup")
@Test
public void t0() throws Exception {
// 预期的结果:启动启动时,自动创建表,并插入一条记录
List<AlbumEntity> all = this.repo.findAll();
printErr(all);
}
}
既然是测试,就走简单的方式,注册这个bean
package org.pajamas.spring.boot;
import org.springframework.boot.autoconfigure.sql.init.SqlDataSourceScriptDatabaseInitializer;
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
import org.springframework.boot.sql.init.DatabaseInitializationMode;
import java.util.Collections;
import javax.sql.DataSource;
/**
* @author william
* @since 2024/5/30
*/
public class ExampleDatabaseInitializer extends SqlDataSourceScriptDatabaseInitializer {
public ExampleDatabaseInitializer(DataSource dataSource) {
super(dataSource, getProperty());
}
private static SqlInitializationProperties getProperty() {
SqlInitializationProperties properties = new SqlInitializationProperties();
properties.setSchemaLocations(Collections.singletonList("classpath:sql/schema.sql"));
properties.setDataLocations(Collections.singletonList("classpath:sql/data.sql"));
properties.setMode(DatabaseInitializationMode.ALWAYS);
properties.setContinueOnError(false);
return properties;
}
}
schema.sql
CREATE TABLE IF NOT EXISTS `t_album`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`album_name` varchar(32) DEFAULT NULL COMMENT 'album name',
`album_year` int DEFAULT NULL COMMENT 'album publish year',
`create_date` timestamp NULL DEFAULT NULL,
`create_user_id` bigint DEFAULT NULL,
`update_date` timestamp NULL DEFAULT NULL,
`update_user_id` bigint DEFAULT NULL,
`ver` int NOT NULL DEFAULT '0',
`del` bigint NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_album_id_del` (`id`,`del`)
) COMMENT='album table';
CREATE TABLE IF NOT EXISTS `t_artist`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`artist_name` varchar(32) DEFAULT NULL COMMENT 'artist name',
`artist_from` varchar(32) DEFAULT NULL COMMENT 'shorten of country name',
`create_date` timestamp NULL DEFAULT NULL,
`create_user_id` bigint DEFAULT NULL,
`update_date` timestamp NULL DEFAULT NULL,
`update_user_id` bigint DEFAULT NULL,
`ver` int NOT NULL DEFAULT '0',
`del` bigint NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_artist_id_del` (`id`,`del`)
) COMMENT='artist table';
data.sql
insert into
`t_album`
(
`album_name`,
`album_year`,
`create_user_id`,
`update_user_id`
)
values
(
'Boomerang',
2023,
1023,
1023
);
3. 话说回来:为甚么,我的h2没有自动创建quartz的schema
这是springboot.Quartz的实现
接下来,源码启动…
package org.springframework.boot.jdbc.init;
/**
* {@link InitializingBean} that performs {@link DataSource} initialization using schema
* (DDL) and data (DML) scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public class DataSourceScriptDatabaseInitializer extends AbstractScriptDatabaseInitializer {
@Override
protected boolean isEmbeddedDatabase() {
try {
// step into ..
return EmbeddedDatabaseConnection.isEmbedded(this.dataSource);
}
catch (Exception ex) {
logger.debug("Could not determine if datasource is embedded", ex);
return false;
}
}
}
----------
// org.springframework.boot.jdbc.EmbeddedDatabaseConnection
/**
* Convenience method to determine if a given data source represents an embedded
* database type.
* @param dataSource the data source to interrogate
* @return true if the data source is one of the embedded types
*/
public static boolean isEmbedded(DataSource dataSource) {
try {
return new JdbcTemplate(dataSource)
// step into ...
.execute(new IsEmbedded());
}
catch (DataAccessException ex) {
// Could not connect, which means it's not embedded
return false;
}
}
----------
// org.springframework.boot.jdbc.EmbeddedDatabaseConnection.IsEmbedded
@Override
public Boolean doInConnection(Connection connection) throws SQLException, DataAccessException {
DatabaseMetaData metaData = connection.getMetaData();
String productName = metaData.getDatabaseProductName();
if (productName == null) {
return false;
}
productName = productName.toUpperCase(Locale.ENGLISH);
// step into ...
EmbeddedDatabaseConnection[] candidates = EmbeddedDatabaseConnection.values();
for (EmbeddedDatabaseConnection candidate : candidates) {
if (candidate != NONE && productName.contains(candidate.getType().name())) {
// 根据jdbc.url判断是不是一个 嵌入式数据库
String url = metaData.getURL();
return (url == null || candidate.isEmbeddedUrl(url));
}
}
return false;
}
------------
public enum EmbeddedDatabaseConnection {
// H2 判断是否为嵌入式数据的依据
/**
* H2 Database Connection.
*/
H2(EmbeddedDatabaseType.H2, DatabaseDriver.H2.getDriverClassName(),
"jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", (url) -> url.contains(":h2:mem")),
}
破案:我的h2 使用默认的 file(xxx.mv.db) 存储,默认配置下(DatabaseInitializationMode.EMBEDDED
), 只有内存(嵌入式)的数据库会开启这个特性。
- 要么配置
DatabaseInitializationMode.ALWAYS
- 要么使用内存数据库
Anyway, h2支持好多种连接方式,新版本h2, 默认的file模式,采用mv的storeEngine 支持MVCC。所以说,对于quartz这种依赖行锁的要求,也是支持的。
4. 话又说回去… 这个东西对项目的意义是什么
- 可以试下这个:如果你有一个连接数据库的测试环境,或者你的程序很简单,又或者 有特殊的xp(内存数据库)
- 专门数据库的版本控制工具:你的程序比较复杂,或者 本身就需要数据库的版本控制工具(如 Liquibase),运行在严肃的生产环境