zoukankan      html  css  js  c++  java
  • 使用数据(二)

    定义模式和预加载数据

    除了 Ingredient 表之外,我们还需要其他的一些表来保存订单和设计信息。下图描述了我们所需要的表以及这些表之间的关联关系。

    上图中的表主要实现如下目的。

    • Ingredient:保存配料信息。
    • Taco:保存 taco 设计相关的信息。
    • Taco_Ingredients:Taco 中的每行数据都对应一行或多行,将 taco 和与之相关的配料映射在一起。
    • Taco_Order:保存必要的订单细节。
    • Taco_Order_Tacos:Taco_Order 中的每行数据都对应一行或多行,将订单和与之相关的 taco 映射在一起。

    下面是创建表的 SQL。

    create table if not exists Ingredient (
      id varchar(4) not null,
      name varchar(25) not null,
      type varchar(10) not null
    );
    
    create table if not exists Taco (
      id identity,
      name varchar(50) not null,
      createdAt timestamp not null
    );
    
    create table if not exists Taco_Ingredients (
      taco bigint not null,
      ingredient varchar(4) not null
    );
    
    alter table Taco_Ingredients
        add foreign key (taco) references Taco(id);
    alter table Taco_Ingredients
        add foreign key (ingredient) references Ingredient(id);
    
    create table if not exists Taco_Order (
      id identity,
        deliveryName varchar(50) not null,
        deliveryStreet varchar(50) not null,
        deliveryCity varchar(50) not null,
        deliveryState varchar(2) not null,
        deliveryZip varchar(10) not null,
        ccNumber varchar(16) not null,
        ccExpiration varchar(5) not null,
        ccCVV varchar(3) not null,
        placedAt timestamp not null
    );
    
    create table if not exists Taco_Order_Tacos (
      tacoOrder bigint not null,
      taco bigint not null
    );
    
    alter table Taco_Order_Tacos
        add foreign key (tacoOrder) references Taco_Order(id);
    alter table Taco_Order_Tacos
        add foreign key (taco) references Taco(id);
    

    现在,最大的问题是将这些模式定义放在什么地方。实际上,Spring Boot 回答了这个问题。

    如果在应用的根类路径下存在名为 schema.sql 的文件,那么在应用启动的时候将会基于数据库执行这个文件中的 SQL。所以,我们需要将上面的内容保存为名为 schema.sql 的文件并放到 src/main/resources 文件夹下。

    我们可能还希望在数据库中预加载一些配料数据。幸运的是,Spring Boot 还会在应用启动的时候执行根类路径下名为 data.sql 的文件。所以,我们可以使用下面的插入语句为数据库加载配料数据,并将其保存到src/main/resources/data.sql 文件中。

    delete from Taco_Order_Tacos;
    delete from Taco_Ingredients;
    delete from Taco;
    delete from Taco_Order;
    
    delete from Ingredient;
    insert into Ingredient (id, name, type)
                    values ('FLTO', 'Flour Tortilla', 'WRAP');
    insert into Ingredient (id, name, type)
                    values ('COTO', 'Corn Tortilla', 'WRAP');
    insert into Ingredient (id, name, type)
                    values ('GRBF', 'Ground Beef', 'PROTEIN');
    insert into Ingredient (id, name, type)
                    values ('CARN', 'Carnitas', 'PROTEIN');
    insert into Ingredient (id, name, type)
                    values ('TMTO', 'Diced Tomatoes', 'VEGGIES');
    insert into Ingredient (id, name, type)
                    values ('LETC', 'Lettuce', 'VEGGIES');
    insert into Ingredient (id, name, type)
                    values ('CHED', 'Cheddar', 'CHEESE');
    insert into Ingredient (id, name, type)
                    values ('JACK', 'Monterrey Jack', 'CHEESE');
    insert into Ingredient (id, name, type)
                    values ('SLSA', 'Salsa', 'SAUCE');
    insert into Ingredient (id, name, type)
                    values ('SRCR', 'Sour Cream', 'SAUCE');
    

    尽管我们目前只为配料数据编写了一个 repository,但是你依然可以将 Taco Cloud 应用启动起来并访问设计页面,看一下 JdbcIngredientRepository 的实际功能。尽可以去尝试一下!当你尝试完回来的时候,我们将会编写 Taco、Order 和数据持久化的 repository。

    实验楼 WebIDE 终端中执行以下命令运行程序。

    mvn clean spring-boot:run
    

    程序运行后,打开 Web 服务,访问 /design,显示结果如下。

    插入数据

    我们已经粗略看到了如何使用 JdbcTemplate 将数据写入到数据库中。JdbcIngredient Repository 的 save() 方法使用 JdbcTemplate 的 update() 方法将 Ingredient 对象保存到了数据库中。

    尽管这是一个非常好的起步样例,但是它过于简单了。你马上将会看到保存数据可能会比 JdbcIngredientRepository 更加复杂。借助 JdbcTemplate,我们有以下两种保存数据的方法。

    • 直接使用 update() 方法。
    • 使用 SimpleJdbcInsert 包装器类。

    让我们首先看一下在持久化需求比保存 Ingredient 更为复杂的情况下该如何使用 update() 方法。

    使用 JdbcTemplate 保存数据

    现在,taco 和 order 的 repository 唯一需要做的事情就是保存对应的对象。为了保存 Taco 对象,TacoRepository 声明了一个 save() 方法:
    在 src/main/java/tacos/data 包下新建 TacoRepository.java 文件,代码如下。

    package tacos.data;
    
    import tacos.Taco;
    
    public interface TacoRepository {
    
      Taco save(Taco design);
    
    }
    

    与之类似,OrderRepository 也声明了一个 save() 方法。

    在 src/main/java/tacos/data 包下新建 OrderRepository.java 文件,代码如下。

    package tacos.data;
    
    import tacos.Order;
    
    public interface OrderRepository {
    
      Order save(Order order);
    }
    

    看起来非常简单,对吧?但是,保存 taco 的时候需要同时将与该 taco 关联的配料保存到 Taco_Ingredients 表中。与之类似,保存订单的时候,需要同时将与该订单关联的 taco 保存到 Taco_Order_Tacos 表中。这样看来,保存 taco 和订单就会比保存配料更困难一些。

    为了实现 TacoRepository,我们需要用 save() 方法首先保存必要的 taco 设计细节(比如,名称和创建时间),然后对 Taco 对象中的每种配料都插入一行数据到 Taco_Ingredients 中。下面代码展示了完整的 JdbcTacoRepository 类。

    在 src/main/java/tacos/data 包下新建 JdbcTacoRepository.java 文件,代码如下。

    package tacos.data;
    
    import java.sql.Timestamp;
    import java.sql.Types;
    import java.util.Arrays;
    import java.util.Date;
    
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.PreparedStatementCreator;
    import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
    import org.springframework.jdbc.support.GeneratedKeyHolder;
    import org.springframework.jdbc.support.KeyHolder;
    import org.springframework.stereotype.Repository;
    
    import tacos.Ingredient;
    import tacos.Taco;
    
    @Repository
    public class JdbcTacoRepository implements TacoRepository {
    
      private JdbcTemplate jdbc;
    
      public JdbcTacoRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
      }
    
      @Override
      public Taco save(Taco taco) {
        long tacoId = saveTacoInfo(taco);
        taco.setId(tacoId);
        for (Ingredient ingredient : taco.getIngredients()) {
          saveIngredientToTaco(ingredient, tacoId);
        }
    
        return taco;
      }
    
      private long saveTacoInfo(Taco taco) {
        taco.setCreatedAt(new Date());
        PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(
                "insert into Taco (name, createdAt) values (?, ?)",
                Types.VARCHAR, Types.TIMESTAMP
        );
        pscf.setReturnGeneratedKeys(true);
        PreparedStatementCreator psc = pscf.newPreparedStatementCreator(
                Arrays.asList(
                    taco.getName(),
                    new Timestamp(taco.getCreatedAt().getTime())));
    
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbc.update(psc, keyHolder);
    
        return keyHolder.getKey().longValue();
      }
    
      private void saveIngredientToTaco(
              Ingredient ingredient, long tacoId) {
        jdbc.update(
            "insert into Taco_Ingredients (taco, ingredient) " +
            "values (?, ?)",
            tacoId, ingredient.getId());
      }
    
    }
    

    我们可以看到,save() 方法首先调用了私有的 saveTacoInfo() 方法,然后使用该方法所返回的 taco ID 来调用 saveIngredientToTaco(),最后的这个方法会保存每种配料。这里的问题在于 saveTacoInfo() 方法的细节。

    当向 Taco 中插入一行数据的时候,我们需要知道数据库生成的 ID,这样我们才可以在每个配料信息中引用它。保存配料数据时所使用的 update() 方法无法帮助我们得到所生成的 ID,所以在这里我们需要一个不同的 update() 方法。

    这里的 update() 方法需要接受一个 PreparedStatementCreator 和一个 KeyHolder。KeyHolder 将会为我们提供生成的 taco ID。但是,为了使用该方法,我们必须还要创建一个 PreparedStatementCreator。

    从代码中可以看到,创建 PreparedStatementCreator 并不简单。首先需要创建 PreparedStatementCreatorFactory,并将我们要执行的 SQL 传递给它,同时还要包含每个查询参数的类型。随后,需要调用该工厂类的 newPreparedStatementCreator() 方法,并将查询参数所需的值传递进来,这样才能生成一个 PreparedStatementCreator。

    有了 PreparedStatementCreator 之后,我们就可以调用 update() 方法了,并且需要将 PreparedStatementCreator 和 KeyHolder(在本例中,也就是 GeneratedKeyHolder 的实例)传递进来。update() 调用完成之后,我们就可以通过 keyHolder.getKey().longValue() 返回 taco 的 ID。

    回到 save() 方法,接下来我们会轮询 Taco 中的每个 Ingredient,并调用 saveIngredientToTaco()。saveIngredientToTaco() 使用更简单的 update() 形式来将对配料的引用保存到 Taco_Ingredients 表中。

    对于 TacoRepository 来说,剩下的事情就是将它注入到 DesignTacoController 中,并在保存 taco 的时候调用它。下面代码展现了注入 repository 所需的必要变更。

    @Controller
    @RequestMapping("/design")
    @SessionAttributes("order")
    public class DesignTacoController {
    
      private final IngredientRepository ingredientRepo;
    
      private TacoRepository designRepo;
    
      @Autowired
      public DesignTacoController(
            IngredientRepository ingredientRepo,
            TacoRepository designRepo) {
        this.ingredientRepo = ingredientRepo;
        this.designRepo = designRepo;
      }
    
      // ...
    
    }
    

    正如我们所看到的,构造器能够同时接受 IngredientRepository 和 TacoRepository 对象。该构造器将得到的对象赋值给实例变量,这样它们就可以在 showDesignForm() 和 processDesign() 中使用了。

    谈到 processDesign() 方法,它的变更要比 showDesignForm() 的变更更大一些。下面代码展现了新的 processDesign() 方法。

    @PostMapping
    public String processDesign(
      @Valid Taco design, Errors errors,
      @ModelAttribute Order order) {
    
      if (errors.hasErrors()) {
        return "design";
      }
    
      Taco saved = designRepo.save(design);
      order.addDesign(saved);
    
      return "redirect:/orders/current";
    }
    

    对 taco 设计的处理位于 processDesign() 方法中。该方法接受 Order 对象作为参数,同时还包括 Taco 和 Errors 对象。Order 参数带有 @ModelAttribute 注解,表明它的值应该是来自模型的,Spring MVC 不会尝试将请求参数绑定到它上面。

    在检查完校验错误之后,processDesign() 使用注入的 TacoRepository 来保存 taco。然后,它将 Taco 对象保存到 session 里面的 Order 中。

    实际上,在用户完成操作并提交订单表单之前,Order 对象会一直保存在 session 中,并没有保存到数据库中。到时,OrderController 需要调用 OrderRepository 的实现来保存订单。接下来,我们编写这个实现类。

    使用 SimpleJdbcInsert 插入数据

    在前文中提到,保存 taco 的时候不仅要将 taco 的名称和创建时间保存到 Taco 表中,还需要将该 taco 所引用的配料保存到 Taco_Ingredients 表中。此时,需要我们知道 Taco 的 ID,而这是通过 KeyHolder 和 PreparedStatementCreator 获取的。

    在保存订单的时候,存在类似的情况。我们不仅要将订单数据保存到 Taco_Order 表中,还要将订单对每个 taco 的引用保存到 Taco_Order_Tacos 表中。但是,在这里,我们不再使用烦琐的 PreparedStatementCreator,而是引入 SimpleJdbcInsert,这个对象对 JdbcTemplate 进行了包装,能够更容易地将数据插入到表中。

    首先,我们要创建一个 JdbcOrderRepository,它是 OrderRepository 的实现。但在编写 save() 方法的实现之前,我们先关注一下构造器,在构造器中我们会创建 SimpleJdbcInsert 的两个实例,分别用来把值插入到 Taco_Order 和 Taco_Order_Tacos 表中。下面代码展现了 JdbcOrderRepository(尚不包含 save() 方法)。

    在 src/main/java/tacos/data 包下新建 JdbcOrderRepository.java 文件,代码如下。

    package tacos.data;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
    import org.springframework.stereotype.Repository;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import tacos.Taco;
    import tacos.Order;
    
    @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 表协作,但是没有声明该表中 ID 是如何生成的。

    该构造器还创建了 Jackson 中 ObjectMapper 类的一个实例,并将其赋值给一个实例变量。尽管 Jackson 的初衷是进行 JSON 处理,但是你很快就会看到我们是如何使用它来帮助我们保存订单和关联的 taco 的。

    现在,我们看一下 save() 方法该如何使用 SimpleJdbcInsert 实例。下面代码展示了 save() 方法以及一些私有方法,其中 save() 方法会将实际的工作委托给这些私有方法。

    在 src/main/java/tacos/data/JdbcOrderRepository.java 中添加以下方法。

    @Override
    public Order save(Order order) {
      order.setPlacedAt(new Date());
      long orderId = saveOrderDetails(order);
      order.setId(orderId);
      List<Taco> tacos = order.getTacos();
      for (Taco taco : tacos) {
        saveTacoToOrder(taco, orderId);
      }
    
      return order;
    }
    
    private long saveOrderDetails(Order order) {
      @SuppressWarnings("unchecked")
      Map<String, Object> values =
        objectMapper.convertValue(order, Map.class);
      values.put("placedAt", order.getPlacedAt());
    
      long orderId =
        orderInserter
        .executeAndReturnKey(values)
        .longValue();
      return orderId;
    }
    
    private void saveTacoToOrder(Taco taco, long orderId) {
      Map<String, Object> values = new HashMap<>();
      values.put("tacoOrder", orderId);
      values.put("taco", taco.getId());
      orderTacoInserter.execute(values);
    }
    

    save() 方法实际上没有保存任何内容,只是定义了保存 Order 及其关联的 Taco 对象的流程,并将实际的持久化任务委托给了 saveOrderDetails() 和 saveTacoToOrder()。

    SimpleJdbcInsert 有两个非常有用的方法来执行数据插入操作:execute() 和 executeAndReturnKey()。它们都接受 Map<String, Object> 作为参数,其中 Map 的 key 对应表中要插入数据的列名,而 Map 中的 value 对应要插入到列中的实际值。

    我们只需将 Order 中的值复制到 Map 的条目中就能很容易地创建一个这样的 Map。但是,Order 有很多属性,这些属性与对应的列有着相同的名称。鉴于此,在 saveOrderDetails() 中,我决定使用 Jackson 的 ObjectMapper 及其 convertValue() 方法,以便于将 Order 转换为 Map。Map 创建完成之后,我们将 Map 中 placedAt 条目的值设置为 Order 对象 placedAt 属性的值。之所以需要这样做,是因为 ObjectMapper 会将 Date 属性转换为 long,这会导致与 Taco_Order 表中的 placedAt 字段不兼容。

    当 Map 中准备好订单数据之后,我们就可以调用 orderInserter 的 executeAndReturnKey() 方法了。该方法会将订单信息保存到 Taco_Order 表中,并以 Number 对象的形式返回数据库生成的 ID,继而调用 longValue() 方法将返回值转换为 long 类型。

    saveTacoToOrder() 方法要简单得多。在这里我们没有使用 ObjectMapper 将对象转换为 Map,而是直接创建了一个 Map 并设置对应的值。同样,Map 的 key 与表中的列名对应。我们只需要简单地调用 orderTacoInserter 的 execute() 方法就能执行插入操作了。

    现在,我们可以将 OrderRepository 注入到 OrderController 中并开始使用它了。下面代码展示了完整的 OrderController,包括使用注入的 OrderRepository 相关的变更。

    修改 src/main/java/tacos/web/OrderController.java 文件,代码如下。

    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.Order;
    import tacos.data.OrderRepository;
    
    @Controller
    @RequestMapping("/orders")
    @SessionAttributes("order")
    public class OrderController {
    
      private OrderRepository orderRepo;
    
      public OrderController(OrderRepository orderRepo) {
        this.orderRepo = orderRepo;
      }
    
      @GetMapping("/current")
      public String orderForm() {
        return "orderForm";
      }
    
      @PostMapping
      public String processOrder(@Valid Order order, Errors errors,
                                 SessionStatus sessionStatus) {
        if (errors.hasErrors()) {
          return "orderForm";
        }
    
        orderRepo.save(order);
        sessionStatus.setComplete();
    
        return "redirect:/";
      }
    
    }
    

    除了将 OrderRepository 注入到控制器中,OrderController 唯一明显的变化就是 processOrder() 方法。在这个方法中,通过表单提交的 Order 对象(同时也是 session 中持有的 Object 对象)会通过注入的 OrderRepository 的 save() 方法进行保存。

    订单保存完成之后,我们就不需要在 session 中持有它了。实际上,如果我们不把它清理掉,那么订单会继续保留在 session 中,其中包括与之关联的 taco,下一次的订单将会从旧订单中保存的 taco 开始。所以,processOrder() 方法请求了一个 SessionStatus 参数,并调用了它的 setComplete() 方法来重置 session。

    所有 JDBC 持久化代码已经就绪,现在我们可以启动 Taco Cloud 应用并进行尝试了。你可以按照自己的意愿创建任意数量的 taco 和订单。

    你可能会发现,深入研究一下数据库中的内容是非常有帮助的。我们目前使用 H2 作为嵌入式数据库并且启用了 Spring Boot DevTools,所以我们可以在浏览器中访问 /h2-console 以查看 H2 Console。使用默认的凭证应该就可以进入,但是你需要确保 JDBC URL 字段设置成了 jdbc:h2:mem:testdb。如下所示。

    点击 Connect 登录之后,我们可以对 Taco Cloud 模式下的表执行任意的查询。如下图所示。

    如果你看到如下信息,是为了安全起见而不允许从远端访问数据库。

    需要在程序配置文件 src/main/resources/application.properties 中添加以下配置允许远程访问。

    spring.h2.console.settings.web-allow-others=true
    

    添加完后 Ctrl + s 保存配置(WebIDE 也会自动保存)后,因为我们引入了 Devtools,所以程序会自动重启,应用我们所做的修改。

    相对于普通的 JDBC,Spring 的 JdbcTemplate 和 SimpleJdbcInsert 能够极大地简化关系型数据库的使用。但是,你会发现使用 JPA 会更加简单。我们回顾一下自己的工作内容,看一下 Spring Data 是如何让数据持久化变得更简单的。

  • 相关阅读:
    ASP.NET 2.0 用户注册控件的密码验证问题
    编程使用GridView,DataList的模版列
    在您的站点上添加 Windows Live Favourites 收藏入口
    推荐个很好玩的开源项目Ascii Generator dotNET
    Castle ActiveRecord 在Web项目和WinForm项目中
    HTML解析器项目进展和新的构思
    SilverLight 的跨域跨域访问
    SQL 语句之Join复习
    【笔记】提高中文分词准确性和效率的方法
    ASP.NET 动态加载控件激发事件的问题
  • 原文地址:https://www.cnblogs.com/sakura579/p/14091197.html
Copyright © 2011-2022 走看看