3.1.4 插入数据

到此,已经了解了如何使用 JdbcTemplate 向数据库写入数据。JdbcIngredientRepository 中的 save() 方法使用 JdbcTemplate 的 update() 方法将 Ingredient 对象保存到数据库中。

虽然这是第一个很好的例子,但是它可能有点太简单了。保存数据一般都会比 JdbcIngredientRepository 所做的更复杂。

在我们的设计中,TacoOrder 和 Taco 是聚合关系,其中 TacoOrder 是聚合根。换句话说,Taco 对象不存在于 TacoOrder 的上下文之外。所以,现在,我们只需要定义一个 Repository 来持久化 TacoOrder 对象,进而持久化 Taco 对象。这样的 Repository 在OrderRepository 接口中定义,如下所示:

package tacos.data;

import java.util.Optional;

import tacos.TacoOrder;

public interface OrderRepository {

  TacoOrder save(TacoOrder order);

}

看起来很简单,对吧?没那么容易的。当保存 TacoOrder 时,还必须保存和它一起的那些 Taco。在保存 Taco 对象时,还需要保存一个 对象,表示 Taco 和构成玉米卷的每个 Ingredient 之间的关联。IngredientRef 类定义了 Taco 和 Ingredient 之间的关系:

package tacos;

import lombok.Data;

@Data
public class IngredientRef {

  private final String ingredient;

}

这个 save() 方法,比之前创建的用于保存 Ingredient 对象的方法要更有趣。

save() 方法需要做的另一件事是确定保存后分配给的 ID。根据建表的 Schema,Taco_Order 表上的 id 属性是 identity,这意味着数据库将自动确定其值。但是如果数据库为您确定的值,您需要知道该值是什么,以便在 save()方法返回的 TacoOrder 对象中返回。幸运的是,Spring 提供了一个 GeneratedKeyHolder 类可以帮助您实现这一点。但它涉及到使用 Prepared Statement,如下面的 save() 方法所示:

package tacos.data;

import java.sql.Types;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Optional;

import org.springframework.asm.Type;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import tacos.IngredientRef;
import tacos.Taco;
import tacos.TacoOrder;

@Repository
public class JdbcOrderRepository implements OrderRepository {

  private JdbcOperations jdbcOperations;

  public JdbcOrderRepository(JdbcOperations jdbcOperations) {
    this.jdbcOperations = jdbcOperations;
  }

  @Override
  @Transactional
  public TacoOrder save(TacoOrder order) {
    PreparedStatementCreatorFactory pscf =
      new PreparedStatementCreatorFactory(
        "insert into Taco_Order "
        + "(delivery_name, delivery_street, delivery_city, "
        + "delivery_state, delivery_zip, cc_number, "
        + "cc_expiration, cc_cvv, placed_at) "
        + "values (?,?,?,?,?,?,?,?,?)",
        Types.VARCHAR, Types.VARCHAR, Types.VARCHAR,
        Types.VARCHAR, Types.VARCHAR, Types.VARCHAR,
        Types.VARCHAR, Types.VARCHAR, Types.TIMESTAMP
    );
    pscf.setReturnGeneratedKeys(true);

    order.setPlacedAt(new Date());
    PreparedStatementCreator psc =
      pscf.newPreparedStatementCreator(
        Arrays.asList(
          order.getDeliveryName(),
          order.getDeliveryStreet(),
          order.getDeliveryCity(),
          order.getDeliveryState(),
          order.getDeliveryZip(),
          order.getCcNumber(),
          order.getCcExpiration(),
          order.getCcCVV(),
          order.getPlacedAt()));

    GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
    jdbcOperations.update(psc, keyHolder);
    long orderId = keyHolder.getKey().longValue();
    order.setId(orderId);

    List<Taco> tacos = order.getTacos();
    int i=0;
    for (Taco taco : tacos) {
      saveTaco(orderId, i++, taco);
    }
    return order;
  }
}

在这个方法中似乎有很多东西。但是如果您把 save() 分解,就会有发现只有少数几个重要步骤。首先,创建PreparedStatementCreatorFactory。它描述了插入查询以及输入字段的类型。因为您会稍后需要获取保存后的订单 ID,您还需要调用 setReturnGeneratedKeys(true)

定义 PreparedStatementCreatorFactory 后,可以使用它创建 PreparedStatementCreator,从将要创建的 TacoOrder 对象传入值进行持久化。提供给 PreparedStatementCreator 的最后一个字段是订单发出的日期。您还需要对 TacoOrder 对象本身进行设置,以便 TacoOrder 将提供这些信息。

现在您有一个 PreparedStatementCreator,可以通过调用 JdbcTemplate上 的 update() 方法实际保存数据了。传入 PreparedStatementCreator 和 GeneratedKeyHolder。订单数据保存后,GeneratedKeyHolder 将包含由数据库生成的 id,并应复制到TacoOrder 对象的 id 属性中。

此时,订单已保存,但还需要保存关联的 Taco 对象。您可以为订单中的每个玉米卷调用 saveTaco()

saveTaco() 方法与 save() 方法非常相似,如下所示:

private long saveTaco(Long orderId, int orderKey, Taco taco) {
  taco.setCreatedAt(new Date());
  PreparedStatementCreatorFactory pscf =
      new PreparedStatementCreatorFactory(
    "insert into Taco "
    + "(name, created_at, taco_order, taco_order_key) "
    + "values (?, ?, ?, ?)",
    Types.VARCHAR, Types.TIMESTAMP, Type.LONG, Type.LONG
  );
  pscf.setReturnGeneratedKeys(true);

  PreparedStatementCreator psc =
    pscf.newPreparedStatementCreator(
      Arrays.asList(
        taco.getName(),
        taco.getCreatedAt(),
        orderId,
        orderKey));

  GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
  jdbcOperations.update(psc, keyHolder);
  long tacoId = keyHolder.getKey().longValue();
  taco.setId(tacoId);

  saveIngredientRefs(tacoId, taco.getIngredients());

  return tacoId;
}

一步一步地,saveTaco() 的结构与 save() 类似,只不过是保存 Taco 数据而不是 TacoOrder 数据。最后,它调用saveingredientfs() 在 Ingredient_Ref 表插入一行,用于将 Taco 行链接到 Ingredient 行。这个 saveingredientfs() 方法如下所示:

private void saveIngredientRefs(
  long tacoId, List<IngredientRef> ingredientRefs) {
  int key = 0;
  for (IngredientRef ingredientRef : ingredientRefs) {
    jdbcOperations.update(
      "insert into Ingredient_Ref (ingredient, taco, taco_key) "
      + "values (?, ?, ?)",
      ingredientRef.getIngredient(), tacoId, key++);
  }
}

谢天谢地,saveIngredientRefs() 方法要简单得多。它循环列表中的 Ingredient 对象,将每个对象保存到 Ingredient_Ref 表中。它还有一个局部变量,被用作一个索引,以确保配料的顺序保持不变。

OrderRepository 剩下要做的就是,将其注入 OrderController,并在需要时使用它保存订单。下面的列表显示注入了 repository 所需的更改。

程序清单 3.11 注入并使用 OrderRepository

package tacos.web;
import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import tacos.TacoOrder;
import tacos.data.OrderRepository;

@Controller
@RequestMapping("/orders")
@SessionAttributes("tacoOrder")
public class OrderController {

  private OrderRepository orderRepo;

  public OrderController(OrderRepository orderRepo) {
    this.orderRepo = orderRepo;
  }

  // ...
  @PostMapping
  public String processOrder(@Valid TacoOrder order, Errors errors, SessionStatus sessionStatus) {
    if (errors.hasErrors()) {
      return "orderForm";
    }

    orderRepo.save(order);
    sessionStatus.setComplete();

    return "redirect:/";
  }
}

如您所见,构造函数将 OrderRepository 作为参数,将其分配给实例变量,并在 processOrder() 方法中使用。说到 processOrder() 方法,它已更改调用 OrderRepository 的 save() 方法,而不是记录 TacoOrder 对象。

Spring 的 JdbcTemplate 使操作关系数据库比使用纯 JDBC 要简单的多。但即使使用JdbcTemplate,一些持久化任务仍然具有挑战性。尤其是在聚合中持久化嵌套对象时。要是有办法解决这个问题就好了。

让我们看一看 Spring Data JDBC,它使得使用 JDBC 变得非常简单——即使是持久化聚合对象时。

results matching ""

    No results matching ""