3.1.2 使用 JdbcTemplate

在开始使用 JdbcTemplate 之前,需要将它添加到项目类路径中。这很容易通过添加 Spring Boot 的 JDBC starter 依赖来实现:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>

还需要一个存储数据的数据库。出于开发目的,嵌入式数据库也可以。我喜欢 H2 嵌入式数据库,所以我添加了以下依赖进行构建:

  1. <dependency>
  2. <groupId>com.h2database</groupId>
  3. <artifactId>h2</artifactId>
  4. <scope>runtime</scope>
  5. </dependency>

稍后,将看到如何配置应用程序来使用外部数据库。但是现在,让我们继续编写一个获取和保存 Ingredient 数据的存储库。

定义 JDBC 存储库

Ingredient repository 需要执行以下操作:

  • 查询所有的 Ingredient 使之变成一个 Ingredient 的集合对象
  • 通过它的 id 查询单个 Ingredient
  • 保存一个 Ingredient 对象

以下 IngredientRepository 接口将这三种操作定义为方法声明:

  1. package tacos.data;
  2. import tacos.Ingredient;
  3. public interface IngredientRepository {
  4. Iterable<Ingredient> findAll();
  5. Ingredient findOne(String id);
  6. Ingredient save(Ingredient ingredient);
  7. }

尽管该接口体现了需要 Ingredient repository 做的事情的本质,但是仍然需要编写一个使用 JdbcTemplate 来查询数据库的 IngredientRepository 的实现。下面的程序清单是编写实现的第一步。

{% code title=”程序清单 3.4 使用 JdbcTemplate 开始编写 Ingredient repository” %}

  1. package tacos.data;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.jdbc.core.JdbcTemplate;
  4. import org.springframework.jdbc.core.RowMapper;
  5. import org.springframework.stereotype.Repository;
  6. import tacos.Ingredient;
  7. @Repository
  8. public class JdbcIngredientRepository implements IngredientRepository {
  9. private JdbcTemplate jdbc;
  10. @Autowired
  11. public JdbcIngredientRepository(JdbcTemplate jdbc) {
  12. this.jdbc = jdbc;
  13. }
  14. ...
  15. }

{% endcode %}

JdbcIngredientRepository 使用 @Repository 进行了注解。这个注解是 Spring 定义的少数几个原型注解之一,包括 @Controller 和 @Component。通过使用 @Repository 对 JdbcIngredientRepository 进行注解,这样它就会由 Spring 组件在扫描时自动发现,并在 Spring 应用程序上下文中生成 bean 实例。

当 Spring 创建 JdbcIngredientRepository bean 时,通过 @Autowired 注解将 JdbcTemplate 注入到 bean 中。构造函数将 JdbcTemplate 分配给一个实例变量,该变量将在其他方法中用于查询和插入数据库。谈到那些其他方法,让我们来看看 findAll() 和 findById() 的实现。

{% code title=”程序清单 3.5 使用 JdbcTemplate 查询数据库” %}

  1. @Override
  2. public Iterable<Ingredient> findAll() {
  3. return jdbc.query("select id, name, type from Ingredient",
  4. this::mapRowToIngredient);
  5. }
  6. @Override
  7. public Ingredient findOne(String id) {
  8. return jdbc.queryForObject(
  9. "select id, name, type from Ingredient where id=?",
  10. this::mapRowToIngredient, id);
  11. }
  12. private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
  13. throws SQLException {
  14. return new Ingredient(
  15. rs.getString("id"),
  16. rs.getString("name"),
  17. Ingredient.Type.valueOf(rs.getString("type")));
  18. }

{% endcode %}

findAll() 和 findById() 都以类似的方式使用 JdbcTemplate。期望返回对象集合的 findAll() 方法使用了 JdbcTemplate 的 query() 方法。query() 方法接受查询的 SQL 以及 Spring 的 RowMapper 实现,以便将结果集中的每一行映射到一个对象。findAll() 还接受查询中所需的所有参数的列表作为它的最后一个参数。但是,在本例中,没有任何必需的参数。

findById() 方法只期望返回单个成分对象,因此它使用 JdbcTemplate 的 queryForObject() 方法而不是 query()。queryForObject() 的工作原理与 query() 非常相似,只是它返回的是单个对象,而不是对象列表。在本例中,它给出了要执行的查询、一个 RowMapper 和要获取的 Ingredient 的 id,后者用于代替查询 SQL 中 的 ?

如程序清单 3.5 所示,findAll() 和 findById() 的 RowMapper 参数作为 mapRowToIngredient() 方法的方法引用。当使用 JdbcTemplate 作为显式 RowMapper 实现的替代方案时,使用 Java 8 的方法引用和 lambda 非常方便。但是,如果出于某种原因,想要或是需要一个显式的 RowMapper,那么 findAll() 的以下实现将展示如何做到这一点:

  1. @Override
  2. public Ingredient findOne(String id) {
  3. return jdbc.queryForObject(
  4. "select id, name, type from Ingredient where id=?",
  5. new RowMapper<Ingredient>() {
  6. public Ingredient mapRow(ResultSet rs, int rowNum)
  7. throws SQLException {
  8. return new Ingredient(
  9. rs.getString("id"),
  10. rs.getString("name"),
  11. Ingredient.Type.valueOf(rs.getString("type")));
  12. };
  13. }, id);
  14. }

从数据库读取数据只是问题的一部分。在某些情况下,必须将数据写入数据库以便能够读取。因此,让我们来看看如何实现 save() 方法。

插入一行

JdbcTemplate 的 update() 方法可用于在数据库中写入或更新数据的任何查询。并且,如下面的程序清单所示,它可以用来将数据插入数据库。

{% code title=”程序清单 3.6 使用 JdbcTemplate 插入数据” %}

  1. @Override
  2. public Ingredient save(Ingredient ingredient) {
  3. jdbc.update(
  4. "insert into Ingredient (id, name, type) values (?, ?, ?)",
  5. ingredient.getId(),
  6. ingredient.getName(),
  7. ingredient.getType().toString());
  8. return ingredient;
  9. }

{% endcode %}

因为没有必要将 ResultSet 数据映射到对象,所以 update() 方法要比 query() 或 queryForObject() 简单得多。它只需要一个包含 SQL 的字符串来执行,以及为任何查询参数赋值。在本例中,查询有三个参数,它们对应于 save() 方法的最后三个参数,提供了 Ingredient 的 id、name 和 type。

完成了 JdbcIngredientRepository后,现在可以将其注入到 DesignTacoController 中,并使用它来提供一个 Ingredient 对象列表,而不是使用硬编码的值(正如第 2 章中所做的那样)。DesignTacoController 的变化如下所示。

{% code title=”程序清单 3.7 在控制器中注入并使用 repository” %}

  1. @Controller
  2. @RequestMapping("/design")
  3. @SessionAttributes("order")
  4. public class DesignTacoController {
  5. private final IngredientRepository ingredientRepo;
  6. @Autowired
  7. public DesignTacoController(IngredientRepository ingredientRepo) {
  8. this.ingredientRepo = ingredientRepo;
  9. }
  10. @GetMapping
  11. public String showDesignForm(Model model) {
  12. List<Ingredient> ingredients = new ArrayList<>();
  13. ingredientRepo.findAll().forEach(i -> ingredients.add(i));
  14. Type[] types = Ingredient.Type.values();
  15. for (Type type : types) {
  16. model.addAttribute(type.toString().toLowerCase(),
  17. filterByType(ingredients, type));
  18. }
  19. return "design";
  20. }
  21. ...
  22. }

{% endcode %}

请注意,showDesignForm() 方法的第 2 行现在调用了注入的 IngredientRepository 的 findAll() 方法。findAll() 方法从数据库中提取所有 Ingredient,然后将它们对应到到模型的不同类型中。

几乎已经准备好启动应用程序并尝试这些更改了。但是在开始从查询中引用的 Ingredient 表读取数据之前,可能应该创建这个表并写一些 Ingredient 数据进去。