《spring实战》学习笔记 第三章 使用数据

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 接口 即可。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值