3.1.2 使用 JdbcTemplate
在开始使用 JdbcTemplate 之前,需要将它添加到项目类路径中。这很容易通过添加 Spring Boot 的 JDBC starter 依赖来实现:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
还需要一个存储数据的数据库。出于开发目的,嵌入式数据库也可以。我喜欢 H2 嵌入式数据库,所以我添加了以下依赖进行构建:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
默认情况下,数据库名称是随机生成的。但这使得很难确定数据库连接 URL。因为需要使用 H2 控制台连接到数据库(使用 Spring Boot DevTools 访问 http://localhost:8080/h2-console,所以指定确定的数据库名称这是个好主意。这通过在 application.properties 中设置两个属性来实现:
spring.datasource.generate-unique-name=false
spring.datasource.name=tacocloud
或者,如果愿意,将 application.properties 重命名为 application.yml 并添加 YAML 格式的属性:
spring:
datasource:
generate-unique-name: false
name: tacocloud
属性文件格式和 YAML 格式之间的选择取决于您。Spring Boot 都支持。鉴于 YAML 的结构增加了可读性,本书所有部分我们都将使用 YAML 格式来配置。
通过将 spring.datasource.generate-unique-name 属性设置为 false,我们可以看出 Spring 不再为数据库生成随机值。相反,它应该使用 spring.datasource.name 属性的值。在这种情况下,数据库名称将是“tacocloud”。因此,数据库 URL 将是 “"jdbc:h2:mem:tacocloud”,您可以在 JDBC UR L中指定 H2 控制台连接。
稍后,将看到如何配置应用程序来使用外部数据库。但是现在,让我们继续编写一个获取和保存 Ingredient 数据的存储库。
定义 JDBC 存储库
Ingredient repository 需要执行以下操作:
- 查询所有的 Ingredient 使之变成一个 Ingredient 的集合对象
- 通过它的 id 查询单个 Ingredient
- 保存一个 Ingredient 对象
以下 IngredientRepository 接口将这三种操作定义为方法声明:
package tacos.data;
import java.util.Optional;
import tacos.Ingredient;
public interface IngredientRepository {
Iterable<Ingredient> findAll();
Optional<Ingredient> findById(String id);
Ingredient save(Ingredient ingredient);
}
尽管该接口体现了需要 Ingredient repository 做的事情的本质,但是仍然需要编写一个使用 JdbcTemplate 来查询数据库的 IngredientRepository 的实现。下面的程序清单是编写实现的第一步。
程序清单 3.4 使用 JdbcTemplate 开始编写 Ingredient repository
package tacos.data;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import tacos.Ingredient;
@Repository
public class JdbcIngredientRepository implements IngredientRepository {
private JdbcTemplate jdbcTemplate;
public JdbcIngredientRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// ...
}
可以看到,JdbcIngredientRepository 使用了 @Repository
注解。这个注解是 Spring 定义的少数几个原型注解之一,包括 @Controller
和 @Component
。通过使用 @Repository
对 JdbcIngredientRepository 进行注解,这样它就会由 Spring 组件在扫描时自动发现,并在 Spring 应用程序上下文中生成 bean 实例。
当 Spring 创建 JdbcIngredientRepository bean 时,Spring会向bean注入JdbcTemplate.这是因为该类只有一个构造方法,Spring会通过构造方法的参数来隐式调用自动装配依赖。如果类里的构造方法不止一个,或者你想让构造方法被显式地自动装配,那么你需要在构造方法上添加@Autowired注解。
@Autowired
public JdbcIngredientRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
构造函数将 JdbcTemplate 分配给一个实例变量,该变量将在其他方法中用于查询和插入数据库。谈到那些其他方法,让我们来看看 findAll()
和 findById()
的实现。
程序清单 3.5 使用 JdbcTemplate 查询数据库
@Override
public Iterable<Ingredient> findAll() {
return jdbcTemplate.query(
"select id, name, type from Ingredient",
this::mapRowToIngredient);
}
@Override
public Optional<Ingredient> findById(String id) {
List<Ingredient> results = jdbcTemplate.query(
"select id, name, type from Ingredient where id=?",
this::mapRowToIngredient,
id);
return results.size() == 0 ?
Optional.empty() :
Optional.of(results.get(0));
}
private Ingredient mapRowToIngredient(ResultSet row, int rowNum)
throws SQLException {
return new Ingredient(
row.getString("id"),
row.getString("name"),
Ingredient.Type.valueOf(row.getString("type")));
}
findAll()
和 findById()
都以类似的方式使用 JdbcTemplate。期望返回对象集合的 findAll()
方法使用了 JdbcTemplate 的 query()
方法。query()
方法接受查询的 SQL 以及 Spring 的 RowMapper 实现,以便将结果集中的每一行映射到一个对象。findAll()
还接受查询中所需的所有参数的列表作为它的最后一个参数。但是,在本例中,没有任何必需的参数。
相反, 相反,findById()
方法需要在其查询中包含 where 子句,以比较 id 列的值与传递给方法的 id 参数的值。因此对 query()
的调用包括 id 参数。当查询执行时,“?”将被替换为此值。
如程序清单 3.5 所示,findAll()
和 findById()
的 RowMapper 参数作为 mapRowToIngredient()
方法的方法引用。当使用 JdbcTemplate 作为显式 RowMapper 实现的替代方案时,使用 Java 8 的方法引用和 lambda 非常方便。但是,如果出于某种原因,想要或是需要一个显式的 RowMapper,那么以下的 findById()
实现将展示如何做到这一点:
@Override
public Ingredient findById(String id) {
return jdbcTemplate.queryForObject(
"select id, name, type from Ingredient where id=?",
new RowMapper<Ingredient>() {
public Ingredient mapRow(ResultSet rs, int rowNum)
throws SQLException {
return new Ingredient(
rs.getString("id"),
rs.getString("name"),
Ingredient.Type.valueOf(rs.getString("type")));
};
}, id);
}
从数据库读取数据只是问题的一部分。在某些情况下,必须将数据写入数据库以便能够读取。因此,让我们来看看如何实现 save()
方法。
插入一行
JdbcTemplate 的 update()
方法可用于在数据库中写入或更新数据的任何查询。并且,如下面的程序清单所示,它可以用来将数据插入数据库。
程序清单 3.6 使用 JdbcTemplate 插入数据
@Override
public Ingredient save(Ingredient ingredient) {
jdbcTemplate.update(
"insert into Ingredient (id, name, type) values (?, ?, ?)",
ingredient.getId(),
ingredient.getName(),
ingredient.getType().toString());
return ingredient;
}
因为没有必要将 ResultSet 数据映射到对象,所以 update()
方法要比 query()
简单得多。它只需要一个包含 SQL 的字符串来执行,以及为任何查询参数赋值。在本例中,查询有三个参数,它们对应于 save()
方法的最后三个参数,提供了 Ingredient 的 id、name 和 type。
完成了 JdbcIngredientRepository 后,现在可以将其注入到 DesignTacoController 中,并使用它来提供一个 Ingredient 对象列表,而不是使用硬编码的值(正如第 2 章中所做的那样)。DesignTacoController 的变化如下所示。
程序清单 3.7 在控制器中注入并使用 repository
@Controller
@RequestMapping("/design")
@SessionAttributes("tacoOrder")
public class DesignTacoController {
private final IngredientRepository ingredientRepo;
@Autowired
public DesignTacoController(
IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
@ModelAttribute
public void addIngredientsToModel(Model model) {
Iterable<Ingredient> ingredients = ingredientRepo.findAll();
Type[] types = Ingredient.Type.values();
for (Type type : types) {
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients, type));
}
}
// ...
}
请注意,addIngredientsToModel()
方法的第 2 行现在调用了注入的 IngredientRepository 的 findAll()
方法。findAll()
方法从数据库中提取所有 Ingredient,然后将它们对应到到模型的不同类型中。
现在我们有了一个 IngredientRepository 来从数据库中提取配料对象,我们还可以简化我们在第 2 章中创建的 IngredientByIdConverter,替换其中硬编码的配料 Map,通过简单调用 IngredientRepository.findById()
方法:
程序清单 3.8 简化的 IngredientByIdConverter
package tacos.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import tacos.Ingredient;
import tacos.data.IngredientRepository;
@Component
public class IngredientByIdConverter implements Converter<String, Ingredient> {
private IngredientRepository ingredientRepo;
@Autowired
public IngredientByIdConverter(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
@Override
public Ingredient convert(String id) {
return ingredientRepo.findById(id).orElse(null);
}
}
几乎已经准备好启动应用程序,并测试这些更改了。但是在开始从 Ingredient 表查询数据之前,应该先创建这个表,并写入一些 Ingredient 数据。