3.1.2 Làm việc với JdbcTemplate
Trước khi bạn có thể bắt đầu sử dụng JdbcTemplate, bạn cần thêm nó vào classpath của dự án. Bạn có thể làm điều này dễ dàng bằng cách thêm dependency starter JDBC của Spring Boot vào phần build như sau:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>Bạn cũng sẽ cần một cơ sở dữ liệu để lưu trữ dữ liệu. Với mục đích phát triển, một cơ sở dữ liệu nhúng là hoàn toàn phù hợp. Tôi ưu tiên sử dụng cơ sở dữ liệu nhúng H2, vì vậy tôi đã thêm dependency sau vào phần build:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Theo mặc định, tên cơ sở dữ liệu sẽ được tạo ngẫu nhiên. Nhưng điều này khiến việc xác định URL của cơ sở dữ liệu trở nên khó khăn nếu, vì lý do nào đó, bạn cần kết nối đến cơ sở dữ liệu thông qua H2 console (mà Spring Boot DevTools cung cấp tại http://localhost:8080/h2-console. Do đó, bạn nên cố định tên cơ sở dữ liệu bằng cách thiết lập một vài thuộc tính trong application.properties, như minh họa sau:
spring.datasource.generate-unique-name=false
spring.datasource.name=tacocloudHoặc, nếu bạn thích, hãy đổi tên application.properties thành application.yml và thêm các thuộc tính theo định dạng YAML như sau:
spring:
datasource:
generate-unique-name: false
name: tacocloudViệc chọn giữa định dạng file .properties và định dạng YAML là tùy bạn. Spring Boot có thể làm việc tốt với cả hai. Vì cấu trúc rõ ràng và khả năng đọc dễ dàng hơn của YAML, chúng ta sẽ sử dụng YAML cho các thuộc tính cấu hình trong phần còn lại của cuốn sách.
Bằng cách đặt thuộc tính spring.datasource.generate-unique-name thành false, chúng ta đang yêu cầu Spring không tạo giá trị tên cơ sở dữ liệu ngẫu nhiên và duy nhất. Thay vào đó, nó sẽ sử dụng giá trị được thiết lập cho thuộc tính spring.datasource.name. Trong trường hợp này, tên cơ sở dữ liệu sẽ là "tacocloud". Do đó, URL cơ sở dữ liệu sẽ là "jdbc:h2:mem:tacocloud", bạn có thể sử dụng URL này trong JDBC URL để kết nối thông qua H2 console.
Sau này, bạn sẽ thấy cách cấu hình ứng dụng để sử dụng một cơ sở dữ liệu bên ngoài. Nhưng hiện tại, hãy tiếp tục viết một repository để truy xuất và lưu dữ liệu Ingredient.
ĐỊNH NGHĨA CÁC JDBC REPOSITORY
Repository cho Ingredient của bạn cần thực hiện các thao tác sau:
- Truy vấn tất cả các nguyên liệu (ingredient) thành một tập hợp các đối tượng
Ingredient - Truy vấn một
Ingredienttheoid - Lưu một đối tượng
Ingredient
Interface IngredientRepository sau đây định nghĩa ba thao tác đó dưới dạng khai báo phương thức:
package tacos.data;
import java.util.Optional;
import tacos.Ingredient;
public interface IngredientRepository {
Iterable<Ingredient> findAll();
Optional<Ingredient> findById(String id);
Ingredient save(Ingredient ingredient);
}Mặc dù interface đã nắm bắt được bản chất những gì bạn cần từ một repository cho nguyên liệu (Ingredient), bạn vẫn cần phải viết phần hiện thực của IngredientRepository sử dụng JdbcTemplate để truy vấn cơ sở dữ liệu. Đoạn mã sau đây là bước đầu tiên trong việc viết phần hiện thực đó.
Listing 3.4 Bắt đầu một repository nguyên liệu với JdbcTemplate
package tacos.data;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import tacos.Ingredient;
@Repository
public class JdbcIngredientRepository implements IngredientRepository {
private JdbcTemplate jdbcTemplate;
public JdbcIngredientRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// ...
}Như bạn thấy, JdbcIngredientRepository được chú thích với @Repository. Chú thích này là một trong số các chú thích theo khuôn mẫu (stereotype) mà Spring định nghĩa, bao gồm @Controller và @Component. Bằng cách chú thích JdbcIngredientRepository với @Repository, bạn khai báo rằng nó nên được Spring tự động phát hiện thông qua quét thành phần (component scanning) và khởi tạo dưới dạng một bean trong context của ứng dụng Spring.
Khi Spring tạo bean JdbcIngredientRepository, nó sẽ tiêm tự động (autowire) JdbcTemplate vào thông qua constructor. Đó là vì khi một class chỉ có một constructor, Spring sẽ tự động áp dụng cơ chế autowiring qua các tham số của constructor đó. Nếu có nhiều hơn một constructor, hoặc nếu bạn muốn chỉ rõ autowiring một cách tường minh, bạn có thể chú thích constructor bằng @Autowired như sau:
@Autowired
public JdbcIngredientRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}Constructor này gán JdbcTemplate vào một biến instance, biến này sẽ được sử dụng trong các phương thức khác để truy vấn và chèn dữ liệu vào cơ sở dữ liệu. Nói về các phương thức đó, hãy xem xét phần hiện thực của findAll() và findById() trong đoạn mã sau.
Listing 3.5 Truy vấn cơ sở dữ liệu với JdbcTemplate
@Override
public Iterable<Ingredient> findAll() {
return jdbcTemplate.query(
"select id, name, type from Ingredient",
this::mapRowToIngredient);
}
@Override
public Optional<Ingredient> findById(String id) {
List<Ingredient> results = jdbcTemplate.query(
"select id, name, type from Ingredient where id=?",
this::mapRowToIngredient,
id);
return results.size() == 0 ?
Optional.empty() :
Optional.of(results.get(0));
}
private Ingredient mapRowToIngredient(ResultSet row, int rowNum)
throws SQLException {
return new Ingredient(
row.getString("id"),
row.getString("name"),
Ingredient.Type.valueOf(row.getString("type")));
}Cả findAll() và findById() đều sử dụng JdbcTemplate theo cách tương tự. Phương thức findAll(), với mục đích trả về một tập hợp các đối tượng, sử dụng phương thức query() của JdbcTemplate. Phương thức query() nhận vào câu lệnh SQL để truy vấn cũng như một hiện thực của RowMapper trong Spring để ánh xạ mỗi dòng trong tập kết quả thành một đối tượng. query() cũng nhận thêm một (hoặc nhiều) đối số cuối cùng là danh sách các tham số cần thiết cho câu truy vấn. Nhưng trong trường hợp này, không có tham số nào cần thiết.
Ngược lại, phương thức findById() cần có một mệnh đề where trong câu truy vấn để so sánh giá trị của cột id với giá trị id được truyền vào phương thức. Do đó, lời gọi query() bao gồm tham số id như đối số cuối cùng. Khi truy vấn được thực thi, dấu ? trong câu lệnh SQL sẽ được thay thế bằng giá trị này.
Như thể hiện trong Listing 3.5, đối số RowMapper cho cả findAll() và findById() được truyền vào dưới dạng tham chiếu phương thức (method reference) đến phương thức mapRowToIngredient(). Cách sử dụng method reference hoặc lambda trong Java rất tiện lợi khi làm việc với JdbcTemplate, như một lựa chọn thay thế cho việc viết một lớp RowMapper riêng biệt. Tuy nhiên, nếu vì lý do nào đó bạn muốn hoặc cần một RowMapper tường minh, thì phần hiện thực sau của findById() sẽ chỉ cho bạn cách thực hiện điều đó:
@Override
public Ingredient findById(String id) {
return jdbcTemplate.queryForObject(
"select id, name, type from Ingredient where id=?",
new RowMapper<Ingredient>() {
public Ingredient mapRow(ResultSet rs, int rowNum)
throws SQLException {
return new Ingredient(
rs.getString("id"),
rs.getString("name"),
Ingredient.Type.valueOf(rs.getString("type")));
};
}, id);
}Đọc dữ liệu từ cơ sở dữ liệu chỉ là một phần của câu chuyện. Vào một thời điểm nào đó, dữ liệu phải được ghi vào cơ sở dữ liệu để có thể đọc lại. Hãy cùng xem cách triển khai phương thức save().
CHÈN MỘT HÀNG DỮ LIỆU
Phương thức update() của JdbcTemplate có thể được sử dụng cho bất kỳ truy vấn nào ghi hoặc cập nhật dữ liệu trong cơ sở dữ liệu. Và như được minh họa trong đoạn mã sau, nó có thể được sử dụng để chèn dữ liệu vào cơ sở dữ liệu.
Listing 3.6 Chèn dữ liệu với JdbcTemplate
@Override
public Ingredient save(Ingredient ingredient) {
jdbcTemplate.update(
"insert into Ingredient (id, name, type) values (?, ?, ?)",
ingredient.getId(),
ingredient.getName(),
ingredient.getType().toString());
return ingredient;
}Vì không cần phải ánh xạ dữ liệu ResultSet vào một đối tượng, nên phương thức update() đơn giản hơn nhiều so với query(). Nó chỉ yêu cầu một chuỗi String chứa câu lệnh SQL cần thực hiện cũng như các giá trị gán cho bất kỳ tham số nào của truy vấn. Trong trường hợp này, truy vấn có ba tham số, tương ứng với ba tham số cuối cùng của phương thức save(), cung cấp ID, tên và loại của nguyên liệu.
Với JdbcIngredientRepository đã hoàn chỉnh, bạn có thể tiêm (inject) nó vào DesignTacoController và sử dụng nó để cung cấp danh sách các đối tượng Ingredient thay vì sử dụng các giá trị được mã hóa cứng (hardcoded) như bạn đã làm trong chương 2. Các thay đổi đối với DesignTacoController được trình bày tiếp theo.
Listing 3.7 Tiêm và sử dụng repository trong controller
@Controller
@RequestMapping("/design")
@SessionAttributes("tacoOrder")
public class DesignTacoController {
private final IngredientRepository ingredientRepo;
@Autowired
public DesignTacoController(
IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
@ModelAttribute
public void addIngredientsToModel(Model model) {
Iterable<Ingredient> ingredients = ingredientRepo.findAll();
Type[] types = Ingredient.Type.values();
for (Type type : types) {
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients, type));
}
}
// ...
}Phương thức addIngredientsToModel() sử dụng phương thức findAll() của IngredientRepository đã được tiêm vào để truy xuất tất cả nguyên liệu từ cơ sở dữ liệu. Sau đó, nó phân loại chúng thành các loại nguyên liệu riêng biệt trước khi thêm vào mô hình (model).
Bây giờ chúng ta đã có một IngredientRepository để truy xuất các đối tượng Ingredient, chúng ta cũng có thể đơn giản hóa IngredientByIdConverter mà chúng ta đã tạo ở chương 2, thay thế Map chứa các đối tượng Ingredient được mã hóa cứng bằng một lời gọi đơn giản tới phương thức IngredientRepository.findById(), như được trình bày dưới đây.
Listing 3.8 Đơn giản hóa IngredientByIdConverter
package tacos.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import tacos.Ingredient;
import tacos.data.IngredientRepository;
@Component
public class IngredientByIdConverter implements Converter<String, Ingredient> {
private IngredientRepository ingredientRepo;
@Autowired
public IngredientByIdConverter(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
@Override
public Ingredient convert(String id) {
return ingredientRepo.findById(id).orElse(null);
}
}Bạn gần như đã sẵn sàng để khởi chạy ứng dụng và thử nghiệm những thay đổi này. Nhưng trước khi bạn có thể bắt đầu đọc dữ liệu từ bảng Ingredient được tham chiếu trong các truy vấn, bạn nên tạo bảng đó và điền vào một số dữ liệu nguyên liệu mẫu.
