Spring实战
第3章 使用数据
JDBC读取和写入数据
- 在处理关系型数据的时候,Java开发人员有多种可选方案,其中最常见的是JDBC和JPA。Spring同时支持这两种抽象形式,能够让JDBC或JPA的使用更加容易。在本节中,我们将会讨论Spring如何支持JDBC,并在之后讨论Spring对JPA的支持。
- Spring对JDBC的支持要归功于JdbcTemplate类。可以使我们在执行SQL操作时避免使用样板式代码。
- 下面是一个简单的查询。
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 where id = ?"
);
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)
{
// 无法解决问题
}
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;
}
- 在创建链接、创建语句或执行查询的时候,可能会出现很多错误。这就要求我们捕获SQLException,它对于找出哪里出现了问题或者如何解决问题可能会有所帮助,也可能毫无用处。
- SQLException是一个检查型异常,他需要在catch代码块中进行处理。但是,对于常见问题,如创建到数据库的连接失败或者输入的查询有错误,在catch代码块却无能为力,并且有可能要继续抛出以便于上游进行处理。作为对比,下面是使用JdbcTemplate的方式。
private JdbcTemplate jdbc;
@Override
public Ingredient findOne(String id){
return jdcb.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"))
);
}
调整领域对象以适应持久化
- 在将对象持久化到数据库的时候,通常最好有一个字段作为对象的唯一标示。Ingredient类现在已经有了一个id字段,但是我们还需要将id字段添加到Taco和Order类中。
- 除此之外,记录Taco和Order时何时创建的可能会很有用。所以,我们还会为每个对象添加一个字段来捕获他所创建的日期和时间。
package tacos;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.sql.Date;
import java.util.List;
@Data
public class Taco {
private Long id;
private Date createdA;
@NotNull
@Size(min=5, message="Name must be ad least 5 characters long")
private String name;
@NotNull(message = "You must choose at least 1 ingredient")
private List<String> ingredients;
}
- Order类也进行类似的变化。
使用JdbcTemplate
- 在开始使用JdbcTemplate之前,需要将其添加到项目的类路径中。同时,添加嵌入式数据库H2(比较方便简单)。
<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>
定义JDBC repository
- 我们的Ingredient repository需要完成如下操作:
- 查询所有配料信息,将他们放到一个Ingredient对象的集合中
- 根据id,查询单个Ingredient;
- 保存Ing对象。
- 如下的IngredientRepository接口以方法声明的方式定义了三个操作:
package tacos.data;
import tacos.Ingredient;
public interface IngredientRepository {
Iterable<Ingredient> finaAll();
Ingredient findOne(String id);
Ingredient save(Ingredient ingredient);
}
- 下面是编写一个IngredientRepository实现,使用JdbcTemplate来查询数据库。
package tacos.data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
@Repository
public class JDBCIngredientRepository implements IngredientRepository{
private JdbcTemplate jdbc;
@Autowired
public JDBCIngredientRepository(JdbcTemplate jdbc)
{
this.jdbc = jdbc;
}
}
- 可以看到,JDBCIngredientRepository添加了@Repository注解,Spring定义了一系列构造型注解,@Repository就是其中之一,其他注解还包括@Controller和@Component。为JDBCIngredientRepository添加了@Repository注解之后,Spring的组件扫描就会自动发现它,并且会将其初始化为Spring应用上下文中的bean。
- 当Spring创建JdbcIngredientRepository bean时,会通过@Autowired标注的构造器将JdbcTemplate注入进来。这个构造器将JdbcTemplate赋值给一个实例变量,这个变量会被其他方法用来执行数据库查询和插入操作。下面是findAll()和findOne()的实现:
@Override
public Ingredient findOne(String id) {
return jdbc.queryForObject(
"select id, name, type from Ingredient where id = ?",
this::mapRowToIngredient
);
}
@Override
public Iterable<Ingredient> finaAll() {
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"))
);
}
- findAll()和findOne()以相同的方式使用了JdbcTemplate。findAll()方法与其返回一个对象的集合,使用了JdbcTemplate的query()方法。该方法会接受要执行的SQL以及Spring RowMapper的一个实现(用以将结果集中的每行数据映射为一个对象)。该方法还能以最终参数的形式接受查询中所需的人以参数。但是在本例中不需要任何参数。
- findOne()方法预期只会返回一个Ingredient对象,所以使用了JdbcTemplate的queryForObject()方法。在本例中,他接受要执行的查询、RowMapper以及要获取的Ingredient的id,该id会替换查询中的"?"。
插入一行数据
- JdbcTemplate的update()方法可以用来执行向数据库写入或更新数据的查询语句。
@Override
public Ingredient save(Ingredient ingredient) {
jdbc.update(
"insert into Ingredient (id, name, type) values (?, ?, ?)",
ingredient.getId(),
ingredient.getName(),
ingredient.getType().toString()
);
return ingredient;
}
- 因为这里不需要将ResultSet数据映射为对象,所以update()方法要比query()或queryForObject()简单得多。它只需要一个包含待执行SQL的String以及每个查询参数对应的值即可。
- JdbcIngredientRepository编写完成之后,我们就可以将其注入到DesignTacoController中了,然后使用它来提供Ingredient对象的列表,不再使用硬编码的值。
private final IngredientRepository ingredientRepo;
@Autowired
public DesignTacoController(IngredientRepository ingredientRepo)
{
this.ingredientRepo = ingredientRepo;
}
@GetMapping
public String showDesignForm(Model model){
List<Ingredient> ingredients = new ArrayList<>();
ingredientRepo.finaAll().forEach(i->ingredients.add(i));
Type[] types = Ingredient.Type.values();
for(Type type : types)
{
model.addAttribute(type.toString().toLowerCase(), filterByType(ingredients, type));
}
return "design";
}
- 需要注意的是,showDesignForm()方法的第二行调用了注入的IngredientRepository的findAll()方法。该方法会从数据库中获取所有配料,并将它们过滤成不同的类型然后放到模型中。
定义模式和预加载数据
-
除了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,
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);
- 下面是预加载配料数据的sql。这两个sql都保存在"src/main/resources"文件中
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');
插入数据
- 现在已经粗略看到了如何使用JdbcTemplate将数据写入数据库中。JDBCIngredientRepository的save()方法使用JdbcTemplate的update()方法将Ingredient对象保存在了数据库中。
- 借助JdbcTemplate,保存数据有以下两种方法
- 直接使用update()方法
- 使用SimpleJdbcInsert包装器类。
使用JdbcTemplate保存数据
- 现在,taco和order的repository唯一需要做的事情就是保存对应的对象。为了保存Taco对象,TacoRepository声明了一个save()方法。
package tacos.data;
import tacos.Taco;
public interface TacoRepository {
Taco save(Taco design);
}
- 为了实现TacoRepository,我们需要用save()方法首先保存必要的taco设计细节(例如名称和创建时间),然后对Taco对象中的每种配料都插入一行数据到Taco_Ingredients中。
package tacos.data;
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;
import java.util.Date;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;
@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()
);
}
}
- 可以看到,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的时候调用它。
private final IngredientRepository ingredientRepo;
private TacoRepository designRepo;
@Autowired
public DesignTacoController(IngredientRepository ingredientRepo, TacoRepository designRepo)
{
this.ingredientRepo = ingredientRepo;
this.designRepo = designRepo;
}
- 也就是该构造器同时接受IngredientRepository和TacoRepository对象。并将其赋值给实例变量。
@ModelAttribute(name="order")
public Order order()
{
return new Order();
}
@ModelAttribute(name="taco")
public Taco taco()
{
return new Taco();
}
@PostMapping
public String processDesign(@Valid @ModelAttribute("design") Taco design, Errors errors, @ModelAttribute Order order){
if(errors.hasErrors())
return "design";
Taco saved = designRepo.save(design);
order.addDesign(saved);
log.info("Processing design:" + design);
return "redirect:/orders/current";
}
- DesignTacoController类添加了@SessionAttributes(“order”)注解,并且有一个新的带有@ModelAttribute注解的方法,即order()方法。与taco()方法类似,order()方法上的@ModelAttribute注解能够确保会在模型中创建一个Order对象。但是与模型中的Taco对象不同,我们需要订单信息在多个请求中都能出现,这样的话我们就能创建多个taco并将它们添加到该订单中。类级别的@SessionAttributes能够指定模型对象(如订单属性)要保存在session中,这样才能跨请求使用。
- 对taco设计的处理位于processDesign()方法中。该方法接受Order对象作为参数,同时还包括Taco和Errors对象。Order参数带有@ModelAttribute注解,表明它的值应该是来自模型的,Spring MVC不会尝试将请求参数绑定到它上面。
- 在检查完校验错误之后,processDesign()使用注入的TacoRepository来保存taco。然后,它将Taco对象保存到session里面的Order中。
- 实际上,在用户完成操作并提交订单表单之前,Order对象会一直保存在session中,并没有保存到数据库中。到时,OrderController需要调用OrderRepository的实现来保存订单。下面是这个实现类。
使用SimpleJdbcInsert插入数据
- Taco的ID是通过KeyHolder和PreparedStatementCreator获取的。
- 在保存订单的时候,存在类似的情况。不仅要将订单数据保存到Taco_Order表中,还要将订单对每个taco的引用保存到Taco_Order_Tacos表中。但是,此时不再使用繁琐的PreparedStatementCreator,而是引入SimpleJdbcInsert,该对象对JdbcTemplate进行了包装,能够更容易地将数据插入到表中。
package tacos.data;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
@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();
}
}
- JdbcOrderRepository通过构造器将JdbcTemplate注入进来。但是在这里,没有将JdbcTemplate直接赋给实例变量,而是使用它构建了两个SimpleJdbcInsert实例。第一个赋值给了orderInserter实例变量,配置为与Taco_Order表协作,并且假定id属性将会由数据库提供或生成。第二个实例赋值给了orderTacoInserter实例变量,配置为与Taco_Order_Tacos表协作,但是没有声明该表中ID如何生成。
- 该构造器还创建了Jackson中ObjectMapper类的实例,并将其赋值给一个实例变量。尽管Jackson的初衷是进行JSON处理,但是在这里可以帮助我们保存订单和关联的taco。
- 下面是save()方法的实现以及如何使用SimpleJdbcInsert实例。
@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(value = "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对应列名,value对应要插入的实际值。
- Order有很多属性,和对应的列有相同的名称。鉴于此,我们使用ObjectMapper及其convertValue()方法,以便于将Order转换为Map。之所以要这样做,是因为ObjectMapper会将Date属性转换为long,这导致与Taco_Order表中的placedAt字段不兼容。
- 现在可以将OrderRepository注入到OrderController中并开始使用了。下面是完整的OrderController
package tacos.web;
import lombok.extern.slf4j.Slf4j;
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;
import javax.validation.Valid;
@Slf4j
@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();
log.info("Order submitted: "+order);
return "redirect:/";
}
}
- 除了将OrderRepository注入到控制器中,OrderController唯一明显的变化就是processOrder()方法。在这个方法中,通过表单提交的Order对象会通过注入的OrderRepository的save()方法进行保存。
- 订单保存完之后,就不需要在session中持有他了。实际上,如果不清理掉,会继续与下一次请求的taco关联。
使用Spring Data JPA持久化数据
- Spring Data是一个非常大的伞形项目,由多个子项目组成,其中大多数子项目都关注对不同数据库类型进行数据持久化。比较流行的几个Spring Data项目包括:
- Spring Data JPA:基于关系型数据库进行JPA持久化
- Spring Data MongoDB:持久化到Mongo文档数据库
- Spring Data Neo4j:持久化到Neo4j图数据库
- Spring Data Redis:持久化到Redis Key-value存储
- Spring Data Cassandra:持久化到Cassandra数据库
- Spring Data 为所有项目提供了一项最有趣且最有用的特性,就是基于repository规范接口自动生成repository功能。
- 要了解Spring Data是如何运行的,我们需要重新开始,将前面基于JDBC的repository替换为使用Spring Data JPA的repository。首先需要将Spring Data JPA添加到项目的构建文件中。
添加Spring Data JPA到项目中
- Spring Data应用可以通过JPA starter来添加Spring Data JPA,这个starter以来不仅会引入Spring Data JPA,还会传递性地将Hibernate作为JPA实现引入进来。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
将领域对象标注为实体
- 在创建repository方面,Spring Data为我们做了很多很有用的事情。但是,在使用JPA映射注解标注领域对象方面,却没有提供太多助益。我们需要打开Ingredient、Taco和Order类并为其添加一些注解。下面是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
}
}
- 为了将Ingredient声明为JPA实体,必须添加@Entity注解。它的id属性需要使用@Id注解,以便于将其指定为数据库中唯一标示该实体的属性。
- 除了JPA特定的注解,我们还在类级别添加了@NoArgsConstructor注解。JPA需要实体有一个无参数的构造器,Lombok的@NoArgsConstructor注解能够帮助我们实现。但是并不想直接使用它,因此通过将access属性设置为AccessLevel.PRIVATE将其变成私有的。因为这里有必须要设置的final属性,所以我们将force设置为true,这样Lombok生成的构造器就会将它们设置为null。
- 我们还添加了一个@RequiredArgsConstructor注解。@Data注解会为我们添加一个有参构造器,但是使用使用@NoArgsConstructor后,这个构造器就会被移除,我们显式添加@RequiredArgeConstructor注解以确保除了private的无参构造器外还有一个有参构造器。
- 下面是Taco类
package tacos;
import lombok.Data;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Date;
import java.util.List;
@Data
@Entity
public class Taco {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Date createdAt;
@NotNull
@Size(min=5, message="Name must be ad least 5 characters long")
private String name;
@ManyToMany(targetEntity = Ingredient.class)
@NotNull(message = "You must choose at least 1 ingredient")
private List<Ingredient> ingredients;
@PrePersist
void createdAt(){
this.createdAt = new Date();
}
}
- 与Ingredient相似,Taco类现在添加了@Entity注解,并为其id属性添加了@Id注解。因为我们要依赖数据库自动生成ID值,所以在这里还为id属性设置了@GeneratedValue,将其strategy设置为AUTO
- 为了声明Taco与其关联的Ingredient列表之间的关系,我们为ingredients添加了@ManyToMany注解。每个Taco可以有多个Ingredient,而每个Ingredient可以是多个Taco组成部分。
- 这里有一个新的方法createdAt()并使用了@PrePersist注解。在Taco持久化之前,我们会使用这个方法将createdAt设置为当前的日期时间。最后,我们要将Order对象标注为实体。下面是Order类
package tacos;
import lombok.Data;
import org.hibernate.validator.constraints.CreditCardNumber;
import javax.persistence.*;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
import java.util.Date;
import java.util.ArrayList;
import java.util.List;
@Data
@Entity
@Table(name = "Taco_Order")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Date placedAt;
@NotBlank(message = "Name is required")
private String deliveryName;
@NotBlank(message = "Street is required")
private String deliveryStreet;
@NotBlank(message = "City is required")
private String deliveryCity;
@NotBlank(message = "State is required")
private String deliveryState;
@NotBlank(message = "Zip code is required")
private String deliveryZip;
@CreditCardNumber(message = "Not a valid credit card number")
private String ccNumber;
@Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$",
message = "Must be formatted MM/YY")
private String ccExpiration;
@Digits(integer = 3, fraction = 0, message = "Invalid CVV")
private String ccCVV;
@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();
}
}
- Order类基本与Taco的变化一样,但是,在类级别多了一个新的注解,即@Table。表明Order实体应该持久化到数据库中名为Taco_Order的表中。主要是因为如果没有的话,JPA会默认将实体持久化到名为Order的表中,但是order是SQL的保留字。
声明JPA repository
- 在JDBC版本的repository中,我们显式声明想要repository提供的方法。但是,借助Spring Data,我们可以扩展CrudRepository接口。举例来说,下面是新的IngredientRepository接口。
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.Ingredient;
public interface IngredientRepository
extends CrudRepository<Ingredient, String>{
}
- CrudRepository定义了很多用于CRUD(创建、读取、更新、删除)操作的方法。它是参数化的,第一个参数是repository要持久化的试题类型,第二个参数是实体ID属性的类型。对于IngredientRepository来说,参数应该是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> {
Order save(Order order);
}
- 现在有了3个repository。Spring Data JPA带来的好处是,我们不需要编写他们的实现类。当应用启动时,Spring Data JPA会在运行期自动生成实现类。这意味着,我们现在就可以使用这些repository了。只需要像使用基于JDBC的实现那样将它们注入控制器中即可。
- CrudRepository所提供的方法对于实体的通用持久化是非常有用的。但是如果我们的需求并不局限于基本持久化,那又该怎么办呢。下面是如何自定义repository来执行特定领域的查询。
自定义JPA repository
- 假设除了CrudRepository提供的基本CRUD操作之外,我们还需要获取投递到指定邮编(Zip)的订单。实际上,我们只需要添加如下的方法声明到OrderRepository中即可:
List<Order> findByDeliveryZip(String deliveryZip);
- 当创建repository实现的时候,Spring Data会检查repository接口的所有方法,解析方法的名称,并基于被持久化的对象来试图推测方法的目的。本质上,Spring Data定义了一组小型的领域特定语言(Domain-Specific Language, DSL),在这里持久化的细节都是通过repository方法的签名来描述的。
- Spring Data能够知道这个方法是要查找Order的,因为我们使用Order对CrudRepository进行了参数化。方法名findByDeliveryZip()确定该方法需要根据deliveryZip属性相匹配来查找Order,而deliveryZip的值就是作为参数传递到方法中来的。
- 例如
List<Order> readOrderByDeliveryZipAndPlaceAtBetween(String deliveryZip, Date startDate, Date endDate);
Spring Data在生成repository实现的时候是这样解析和理解方法名的:read也可以使用get、find表示读取数据,by开始声明要匹配的属性,deliveryzip表示匹配.delivery.zip属性,and表并列,placedat匹配.placeAt或.placed.at属性,between表示值必须要在给定范围内。 - 和between类似的操作符有很多种,除此之外,还可以添加AllIgnoringCase或AllIgnoreCase会忽略所有String对比的大小写。
- 可以在结尾处添加OrderBy实现结果结果集按照某个列排序,例如按照deliveryTo属性排序:
List<Order> findByDeliveryCityOrderByDeliveryTo(String city);
- 尽管方法名称约定对于相对简单的查询很有用,但是不难想象,对于更为复杂的查询方法名会面临失控风险。在这种方法下,可以将方法定义为任何名称,并为其添加@Query注解,指明方法调用时要执行的查询,如下所示:
@Query("from Order o where o.deliveryCity='Seattle'")
List<Order> readOrderDeliveryInSeattle();
- 在本例中,通过使用@Query,声明只查询所有投递到Seattle的订单。