Skip to content

3.1.4 Chèn dữ liệu

Bạn đã từng thấy qua cách sử dụng JdbcTemplate để ghi dữ liệu vào cơ sở dữ liệu. Phương thức save() trong JdbcIngredientRepository đã sử dụng phương thức update() của JdbcTemplate để lưu các đối tượng Ingredient vào cơ sở dữ liệu.

Mặc dù đó là một ví dụ đầu tiên tốt, nhưng có lẽ nó hơi quá đơn giản. Như bạn sẽ thấy ngay sau đây, việc lưu dữ liệu có thể phức tạp hơn nhiều so với những gì JdbcIngredientRepository cần.

Trong thiết kế của chúng ta, TacoOrderTaco là một phần của một aggregate, trong đó TacoOrderaggregate root. Nói cách khác, các đối tượng Taco không tồn tại bên ngoài ngữ cảnh của một TacoOrder. Do đó, hiện tại chúng ta chỉ cần định nghĩa một repository để lưu trữ các đối tượng TacoOrder, và thông qua đó, lưu luôn các đối tượng Taco đi kèm. Một repository như vậy có thể được định nghĩa trong một interface OrderRepository như sau:

java
package tacos.data;

import java.util.Optional;

import tacos.TacoOrder;

public interface OrderRepository {

  TacoOrder save(TacoOrder order);

}

Dường như khá đơn giản, phải không? Không hẳn vậy. Khi bạn lưu một TacoOrder, bạn cũng phải lưu các đối tượng Taco đi kèm với nó. Và khi lưu các đối tượng Taco, bạn cũng sẽ cần lưu một đối tượng thể hiện mối liên kết giữa Taco và mỗi Ingredient tạo nên chiếc taco. Lớp IngredientRef định nghĩa mối liên kết giữa TacoIngredient như sau:

java
package tacos;

import lombok.Data;

@Data
public class IngredientRef {

  private final String ingredient;

}

Phải nói rằng phương thức save() lần này sẽ thú vị hơn khá nhiều so với phương thức tương ứng bạn từng viết để lưu một đối tượng Ingredient đơn giản trước đó.

Một việc khác mà phương thức save() cần làm là xác định ID nào được gán cho đơn hàng sau khi nó đã được lưu. Theo lược đồ, thuộc tính id trong bảng Taco_Order là một identity, nghĩa là cơ sở dữ liệu sẽ tự động xác định giá trị này. Nhưng nếu cơ sở dữ liệu xác định giá trị thay cho bạn, thì bạn cần biết giá trị đó là gì để có thể trả về trong đối tượng TacoOrder từ phương thức save(). May mắn thay, Spring cung cấp một kiểu tiện lợi có tên là GeneratedKeyHolder có thể giúp bạn làm điều đó. Tuy nhiên, nó yêu cầu phải làm việc với một prepared statement, như thể hiện trong phần hiện thực sau của phương thức save():

java
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;
  }
}

Có vẻ như có rất nhiều việc diễn ra trong phương thức save(), nhưng chúng ta có thể chia nhỏ nó thành một vài bước chính như sau. Trước tiên, bạn tạo một PreparedStatementCreatorFactory mô tả câu lệnh insert cùng với kiểu dữ liệu của các trường đầu vào trong truy vấn. Bởi vì sau đó bạn cần lấy ID của đơn hàng đã lưu, bạn cũng sẽ cần gọi setReturnGeneratedKeys(true).

Sau khi định nghĩa PreparedStatementCreatorFactory, bạn sử dụng nó để tạo ra một PreparedStatementCreator, truyền vào các giá trị từ đối tượng TacoOrder sẽ được lưu trữ. Trường cuối cùng được truyền vào PreparedStatementCreator là ngày tạo đơn hàng, trường này cũng cần được gán vào chính đối tượng TacoOrder để đối tượng trả về có đầy đủ thông tin.

Giờ đây, khi bạn đã có một PreparedStatementCreator, bạn sẵn sàng lưu dữ liệu đơn hàng bằng cách gọi phương thức update() trên JdbcTemplate, truyền vào PreparedStatementCreator và một GeneratedKeyHolder. Sau khi dữ liệu đơn hàng được lưu, GeneratedKeyHolder sẽ chứa giá trị của trường id được cơ sở dữ liệu gán, và giá trị đó nên được gán lại vào thuộc tính id của đối tượng TacoOrder.

Tại thời điểm này, đơn hàng đã được lưu, nhưng bạn cũng cần lưu các đối tượng Taco liên kết với đơn hàng. Bạn có thể làm điều đó bằng cách gọi saveTaco() cho mỗi Taco trong đơn hàng.

Phương thức saveTaco() khá giống với phương thức save(), như bạn có thể thấy dưới đây:

java
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;
}

Từng bước một, saveTaco() phản ánh cấu trúc của save(), mặc dù dành cho dữ liệu Taco thay vì dữ liệu TacoOrder. Cuối cùng, nó gọi đến saveIngredientRefs() để tạo một dòng trong bảng Ingredient_Ref nhằm liên kết dòng Taco với một dòng Ingredient. Phương thức saveIngredientRefs() như sau:

java
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++);
  }
}

May mắn thay, phương thức saveIngredientRefs() đơn giản hơn nhiều. Nó lặp qua danh sách các đối tượng Ingredient, lưu từng cái vào bảng Ingredient_Ref. Nó cũng có một biến key cục bộ được dùng làm chỉ số để đảm bảo thứ tự của các nguyên liệu được giữ nguyên.

Tất cả những gì còn lại cần làm với OrderRepository là tiêm nó vào OrderController và sử dụng nó khi lưu đơn hàng. Danh sách dưới đây cho thấy các thay đổi cần thiết để tiêm repository.

Listing 3.11 Tiêm và sử dụng OrderRepository

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.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:/";
  }
}

Như bạn thấy, constructor nhận một OrderRepository làm tham số và gán nó vào một biến thể hiện để sử dụng trong phương thức processOrder(). Nói đến phương thức processOrder(), nó đã được thay đổi để gọi phương thức save() trên OrderRepository thay vì chỉ ghi log đối tượng TacoOrder.

JdbcTemplate của Spring giúp làm việc với cơ sở dữ liệu quan hệ trở nên đơn giản hơn rất nhiều so với JDBC thuần túy. Nhưng ngay cả với JdbcTemplate, một số tác vụ lưu trữ vẫn còn khó khăn, đặc biệt là khi lưu trữ các đối tượng miền lồng nhau trong một tổng hợp (aggregate). Giá như có một cách làm việc với JDBC đơn giản hơn nữa thì tốt biết mấy.

Hãy cùng xem xét Spring Data JDBC, thứ giúp làm việc với JDBC trở nên cực kỳ dễ dàng — ngay cả khi làm việc với các aggregate.

Released under the MIT License.