3. 1 使用 JDBC 读取 和 写入 数据
在 处理 关系 型 数据 的 时候,Java 开发 人员 有多 种 可选 方案, 其中 最 常见 的 是 JDBC 和 JPA。
3. 1. 1 调整 领域 对象 以 适应 持久 化
在 将对 象 持久 化 到 数据库 的 时候, 通常 最好 有一个 字段 作为 对象 的 唯一 标识。
3. 1. 2 使用 JdbcTemplate
首先加入jdbc和h2的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
定义 JDBC repository
IngredientRepository 接口代码案例
public interface IngredientRepository {
Iterable<Ingredient> findAll();
Ingredient findById(String id);
Ingredient save(Ingredient ingredient);
}
JdbcIngredientRepository代码实现
package tacos.data;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import tacos.Ingredient;
@Repository
public class JdbcIngredientRepository
implements IngredientRepository {
private JdbcTemplate jdbc;
@Autowired
public JdbcIngredientRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Override
public Iterable<Ingredient> findAll() {
return jdbc.query("select id, name, type from Ingredient",
this::mapRowToIngredient);
}
@Override
public Ingredient findById(String id) {
return jdbc.queryForObject(
"select id, name, type from Ingredient where id=?",
this::mapRowToIngredient, id);
}
@Override
public Ingredient save(Ingredient ingredient) {
jdbc.update(
"insert into Ingredient (id, name, type) values (?, ?, ?)",
ingredient.getId(),
ingredient.getName(),
ingredient.getType().toString());
return ingredient;
}
private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
throws SQLException {
return new Ingredient(
rs.getString("id"),
rs.getString("name"),
Ingredient.Type.valueOf(rs.getString("type")));
}
}
@ Repository 、@ Controller 和@ Component。添加注解 之后, Spring 的 组件 扫描 就会 自动 发现 它, 并且 会 将其 初始 化为 Spring 应用 上下文 中的 bean。
findAll() 和 findOne() 以 相同 的 方式 使用 了 JdbcTemplate。 findAll() 方法 预期 返回 一个 对象 的 集合, 它 使用 了 JdbcTemplate 的 query() 方法。 query() 会 接受 要 执行 的 SQL 以及 Spring RowMapper 的 一个 实现( 用来 将 结果 集 中的 每 行数 据 映射 为 一个 对象)。 query ()方法 还能 以 最终 参数 的 形式 接收 查询 中 所需 的 任意 参数。 但是, 在 本例 中, 我们 不需要 任何 参数。
findOne() 方法 预期 只会 返回 一个 Ingredient 对象, 所以 它 使用 了 JdbcTemplate 的 queryForObject() 方法, 而 不是 query() 方法。
插入 一行 数据
save()方法中的update()用来 执行 向 数据库 中 写入 或 更新 数据 的 查询 语句。
3. 1. 3 定义 模式 和 预加 载 数据
我们需要数据库和表结构来储存数据,
如果 在 应 用的 根 类 路径 下 存在 名为 schema. sql 的 文件, 那么 在 应用 启动 的 时候 将会 基于 数据库 执行 这个 文件 中的 SQL。 所以, 我们 需要 将 表结构生成语句 的 内容 保存 为 名为 schema. sql 的 文件 并 放到“ src/ main/ resources” 文件夹 下。
我们 还可以 在 数据库 中 预加 载 一些 配料 数据。 Spring Boot 还会 在 应用 启动 的 时候 执行 根 类 路径 下 名为 data. sql 的 文件。 所以, 我们 可以 使用 数据库初始化语句 为 数据库 加载 配料 数据, 并将 其 保存 到“ src/ main/ resources/ data. sql” 文件 中。
总之SpringBoot默认会采用资源根目录下的schema.sql文件进行创建表的初始化,使用data.sql进行插入初始化数据的工作。
这里有两点需要注意:
1.sql文件命名要按规范。并且放置在resource根目录。否则需要显示配置:例如将sql放在sql目录中
spring.datasource.schema=classpath:sql/schema.sql
spring.datasource.data=classpath:sql/data.sql
2.放置完需要一个配置,否则不生效。
spring.datasource.initialization-mode=always
always为始终执行初始化,embedded只初始化内存数据库(默认值),如h2等,never为不执行初始化。
3. 1. 4 插入 数据
使用 SimpleJdbcInsert 插入 数据
SimpleJdbcInsert, 这个 对象 对 JdbcTemplate 进行 了 包装, 能够 更容易 地 将 数据 插入 到 表中。
案例代码
@Repository
public class JdbcOrderRepository implements OrderRepository {
private SimpleJdbcInsert orderInserter;
private SimpleJdbcInsert orderTacoInserter;
private ObjectMapper objectMapper;
@Autowired
public JdbcOrderRepository(JdbcTemplate jdbc) {
this.orderInserter = new SimpleJdbcInsert(jdbc)
.withTableName("Taco_Order")
.usingGeneratedKeyColumns("id");
this.orderTacoInserter = new SimpleJdbcInsert(jdbc)
.withTableName("Taco_Order_Tacos");
this.objectMapper = new ObjectMapper();
}
//……
}
与 JdbcTacoRepository 类似, JdbcOrderRepository 通过 构造 器 将 JdbcTemplate 注入 进来。 但是 在这里, 我们 没有 将 JdbcTemplate 直接 赋给 实例 变量, 而是 使用 它 构建 了 两个 SimpleJdbcInsert 实例。 第一个 实例 赋值 给了 orderInserter 实例 变量, 配置 为 与 Taco_ Order 表 协作, 并且 假定 id 属性 将会 由 数据库 提供 或 生成。 第二个 实例 赋值 给了 orderTacoInserter 实例 变量, 配置 为 与 Taco_ Order_ Tacos 表 协作,
相对于 普通 的 JDBC, Spring 的 JdbcTemplate 和 SimpleJdbcInsert 能够 极大 地 简化 关系 型 数据库 的 使用。
3. 2 使用 Spring Data JPA 持久 化 数据
3. 2. 1 添加 Spring Data JPA 到 项目 中
Spring Boot 应用 可以 通过 JPA starter 来 添加 Spring Data JPA。 这个 starter 依赖 不仅 会 引入 Spring Data JPA, 还会 传递性 地 将 Hibernate 作为 JPA 实现 引入 进来:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
3. 2. 2 将领 域 对象 标注 为 实体
案例代码
@Data
@RequiredArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@Entity
public class Ingredient {
@Id
private final String id;
private final String name;
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
为了 将 Ingredient 声明 为 JPA 实体, 它 必须 添加@ Entity 注解。 它的 id 属性 需要 使用@ Id 注解, 以便于 将其 指定 为 数据库 中 唯一 标识 该 实体 的 属性。
除了 JPA 特定 的 注解, 你 可能 会 发现 我们 在 类 级别 添加 了@ NoArgsConstructor 注解。 JPA 需要 实体 有一个 无 参 的 构造 器, Lombok 的@ NoArgsConstructor 注解 能够 帮助 我们 实现 这一点。 但是, 我们 不想 直接 使用 它, 因此 通过 将 access 属性 设置 为 AccessLevel. PRIVATE 使其 变成 私有 的。 因为 这里 有 必须 要 设置 的 final 属性, 所以 我们将 force 设置 为 true, 这样 Lombok 生成 的 构造 器 就会 将它 们 设置 为 null。
我们 还 添加 了 一个@ RequiredArgsConstructor 注解。@ Data 注解 会为 我们 添加 一个 有 参 构造 器, 但是 使用@ NoArgsConstructor 注解 之后, 这个 构造 器 就 会被 移 除掉。 现在, 我们 显 式 添加@ RequiredArgsConstructor 注解, 以 确保 除了 private 的 无 参 构造 器 之外, 我们 还会 有一个 有 参 构造 器。
案例代码
@Data
@Entity
public class Taco {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@NotNull
@Size(min=5, message="Name must be at least 5 characters long")
private String name;
private Date createdAt;
@ManyToMany(targetEntity=Ingredient.class)
@Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients = new ArrayList<>();
@PrePersist
void createdAt() {
this.createdAt = new Date();
}
}
与 Ingredient 类似, Taco 类 现在 添加 了@ Entity 注解, 并为 其 id 属性 添加 了@ Id 注解。 因为 我们 要 依赖 数据库 自动 生成 ID 值, 所以 在这里 还为 id 属性 设置 了@ GeneratedValue, 将它 的 strategy 设置 为 AUTO。
为了 声明 Taco 与其 关联 的 Ingredient 列表 之间 的 关系, 我们 为 ingredients 添加 了@ ManyToMany 注解。 每个 Taco 可以 有 多个 Ingredient, 而 每个 Ingredient 可以 是 多个 Taco 的 组成部分。
@PrePersist 注解, 设置 属性 当前 的 日期 和 时间。
@Table注释。案例:@Table(name="Taco_Order") 它 表明 将实体 应该 持久 化 到 数据库 中 名为 Taco_ Order 的 表中。
3. 2. 3 声明 JPA repository
借助 Spring Data, 我们 可以 扩展 CrudRepository 接口。
CrudRepository 定义 了 很多 用于 CRUD( 创建、 读取、 更新、 删除) 操作 的 方法。 注意, 它是 参数 化 的, 第一个 参数 是 repository 要 持久 化 的 实体 类型, 第二个 参数 是 实体 ID 属性 的 类型。 对于 IngredientRepository 来说, 参数 应该 是 Ingredient 和 String。
public interface TacoRepository
extends CrudRepository<Taco, Long> {
}
接口现在有了, Spring Data JPA 带来 的 好消息 是, 我们 根本 就不 用 编写 实现 类! 当 应用 启动 的 时候, Spring Data JPA 会在 运行 期 自动 生成 实现 类。
3. 2. 4 自定义 JPA repository
没太看明白这部分,自定义的查询,却不需要写实现,只是在接口处增加了一个方法,加上参数和返回,似乎这东西是通过方法名来实现的吗?比如说这种鬼畜的方法名readOrdersByDeliveryZipAndPlacedAtBetween() 或者 findByDeliveryToAndDeliveryCityAllIgnoresCase()
对于 更为 复杂 的 查询, 方法 名 可能 会面 临 失控 的 风险。 在 这种 情况下, 可以 将 方法 定义 为 任何 你想 要的 名称, 并为 其 添加@ Query 注解, 从而 明确 指明 方法 调用 时 要 执行 的 查询,
@Query("Order o where o.deliveryCity='Seattle'")
List<Order> readOrdersDeliveredInSeattle();
还是这种准确的SQL语句看着舒服一点……
3. 3 小结
- Spring 的 JdbcTemplate 能够 极大 地 简化 JDBC 的 使用。
- 在 我们 需要 知道 数据库 所 生成 的 ID 值 时, 可以 组合组合 使用 PreparedStatementCreator 和 KeyHolder。
- 为了 简化 数据 的 插入, 可以 使用 SimpleJdbcInsert。
- Spring Data JPA 能够 极大 地 简化 JPA 持久 化, 我们 只需 编写 repository 接口 即可。