3.1 使用JDBC 读写数据
3.1 使用JDBC 读写数据
几十年来,关系数据库和
在处理关系数据时,
为了更好地理解
@Override
public Ingredient findOne(String id) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = dataSource.getConnection();
statement = connection.prepareStatement(
"select id, name, type from Ingredient");
statement.setString(1, id);
resultSet = statement.executeQuery();
Ingredient ingredient = null;
if(resultSet.next()) {
ingredient = new Ingredient(
resultSet.getString("id"),
resultSet.getString("name"),
Ingredient.Type.valueOf(resultSet.getString("type")));
}
return ingredient;
} catch (SQLException e) {
// ??? What should be done here ???
} finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
}
}
}
return null;
}
在程序清单
更糟糕的是,在创建连接或语句或执行查询时,可能会出现许多问题。这要求捕获一个
private JdbcTemplate jdbc;
@Override
public Ingredient findOne(String id) {
return jdbc.queryForObject(
"select id, name, type from Ingredient where id=?",
this::mapRowToIngredient, id);
}
private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
throws SQLException {
return new Ingredient(
rs.getString("id"),
rs.getString("name"),
Ingredient.Type.valueOf(rs.getString("type")));
}
程序清单
程序清单
3.1.1 为域适配持久化
在将对象持久化到数据库时,通常最好有一个惟一标识对象的字段。
此外,了解何时创建
@Data
public class Taco {
private Long id;
private Date createdAt;
...
}
因为使用
@Data
public class Order {
private Long id;
private Date placedAt;
...
}
同样,
域类现在已经为持久化做好了准备。让我们看看如何使用
3.1.2 使用JdbcTemplate
在开始使用
<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>
稍后,将看到如何配置应用程序来使用外部数据库。但是现在,让我们继续编写一个获取和保存
定义
- 查询所有的
Ingredient 使之变成一个Ingredient 的集合对象 - 通过它的
id 查询单个Ingredient - 保存一个
Ingredient 对象
以下
package tacos.data;
import tacos.Ingredient;
public interface IngredientRepository {
Iterable<Ingredient> findAll();
Ingredient findOne(String id);
Ingredient save(Ingredient ingredient);
}
尽管该接口体现了需要
package tacos.data;
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 findOne(String id) {
return jdbc.queryForObject(
"select id, name, type from Ingredient where id=?",
this::mapRowToIngredient, id);
}
private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
throws SQLException {
return new Ingredient(
rs.getString("id"),
rs.getString("name"),
Ingredient.Type.valueOf(rs.getString("type")));
}
?
。
如程序清单
@Override
public Ingredient findOne(String id) {
return jdbc.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);
}
从数据库读取数据只是问题的一部分。在某些情况下,必须将数据写入数据库以便能够读取。因此,让我们来看看如何实现
插入一行
@Override
public Ingredient save(Ingredient ingredient) {
jdbc.update(
"insert into Ingredient (id, name, type) values (?, ?, ?)",
ingredient.getId(),
ingredient.getName(),
ingredient.getType().toString());
return ingredient;
}
因为没有必要将
完成了
@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
private final IngredientRepository ingredientRepo;
@Autowired
public DesignTacoController(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
@GetMapping
public String showDesignForm(Model model) {
List<Ingredient> ingredients = new ArrayList<>();
ingredientRepo.findAll().forEach(i -> ingredients.add(i));
Type[] types = Ingredient.Type.values();
for (Type type : types) {
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients, type));
}
return "design";
}
...
}
请注意,
几乎已经准备好启动应用程序并尝试这些更改了。但是在开始从查询中引用的
3.1.3 定义模式并预加载数据
除了
图
图
Ingredient - 保存着原料信息Taco - 保存着关于taco 设计的重要信息Taco_Ingredient - 包含Taco 表中每一行的一个或多行数据,将Taco 映射到该Taco 的Ingredient Taco_Order - 保存着重要的订单细节Taco_Order_Tacos - 包含Taco_Order 表中的每一行的一个或多行数据,将Order 映射到Order 中的Tacos
下一个程序清单显示了创建表的
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);
最大的问题是把这个模式定义放在哪里。事实证明,
如果有一个名为
还需要用一些
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');
即使只开发了
3.1.4 插入数据
到此,已经了解了如何使用
虽然这是第一个很好的例子,但是它可能有点太简单了。保存数据可能比
- 直接使用
update() 方法 - 使用
SimpleJdbcInsert 包装类
让我们首先看看,当持久话需求比保存一个
使用
目前,
package tacos.data;
import tacos.Taco;
public interface TacoRepository {
Taco save(Taco design);
}
类似地,
package tacos.data;
import tacos.Order;
public interface OrderRepository {
Order save(Order order);
}
看起来很简单,对吧?没那么快。保存一个
要实现
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());
PreparedStatementCreator psc = new PreparedStatementCreatorFactory(
"insert into Taco (name, createdAt) values (?, ?)",
Types.VARCHAR, Types.TIMESTAMP
).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());
}
}
在
需要的
如程序清单
通过使用
回到
@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;
}
...
}
构造函数包含一个
说到
@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
@ModelAttribute(name = "order")
public Order order() {
return new Order();
}
@ModelAttribute(name = "taco")
public Taco taco() {
return new Taco();
}
@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";
}
...
}
关于程序清单
在检查验证错误之后,
实际上,
使用
保存一个
在保存订单方面,也存在类似的情况。不仅必须将订单数据保存到
首先创建一个
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();
}
...
}
与
第一个实例被分配给
构造函数还创建
现在让我们看看
@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);
}
通过将
随着
现在可以将
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:/";
}
}
除了将
一旦订单被保存,就不再需要它存在于
所有的
可能还会发现在数据库中进行挖掘是很有帮助的。因为使用