在处理关系型数据的时候, Java开发人员有多种可选方案, 其中最常见的是JDBC和JPA。 Spring同时支持这两种抽象形式, 能够让JDBC或JPA的使用更加容易。
Spring对JDBC的支持要归功于JdbcTemplate类。 JdbcTemplate提供了一种特殊的方式, 通过这种方式, 开发人员在对关系型数据库执行SQL操作的时候能够避免使用JDBC时常见的繁文缛节和样板式代码。
Spring对JDBC的支持要归功于JdbcTemplate类。 JdbcTemplate提供了一种特殊的方式, 通过这种方式, 开发人员在对关系型数据库执行SQL操作的时候能够避免使用JDBC时常见的繁文缛节和样板式代码。
不使用JdbcTemplate类查询
@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 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) {
// ??? 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;
}
使用JdbcTemplate类
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")));
}
不使用JdbcTemplate类,需要自己创建连接、 创建语句以及关闭连接、 语句和结果集的清理等。
而使用JdbcTemplate类,仅仅关注执行查询(调用JdbcTemplate的queryForObject()) 和将结果映射到Ingredient对象(在mapRowToIngredient()方法中)上。
在将对象持久化到数据库的时候, 通常最好有一个字段作为对象的唯一标识。 Ingredient类现在已经有了一个id字段, 但是我们还需要将id字段添加到Taco和Order类中。
除此之外, 记录Taco和Order是何时创建的可能会非常有用。 所以, 还要为每个对象添加一个字段来捕获它所创建的日期和时间。
@Data
public class Taco {
private Long id;
private Date createdAt;
...
}
@Data
public class Order {
private Long id;
private Date placedAt;
...
}
在开始使用JdbcTemplate之前,需要添加JDBC的依赖,只需要在Spring Boot的JDBC starter依赖添加到构建文件中就可以了:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
还需要一个存储数据的数据库。 这里使用H2嵌入式数据库, 将如下的依赖添加到构建文件中:
<dependency>
<groupId>com.h2databasegroupId>
<artifactId>h2artifactId>
<scope>runtimescope>
dependency>
Ingredient repository需要完成以下操作。
IngredientRepository接口以方法声明的方式定义了3个操作:
public interface IngredientRepository {
Iterable<Ingredient> findAll();
Ingredient findOne(String id);
Ingredient save(Ingredient ingredient);
}
实现IngredientRepository接口, 使用JdbcTemplate来查询数据库。
@Repository
public class IngredientRepositoryImpl implements IngredientRepository {
@Autowired
private JdbcTemplate 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 save(Ingredient ingredient) {
jdbc.update("insert into Ingredient (id, name, type) values (?, ?, ?)",
ingredient.getId(),
ingredient.getName(),
ingredient.getType().toString());
return ingredient;
}
}
IngredientRepositoryImpl 添加了@Repository注解。IngredientRepositoryImpl 添加@Repository注解之后, Spring的组件扫描就会自动发现它, 并且会将其初始化为Spring应用上下文中的bean。
@Autowired注解将JdbcTemplate注入进来。 给JdbcTemplate赋值一个实例变量, 这个变量会被其他方法用来执行数据库查询和插入操作。
findAll()和findOne()以相同的方式使用了JdbcTemplate。
findAll()方法预期返回一个对象的集合, 它使用了JdbcTemplate的query()方法。
query()会接受要执行的SQL以及Spring RowMapper的一个实现(用来将结果集中的每行数据映射为一个对象)。
findOne()方法预期只会返回一个Ingredient对象, 所以它使用了JdbcTemplate的queryForObject()方法。queryForObject()方法的运行方式和query()非常类似, 只不过它只返回一个对象, 而不是对象的List。
save()方法向数据库中写入数据,JdbcTemplate的update()方法可以用来执行向数据库中写入或更新数据的查询语句,在save方法中使用jdbc中的update()方法。
update()方法只需要一个包含待执行SQL的String以及每个查询参数对应的值即可。
IngredientRepository编写完成之后, 我们就可以将其注入到DesignTacoController中了, 然后使用它来提供Ingredient对象的列表。
修改后的DesignTacoController
@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
@Autowired
private IngredientRepository ingredientRepository;
@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";
}
...
}
在数据库中定义出表结构和向表中写入数据。
在数据库中将定义如下的表:
create table if not exists Ingredient (
id varchar(4) not null primary key,
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);
将上面的文件命名为schema.sql,并放到“src/main/resources”文件夹下。应用启动的时候将会基于数据库执行这个文件中的SQL。
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');
在数据库中预加载一些配料数据。Spring Boot还会在应用启动的时候执行根类路径下名为data.sql的文件。可以将上面的sql文件命名为data.sql,将文件放在src/main/resources/路径下。
有以下两种保存数据的方法。
直接使用update()方法。
使用SimpleJdbcInsert包装器类。
IngredientRepositoryImpl 的save()方法就是使用JdbcTemplate的update()方法将Ingredient对象保存到了数据库中。
使用JdbcTemplate保存数据
在程序中创建taco和order的repository类,类中唯一需要做的事情就是保存对应的对象。
TacoRepository接口
public interface TacoRepository {
Taco save(Taco design);
}
OrderRepository接口
public interface OrderRepository {
Order save(Order order);
}
保存taco的时候需要同时将与该taco关联的配料保存到Taco_Ingredients表中。 与之类似, 保存订单的时候, 需要同时将与该订单关联的taco保存到Taco_Order_Tacos表中。 这样看来, 保存taco和订单就会比保存配料更困难一些。
save()方法首先保存必要的taco设计细节(比如, 名称和创建时间) , 然后对Taco对象中的每种配
料都插入一行数据到Taco_Ingredients中。
@Repository
public class TacoRepositoryImpl implements TacoRepository {
@Autowired
private JdbcTemplate 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());
PreparedStatementCreatorFactory preparedStatementCreatorFactory =
new PreparedStatementCreatorFactory("INSERT into Taco (name,createdAt) values (?,?)", Types.VARCHAR,Types.TIMESTAMP);
preparedStatementCreatorFactory.setReturnGeneratedKeys(true); //不加这句话会导致keyHolder.getKey()获取不到id
PreparedStatementCreator psc = preparedStatementCreatorFactory.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(), 最后的这个方法会保存每种配料。
当向Taco中插入一行数据的时候, 我们需要知道数据库生成的ID,这样我们才可以在每个配料信息中引用它。 这里的update()方法需要接受一个PreparedStatementCreator和一个KeyHolder。 KeyHolder将会为我们提供生成的taco ID。
创建PreparedStatementCreator,需要创建PreparedStatementCreatorFactory, 并将我们要执行的
SQL传递给它, 同时还要包含每个查询参数的类型。 调用工厂类的newPreparedStatementCreator()方法, 并将查询参数所需的值传递进来, 这样才能生成一个PreparedStatementCreator。
调用update()将PreparedStatementCreator和KeyHolder传递进来,keyHolder.getKey().longValue()返回taco的ID。
接下来轮询Taco中的每个Ingredient,并调用saveIngredientToTaco()。 saveIngredientToTaco()使用更简单的update()形式来将对配料的引用保存到Taco_Ingredients表中。
对于TacoRepository来说, 剩下的事情就是将它注入到DesignTacoController中, 并在保存taco的时候调用它。
@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";
}
...
}
@SessionAttributes(“order”),类级别的@SessionAttributes能够指定模型对象要保存在session中, 这样才能跨请求使用。
@SessionAttributes的使用:
如果在类上面使用了@SessionAttributes(“attributeName”)注解,而本类中恰巧存在attributeName,则会将attributeName放入到session作用域。
如果在类上面使用了@SessionAttributes(“attributeName”)注解,SpringMVC会在执行方法之前,自动从session中读取key为attributeName的值,并注入到Model中。
@ModelAttribute运用详解
被@ModelAttribute注解的方法会在Controller每个方法执行之前都执行,会将方法中返回的对象放在Model中,会将对象带入对应的Controller方法中。
使用@ModelAttribute注解的参数,意思是从前面的Model中提取对应名称的属性。
在检查完校验错误之后, processDesign()使用注入的TacoRepository来保存taco。 然后, 它将Taco对象保存到session里面的Order中
使用SimpleJdbcInsert插入数据
在保存订单的时候,不仅要将订单数据保存到Taco_Order表中, 还要将订单对每个taco的引用保存到
Taco_Order_Tacos表中。 引入SimpleJdbcInsert, 这个对象对JdbcTemplate进行了包装, 能够更容易地将数据插入到表中。
@Repository
public class OrderRepositoryImpl implements OrderRepository {
private JdbcTemplate jdbc;
private SimpleJdbcInsert orderInserter;
private SimpleJdbcInsert orderTacoInserter;
private ObjectMapper objectMapper;
@Autowired
public OrderRepositoryImpl(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.setCreatedAt(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.getCreatedAt());
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);
}
}
在构造器中会创建SimpleJdbcInsert的两个实例,用来把值插入到Taco_Order和Taco_Order_Tacos表中。
第一个实例赋值给了orderInserter实例变量, 配置为与Taco_Order表协作, 并且假定id属性将会由数据库提供或生成。
第二个实例赋值给了orderTacoInserter实例变量, 配置为与Taco_Order_Tacos表协作, 但是没有声明该表中ID是如何生成的。
该构造器还创建了Jackson中ObjectMapper类的一个实例, 并将其赋值给一个实例变量。
save()方法没有保存任何内容, 只是定义了保存Order及其关联的Taco对象的流程,并将实际的持久化任务委托给了saveOrderDetails()和saveTacoToOrder()。
SimpleJdbcInsert有两个非常有用的方法来执行数据插入操作:execute()和execute AndReturnKey()。
它们都接受Map
在saveOrderDetails()中, 我决定使用Jackson的ObjectMapper及其convertValue()方法, 以便于将Order转换为Map。Map创建完成之后,将Map中placedAt条目的值设置为Order对象placedAt属性的值。 调用orderInserter的executeAnd ReturnKey()方法了。 该方法会将订单信息保存到Taco_Order
表中, 并以Number对象的形式返回数据库生成的ID, 继而调用longValue()方法将返回值转换为long类型。
saveTacoToOrder()方法要简单得多。创建了一个Map并设置对应的值。调用orderTacoInserter的execute()方法就能执行插入操作了。
将OrderRepository注入到OrderController中并开始使用它了。
@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
public class OrderController {
@Autowired
private OrderRepository orderRepository;
@GetMapping("/current")
public String orderForm() {
return "orderForm";
}
@PostMapping
public String processOrder(@Valid Order order, Errors errors,SessionStatus sessionStatus) {
if (errors.hasErrors()) {
return "orderForm";
}
orderRepository.save(order);
sessionStatus.setComplete();
return "redirect:/";
}
}
将OrderRepository注入到控制器中,OrderRepository的save()方法进行保存提交的Order对象。
订单保存完成之后, 我们就不需要在session中持有它了。所以,processOrder()方法请求了一个SessionStatus参数, 并调用了它的setComplete()方法来重置session。
相对于普通的JDBC,Spring的JdbcTemplate和SimpleJdbcInsert能够极大地简化关系型数据库的使用。 但是, 使用JPA会更加简单。
尝试运行,发现很多问题。Taco类getIngredients()返回值问题,表单中的Ingredient都是String类型,而此时没有办法将String类型转化为Ingredient,需要配置Converter类将Sring转化为Ingredient类。
添加如下类,完成String到Ingredient的转换。
@Component
public class IngredientByIdConverter implements Converter<String,Ingredient> {
@Autowired
private IngredientRepository ingredientRepository;
@Override
public Ingredient convert(String id) {
return ingredientRepository.findOne(id);
}
}
问题:JdbcTacoRepository的keyHolder.getKey().longValue()空指针问题,上面代码中已经解决。
问题:H2控制台访问问题,在properties文件中,设置url,用户和密码。输入设置的url,用户,密码,进行访问。
spring.thymeleaf.cache=false
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.sql.init.schema-locations=classpath:db/schema.sql
spring.sql.init.data-locations=classpath:db/data.sql
spring.h2.console.enabled=true
Spring Boot应用可以通过JPA starter来添加Spring Data JPA。 这个starter依赖不仅会引入Spring Data JPA, 还会传递性地将Hibernate作为JPA实现引入进来:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
打开Ingredient、 Taco和Order类, 并为其添加一些注解。
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注解, 以便于将其指定为数据库中唯一标识该实体的属性。
@NoArgsConstructor注解。JPA需要实体有一个无参的构造器, Lombok的@NoArgsConstructor注解能够实现这一点。 但是, 我们不想直接使用它, 因此通过将access属性设置AccessLevel.PRIVATE使其变成私有的。 因为这里有必须要设置的final属性, 所以我们将force设置为true, 这样Lombok生成的构造器就会将它们设置为null。
@Data注解会为我们添加一个有参构造器,但使用@NoArgsConstructor注解后, 这个构造器就会被移除掉。 显式添加@RequiredArgsConstructor注解, 以确保除了private的无参构造器之外,还会有一个有参构造器。
@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;
@ManyToMany(targetEntity=Ingredient.class)
@Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients;
@PrePersist
void createdAt() {
this.createdAt = new Date();
}
}
Taco类现在添加了@Entity注解, 并为其id属性添加了@Id注解。 要依赖数据库自动生成ID值, 所以在这里还为id属性设置了@GeneratedValue, 将它的strategy设置为AUTO。
为了声明Taco与其关联的Ingredient列表之间的关系, 为ingredients添加了@ManyToMany注解。 每个Taco可以有多个Ingredient, 而每个Ingredient可以是多个Taco的组成部分。
@PrePersist注解。 在Taco持久化之前, 我们会使用这个方法将createdAt设置为当前的日期和时间。
@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;
...
@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();
}
}
@Table,它表明Order实体应该持久化到数据库中名为Taco_Order的表中。如果没有它, JPA默认会将实体持久化到名为Order的表中。
在JDBC版本的repository中, 我们显式声明想要repository提供的方法。 但是,借助 Spring Data,我们可以扩展CrudRepository接口。
CrudRepository定义了很多用于CRUD(创建、 读取、 更新、 删除)操作的方法。 注意, 它是参数化的, 第一个参数是repository要持久化的实体类型, 第二个参数是实体ID属性的类型。
import org.springframework.data.repository.CrudRepository;
import tacos.Ingredient;
public interface IngredientRepository extends CrudRepository<Ingredient, String> {
}
import org.springframework.data.repository.CrudRepository;
import tacos.Taco;
public interface TacoRepository extends CrudRepository<Taco, Long> {
}
import org.springframework.data.repository.CrudRepository;
import tacos.Order;
public interface OrderRepository extends CrudRepository<Order, Long> {
}
现在,有了3个repository。 你可能会想, 我们应该需要编写它们的实现类, 包括每个实现类所需的十多个方法。
Spring Data JPA带来的好消息是, 我们根本就不用编写实现类! 当应用启动的时候, Spring Data JPA会在运行期自动生成实现类。 可以直接使用这些repository了。 我们只需要像使用基于JDBC的实现那样将它们注入控制器中就可以了。
除了CrudRepository提供的基本CRUD操作之外,
Spring Data定义了一组小型的领域特定语言(DomainSpecific Language, DSL) , 在这里持久化的细节都是通过repository方法的签名来描述的,
repository方法是由一个动词、 一个可选的主题(Subject) 、 关键词By以及一个断言所组成的。
例子:
List<Order> readOrdersByDeliveryZipAndPlacedAtBetween(String deliveryZip, Date startDate, Date endDate);
动词是read。Spring Data会将get、 read和find视为同义词, 它们都是用来获取一个或多个实体的。 count作为动词, 它会返回一个int值, 代表匹配实体的数量。
方法的主题是可选的, 但是这里要查找的就是Order。 SpringData会忽略主题中大部分的单词。
deliveryZip属性的值必须要等于方法第一个参数传入的值。 关键字Between表明placedAt属性的值必须要位于方法最后两个参数的值之间。
除了Equals和Between操作之外, Spring Data方法签名还能包括如下的操作符:
还可以在方法名称的结尾处添加OrderBy, 实现结果集根据某个列排序。
尽管方法名称约定对于相对简单的查询非常有用,但是,对于更为复杂的查询, 方法名可能会面临失控的风险。 在这种情况下, 可以将方法定义为任何你想要的名称, 并为其添加@Query注解,从而明确指明方法调用时要执行的查询。
例子:
@Query("Order o where o.deliveryCity='Seattle'")
List<Order> readOrdersDeliveredInSeattle();