如何在 Spring Boot 中使用 H2 数据库

204 阅读7分钟

一个用 Groovy 编程语言编写的示例应用程序,并演示如何将 H2 关系数据库 (H2 DB/H2) 与 Spring Boot 一起使用。

在这种情况下,使用 Groovy 编程语言的好处是,它允许示例只提供一个文件,其中包含运行应用程序所需的一切。

H2 数据库引擎是一个功能强大的开源关系数据库,它是用 Java 编程语言编写的,在软件开发项目中经常使用,尤其是在测试应用程序时。

将 H2 与 Spring Framework 和 Spring Boot 一起使用,是我们将在这里演示的用例。

在下一节中,我们将查看该示例,并详细剖析每个步骤中发生的情况。

GitHub 上带有 Spring Boot 示例的 H2

此处包含指向 GitHub gist 的链接,该要点与用于演示将 H2 关系数据库与 Spring Boot 连接的示例有关。

在本文后面,我还添加了完整的示例,您应该能够将其粘贴到 groovyConsole 中并按原样运行。

Maven 依赖项

此示例中使用了以下依赖项:

  1. Spring 启动
  2. Spring Boot 自动配置
  3. Spring JDBC
  4. H2 数据库引擎
  5. Javax 注解 API
  6. SLF4J 简单提供程序

Groovy Grape 依赖项管理系统应该在执行脚本时自动找到这些依赖项,但是为了参考目的,我已将这些依赖项包含在此处。

与将 H2 数据库引擎与 Spring Boot 一起使用的示例

在本节中,我们将更详细地查看脚本,并介绍每个步骤中发生的情况。

先决条件

若要运行此示例,需要满足以下条件:

  • Java 版本 22.0.1(必需)— 请参阅 OpenJDK on java.net 和 Oracle Java SE Development Kit 22.0.1 (JDK 22.0.1)
  • Groovy 4.0.17 (必需)
  • groovyConsole(可选)

此脚本可以单独使用 Groovy 执行;因此,groovyConsole 是可选的

该脚本使用 Groovy 自适应打包引擎 (Groovy Grape) 从 Maven Central 拉取依赖项,因此还需要连接到 Internet。

我在这里提供了一个示例,说明从命令行运行此脚本时输出应该是什么样子。

使用 H2 数据库执行多个 CRUD 操作的 Spring Boot 示例脚本的成功示例输出。

具有 Spring Boot 成功的 CRUD 示例输出的 H2 DB

红色箭头指向用于运行脚本的命令,橙色箭头指向指示脚本正在启动的日志语句,蓝色箭头指向指示脚本已完成运行的日志语句。

在此示例中,脚本运行一个 Spring Boot 应用程序,该应用程序在 H2 数据库中创建一个表,对该表执行多个 CRUD(、)操作,然后删除该表。create``read``update``delete

Groovy 脚本成功运行完成,然后退出。

步骤 1:声明包

当我们定义 Spring Boot 应用程序时,我们将包括 scanBasePackages 设置,它需要一个包名称,因此我们在这里设置它。

步骤一:为此脚本定义包。在步骤 9 中需要一个包进行扫描,因此我们在这里定义它。

第 2 步:添加 Groovy Grape GrabConfig 注解

在第二步中,我们需要添加 Groovy Grape GrabConfig 注解,并将 systemClassLoader 属性设置为 true——如果我们没有这个属性,则在执行脚本时会抛出异常。

Groovy 中的注释 @GrabConfig(systemClassLoader=true) 将依赖项管理器配置为使用系统类加载器,从而确保整个 Java 虚拟机 (JVM) 都可以访问依赖项。

第 3 步:获取依赖项并导入所需的类

在第三步中,我们需要获取运行此示例所需的依赖项,并导入所需的类 - 这包括 Spring Boot、H2 数据库和其他支持类。

请注意,在此示例中,我们使用的是 Hikari 数据库驱动程序。

第三步:获取依赖项并导入类,包括 H2 数据库、Spring Boot 和其他所需的框架。

有关完整的详细信息,请参阅本文中的部分。

步骤4:获取对SLF4J记录器的引用

在此示例中,我们使用 SLF4J 日志委派框架,我们将消息发送到控制台输出,以便我们可以在脚本执行时查看发生的情况。

HikariCP 依赖项是我们使用的另一个框架,它也使用 SLF4J,我们在此示例中包含了这个高性能连接池实现。

第四步:获取 SLF4J 记录器的实例。

步骤 5:配置 H2 数据库数据源和 JdbcTemplate Bean

在第五步中,我们将配置 H2 数据库数据源,该数据源利用 HikariCP 高性能连接池依赖项作为数据源类型。

由于此示例演示了从 Spring Boot 应用程序对 H2 数据库执行的一些简单 CRUD 操作,因此我们还将配置一个使用此数据源的 here 实例。JdbcTemplate

请注意,我们将该类指定为数据源类型。HikariDataSource

此示例中配置的 H2 数据库实例将仅驻留在内存中 — 如果我们想将此信息保存到磁盘,则需要更改 URL。

Spring Boot 应用程序的配置类,用于定义两个 bean:一个是使用 H2 内存数据库和 HikariCP 连接池的 DataSource bean,另一个是用于数据库操作的 JdbcTemplate bean。

步骤 6:创建存储库类

在此步骤中,我们实现了一个存储库,其中包含 CRUD 操作,我们可以通过 在 H2 数据库实例上执行这些操作,在本例中,该操作由 Spring Boot 自动连接。JdbcTemplate

Spring 存储库类,用于管理 H2 数据库表,并使用 JdbcTemplate bean 创建、删除、添加、更新和读取 (CRUD) 记录。

步骤 7:实现服务 Bean

在此步骤中,我们将实现一个事务性服务 Bean,它具有停止和启动生命周期方法以及委托给存储库 Bean 的便利方法。

当 Spring Boot 初始化容器正在管理的 bean 时,start 方法会在 H2 中创建示例表,并且 stop 方法会在容器停止之前删除示例表。

ExampleService 中定义的其他方法提供了便利并隐藏了实现详细信息。

使用服务有助于重用,在测试我们的代码时也很有帮助。

Spring 服务类,具有用于管理 H2 数据库操作的事务方法,包括创建和删除表以及执行 CRUD 操作。

由于图像已被截断,请参阅下面的完整示例或查看 GitHub Gist

步骤 8:实现 Spring Boot CommandLineRunner 接口

在此步骤中,我们将实现 Spring Boot 规范。CommandLineRunner

我们的实现包括通过步骤 7 中创建的服务针对 H2 数据库执行 CRUD 操作。

在此过程中,我们会记录一些信息,以便我们可以看到每个 CRUD 操作完成时会发生什么。

在应用程序启动时运行的 Spring 组件类,通过 ExampleService 执行数据库操作并记录结果。

步骤 9:配置用于组件扫描的 Spring Boot 应用程序

代码段中的代码定义了 Spring Boot 应用程序,并指定了组件扫描的基本包。

Spring Boot 和 H2 数据库集成示例应用程序类,为特定包配置了组件扫描。

步骤十:配置并运行 Spring Boot 应用程序

此代码段中的代码使用以下配置配置并运行 Spring Boot 应用程序:

  1. 初始化 :使用 Spring Boot 应用程序创建构建器SpringApplicationBuilder``H2SpringBootExampleApplication
  2. 设置配置文件和 Web 应用程序类型:将应用程序配置为使用默认配置文件并禁用 Web 环境(这不是 Web 应用程序,因此我们不需要它)
  3. 设置父上下文:将 、 、 和 类指定为父上下文中的组件BeanConfiguration``ExampleRepository``ExampleService``ExampleCommandLineRunner
  4. 运行应用程序:使用提供的参数执行应用程序
  5. 关闭上下文:关闭应用程序上下文 — 此步骤确保在 Spring Boot 示例应用程序退出之前调用服务中的停止生命周期方法(请参阅步骤 6),从而导致 H2 DB 中的 names 表被删除。

最后,脚本记录完成消息,然后退出。

使用特定的配置文件和组件配置并运行 Spring Boot 应用程序,然后关闭上下文并记录完成。

下一节包括完整的 Spring Boot with the H2 Database 示例脚本。

使用 H2 数据库引擎的 Spring Boot 完整示例

在这里,我提供了整个脚本的副本,您应该能够使用 groovyConsole 或通过带有 groovy shell 的命令行运行它。

/*
 * Precondition:
 *
 * - Java version "22.0.1" 2024-04-16
 * - Groovy version 4.0.17
 */
package com.thospfuller.examples.h2.database.spring.boot

@GrabConfig(systemClassLoader=true)

@Grab(group='org.springframework.boot', module='spring-boot', version='3.3.0')
@Grab(group='org.springframework.boot', module='spring-boot-autoconfigure', version='3.3.0')
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.context.annotation.Configuration
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
import org.springframework.stereotype.Component
import org.springframework.boot.jdbc.DataSourceBuilder

@Grab(group='org.springframework', module='spring-jdbc', version='6.1.8')
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter
import org.springframework.transaction.annotation.Transactional
import org.springframework.context.annotation.Bean
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.WebApplicationType
import org.springframework.stereotype.Service

@Grab(group='com.h2database', module='h2', version='2.2.224')
@Grab(group='com.zaxxer', module='HikariCP', version='5.1.0')
import com.zaxxer.hikari.HikariDataSource
import javax.sql.DataSource
import java.sql.PreparedStatement

@Grab(group='javax.annotation', module='javax.annotation-api', version='1.3.2')
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy

@Grab(group='org.slf4j', module='slf4j-simple', version='2.0.9')
import org.slf4j.LoggerFactory

def log = LoggerFactory.getLogger(this.class)

log.info "H2 Database with Spring Boot example begins; args: $args"

@Configuration
class BeanConfiguration {

  @Bean
  DataSource getDataSource () {

    return DataSourceBuilder
      .create()
      .driverClassName("org.h2.Driver")
      .type(HikariDataSource)
      .url("jdbc:h2:mem:example-db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE")
      .username("sa")
      .password("sa")
    .build()
  }

  @Bean  
  JdbcTemplate getJdbcTemplate (DataSource dataSource) {
    return new JdbcTemplate (dataSource)
  }
}

@Repository
class ExampleRepository {

  private static final def log = LoggerFactory.getLogger(ExampleRepository)

  static final def TABLE_NAME = "NAMES"

  @Autowired
  private JdbcTemplate jdbcTemplate

  void createExampleTable () {

    log.info "createExampleTable: method begins."

    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR(255));")

    log.info "createExampleTable: method ends."
  }

  def deleteExampleTable () {

    log.info "deleteExampleTable: method begins."

    jdbcTemplate.execute("DROP TABLE IF EXISTS ${TABLE_NAME};")

    log.info "deleteExampleTable: method ends."
  }

  def addNames (String... names) {
    return addNames (names as List<String>)
  }

  def addNames (List<String> nameList) {

    return jdbcTemplate.batchUpdate(
      "INSERT INTO ${TABLE_NAME} (name) VALUES (?);",
      nameList,
      nameList.size (),
      { PreparedStatement preparedStatement, String name ->
        preparedStatement.setString(1, name)
      } as ParameterizedPreparedStatementSetter<String>
    )
  }

  def updateName (int id, String newName) {

    return jdbcTemplate.update(
      "UPDATE ${TABLE_NAME} SET NAME = ? WHERE ID = ?;",
      newName,
      id
    )
  }

  def deleteName (int id) {

    return jdbcTemplate.update(
      "DELETE FROM ${TABLE_NAME} WHERE ID = ?;",
      id
    )
  }

  def readNames () {
    return jdbcTemplate.queryForList ("select name from ${TABLE_NAME}")
  }
}

@Service
@Transactional
class ExampleService {

  private static final def log = LoggerFactory.getLogger(ExampleService)

  @Autowired
  def exampleRepository

  @PostConstruct
  def start () {

    log.info "start: method begins."

    exampleRepository.createExampleTable ()

    log.info "start: method ends."
  }

  @PreDestroy
  def stop () {

    log.info "stop: method begins."

    exampleRepository.deleteExampleTable ()

    log.info "stop: method ends."
  }

  def addNames (String... nameList) {
    return exampleRepository.addNames (nameList)
  }

  def updateName (int id, String newName) {
    return exampleRepository.updateName (id, newName)
  }

  def deleteName (int id) {
    return exampleRepository.deleteName (id)
  }

  def readNames () {
    return exampleRepository.readNames ()
  }
}

@Component
class ExampleCommandLineRunner implements CommandLineRunner {

  private static final def log = LoggerFactory.getLogger(H2SpringBootExampleApplication)

  @Autowired
  private def exampleService

  @Override
  public void run (String... args) {

    log.info "run: method begins; args: $args"

    def namesAdded = exampleService.addNames ("aaa", "bbb", "ccc", "ddd")

    log.info "namesAdded: $namesAdded"

    def updateResult = exampleService.updateName (2, "ZZZ")

    def names = exampleService.readNames ()

    log.info "updateResult: $updateResult, names after update: $names"

    def deletedNames = exampleService.deleteName (2)

    names = exampleService.readNames ()

    log.info "deletedNames: $deletedNames, names after deletion: $names"

    log.info "run: method ends."
  }
}

@SpringBootApplication(scanBasePackages = ["com.thospfuller.examples.h2.database.spring.boot"])
class H2SpringBootExampleApplication {}

def springApplicationBuilder = new SpringApplicationBuilder(H2SpringBootExampleApplication)

def context = springApplicationBuilder
  .profiles("default")
  .web(WebApplicationType.NONE)
  .parent (
    BeanConfiguration,
    ExampleRepository,
    ExampleService,
    ExampleCommandLineRunner
  )
  .run(args)

context.close ()

log.info "...done!"

return