一、使用 Spring JdbcTemplate:
首先使用 Spring 对 JDBC(Java Database Connectivity)的支持来消除样板代码。然后,将重新使用 JPA(Java Persistence API)处理数据存储库,从而消除更多代码。
Spring JDBC 支持起源于 JdbcTemplate
类。JdbcTemplate 提供了一种方法,通过这种方法,可以对关系数据库执行 SQL 操作,与通常使用 JDBC 不同的是,这里不需要满足所有的条件和样板代码。
在没有 JdbcTemplate 的情况下用 Java 执行一个简单的查询:
@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;
}
Ingredient - 保存着原料信息
Taco - 保存着关于 taco (玉米卷)设计的重要信息,
Taco_Ingredient - 包含 Taco 表中每一行的一个或多行数据,将 Taco 映射到该 Taco 的 Ingredient
Taco_Order - 保存着重要的订单细节
Taco_Order_Tacos - 包含 Taco_Order 表中的每一行的一个或多行数据,将 Order 映射到 Order 中的Tacos
使用JdbcTemplate 的方法
:
1、引入jdbc依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
数据库如h2:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
2、定义 JDBC 存储库
IngredientRepository
:
package tacos.data;
import tacos.Ingredient;
public interface IngredientRepository {
Iterable<Ingredient> findAll();
Ingredient findOne(String id);
Ingredient save(Ingredient ingredient);
}
TacoRepository
:
package tacos.data;
import tacos.Taco;
public interface TacoRepository {
Taco save(Taco design);
}
OrderRepository
:
package tacos.data;
import tacos.Order;
public interface OrderRepository {
Order save(Order order);
}
使用 JdbcTemplate 保存数据的两种方法包括:
直接使用 update() 方法
使用 SimpleJdbcInsert 包装类
使用 JdbcTemplate实现JdbcIngredientRepository
:
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;
@Override
public Ingredient findOne(String id) {
return jdbc.queryForObject(
"select id, name, type from Ingredient where id=?",
this::mapRowToIngredient, id);
}
@Override
public Iterable<Ingredient> findAll() {
return jdbc.query("select id, name, type from Ingredient",
this::mapRowToIngredient);
}
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 save(Ingredient ingredient) {
// 使用 update() 方法
jdbc.update(
"insert into Ingredient (id, name, type) values (?, ?, ?)",
ingredient.getId(),
ingredient.getName(),
ingredient.getType().toString());
return ingredient;
}
}
使用 JdbcTemplate 实现 TacoRepository:
JdbcTacoRepository
:
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;
}
// 保存 Ingredient 数据时使用的 update() 方法不能获得生成的 id,因此这里需要一个不同的 update() 方法。
// 需要的 update() 方法接受 PreparedStatementCreator 和 KeyHolder。KeyHolder 将提供生成的 Taco id,但是为了使用它,还必须创建一个 PreparedStatementCreator。
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());
}
}
二、使用 SimpleJdbcInsert 插入数据:
JdbcOrderRepository
:
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;
}
// SimpleJdbcInsert 有两个执行插入的有用方法:execute() 和 executeAndReturnKey()。
// 两者都接受 Map<String, Object>,其中 Map 键对应于数据插入的表中的列名,映射的值被插入到这些列中。
private long saveOrderDetails(Order order) {
// 使用 Jackson 的 ObjectMapper 及其 convertValue() 方法将 Order 转换为 Map。
// 这是必要的,否则 ObjectMapper 会将 Date 属性转换为 long,这与 Taco_Order 表中的 placedAt 字段不兼容
@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);
}
}
DesignTacoController
:
@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";
}
...
}
DesignTacoController
:
@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;
}
@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";
}
...
}
OrderController
:
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);
// 一旦订单被保存,就不再需要它存在于 session 中了。
// 事实上,如果不清除它,订单将保持在 session 中,包括其关联的 tacos,下一个订单将从旧订单中包含的任何 tacos 开始。
// 因此需要 processOrder() 方法请求 SessionStatus 参数并调用其 setComplete() 方法来重置会话。
sessionStatus.setComplete();
return "redirect:/";
}
}
三、使用 Spring Data 声明 JPA repositories:
使用 Spring Data JPA 持久化数据
Spring Data JPA - 针对关系数据库的持久化
Spring Data Mongo - 针对 Mongo 文档数据库的持久化
Spring Data Neo4j - 针对 Neo4j 图形数据库的持久化
Spring Data Redis - 针对 Redis 键值存储的持久化
Spring Data Cassandra - 针对 Cassandra 数据库的持久化
下面重新查看域对象并对它们进行注解以实现 JPA 持久化。
1、引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
如果想使用不同的 JPA 实现,那么至少需要排除 Hibernate 依赖,并包含所选择的 JPA 库。例如,要使用 EclipseLink 而不是 Hibernate,需要按如下方式更改构建:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<artifactId>hibernate-entitymanager</artifactId>
<groupId>org.hibernate</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.5.2</version>
</dependency>
2、为了将其声明为 JPA 实体
,必须使用 @Entity
注解。它的 id 属性必须使用 @Id
进行注解,以便将其指定为惟一标识数据库中实体的属性。
Ingredient
:
package tacos;
import javax.persistence.Entity;
import javax.persistence.Id;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@Entity
public class Ingredient {
@Id
private final String id;
private final String name;
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
除了特定于 JPA 的注解之外,还在类级别上添加了 @NoArgsConstructor 注解。
JPA 要求实体有一个无参构造函数,所以 Lombok 的 @NoArgsConstructor 实现了这一点。
但是要是不希望使用它,可以通过将 access 属性设置为 AccessLevel.PRIVATE 来将其设置为私有。
因为必须设置 final 属性,所以还要将 force 属性设置为 true,这将导致 Lombok 生成的构造函数将它们设置为 null。
还添加了一个 @RequiredArgsConstructor。
@Data 隐式地添加了一个必需的有参构造函数,但是当使用 @NoArgsConstructor 时,该构造函数将被删除。
显式的 @RequiredArgsConstructor 确保除了私有无参数构造函数外,仍然有一个必需有参构造函数。
把 Taco 注解为实体:
Taco
:
package tacos;
import java.util.Date;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.PrePersist;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
@Data
@Entity
public class Taco {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@NotNull
@Size(min=5, message="Name must be at least 5 characters long")
private String name;
private Date createdAt;
// 要声明 Taco 及其相关 Ingredient 列表之间的关系,可以使用 @ManyToMany 注解 ingredient 属性。
// 一个 Taco 可以有很多 Ingredient,一个 Ingredient 可以是很多 Taco 的一部分
@ManyToMany(targetEntity=Ingredient.class)
@Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients;
// @PrePersist 注解。
// 将使用它将 createdAt 属性设置为保存 Taco 之前的当前日期和时间
@PrePersist
void createdAt() {
this.createdAt = new Date();
}
}
与 Ingredient 一样,Taco 类现在使用 @Entity
注解,其 id 属性使用 @Id
注解。因为依赖于数据库自动生成 id 值,所以还使用 @GeneratedValue
注解 id 属性
,指定自动
策略。
Order 注解为 JPA 实体:
Order
:
package tacos;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.hibernate.validator.constraints.NotBlank;
import lombok.Data;
@Data
@Entity
@Table(name="Taco_Order") // 这指定订单实体应该持久化到数据库中名为 Taco_Order 的表中
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private Date placedAt;
// ...
@ManyToMany(targetEntity=Taco.class)
private List<Taco> tacos = new ArrayList<>();
public void addDesign(Taco design) {
this.tacos.add(design);
}
@PrePersist
void placedAt() {
this.placedAt = new Date();
}
}
3、声明 JPA repository
但是使用 Spring Data,扩展 CrudRepository 接口。
IngredientRepository:
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.Ingredient;
// CrudRepository 为 CRUD(创建、读取、更新、删除)操作声明了十几个方法。
// 注意,它是参数化的,第一个参数是存储库要持久化的实体类型,第二个参数是实体 id 属性的类型。
public interface IngredientRepository extends CrudRepository<Ingredient, String> {
}
TacoRepository
:
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.Taco;
public interface TacoRepository extends CrudRepository<Taco, Long> {
}
OrderRepository
:
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.Order;
public interface OrderRepository extends CrudRepository<Order, Long> {
// 自定义 JPA repository:
// 在 findByDeliveryZip() 中,动词是 find,谓词是 DeliveryZip,主语没有指定,暗示是一个 Order。
List<Order> findByDeliveryZip(String deliveryZip);
// 需要查询在给定日期范围内投递给指定邮政编码的所有订单
// deliveryZip 属性必须与传递给方法的第一个参数的值一致。
// Between 关键字表示 deliveryZip 的值必须位于传入方法最后两个参数的值之间.
List<Order> readOrdersByDeliveryZipAndPlacedAtBetween(
String deliveryZip, Date startDate, Date endDate);
// AllIgnoringCase 或 AllIgnoresCase 来忽略所有 String 比较的大小写。
List<Order> findByDeliveryToAndDeliveryCityAllIgnoresCase(
String deliveryTo, String deliveryCity);
// 将 OrderBy 放在方法名的末尾,以便根据指定的列对结果进行排序。
List<Order> findByDeliveryCityOrderByDeliveryTo(String city);
// 可以随意将方法命名为任何想要的名称,
// 并使用 @Query 对其进行注解,以显式地指定调用方法时要执行的查询
@Query("Order o where o.deliveryCity='Xxx'")
List<Order> readOrdersDeliveredInXxx();
}
4、Spring Data 方法签名还包括:
IsAfter | After | IsGreaterThan | GreaterThan |
IsGreaterThanEqual | GreaterThanEqual | ||
IsBefore | Before | IsLessThan | LessThan |
IsLessThanEqual | LessThanEqual | ||
IsBetween | Between | ||
IsNull | Null | ||
IsNotNull | NotNull | ||
IsIn | In | ||
IsNotIn | NotIn | ||
IsStartingWith | StartingWith | StartsWith | |
IsEndingWith | EndingWith | EndsWith | |
IsContaining | Containing | Contains | |
IsLike | Like | ||
IsNotLike | NotLike | ||
IsTrue | True | ||
IsFalse | False | ||
Is | Equals | ||
IsNot | Not | ||
IgnoringCase | IgnoresCase |
当应用程序启动时,Spring Data JPA 会动态地自动生成一个实现。
这意味着 repository 可以从一开始就使用。
只需将它们注入到控制器中,就像在基于 JDBC 的实现中所做的那样。
CrudRepository 提供的基本 CRUD 操作之外。
Spring Data 还将 find、read 和 get 理解为获取一个或多个实体的同义词。
repository 的方法由一个动词、一个可选的主语、单词 by 和一个谓词组成。