Говоря о методе processDesign(), его изменения немного более обширны, чем изменения, которые вы сделали в showDesignForm(). В следующем списке показан новый метод processDesign().
Листинг 3.12 сохранение состава 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";
}
...
}
Первое, что можно заметить в коде в листинге 3.12, это то, что DesignTacoController теперь аннотируется SessionAttributes("order") и что у него есть новый аннотированный @ModelAttribute метод order(). Как и в случае с методом taco(), аннотация @ModelAttribute для order() гарантирует, что объект Order будет создан в модели. Но в отличие от объекта Taco в сеансе необходимо, чтобы order присутствовал в нескольких запросах, чтобы можно было создать несколько тако и добавить их в заказ. Аннотация @SessionAttributes на уровне класса задает любые объекты модели, такие как атрибут order, которые должны храниться в сеансе и доступны для нескольких запросов.
Реальная обработка состава taco происходит в методе processDesign(), который теперь принимает объект Order в качестве параметра, в дополнение к объектам Taco и Errors. Параметр Order аннотируется @ModelAttribute, чтобы указать, что его значение должно исходить из модели и что Spring MVC не должен пытаться привязать к нему параметры запроса.
После проверки на наличие ошибок, processDesign() использует внедренный TacoRepository для сохранения taco. Затем он добавляет объект Taco в Order, который сохраняется в сеансе.
Фактически объект Order остается в сеансе и не сохраняется в базе данных до тех пор, пока пользователь не завершит и не отправит форму заказа. В этот момент OrderController должен вызвать реализацию OrderRepository, чтобы сохранить заказ. Давайте напишем эту реализацию.
ВСТАВКА ДАННЫХ С SempleJdbcInsert
Вы помните, что сохранение тако связано не только с сохранением имени тако и времени создания в таблицу Taco, но и с сохранением ссылки на ингредиенты, связанные с тако, в таблицу Taco_Ingredients. И вы также помните, что это потребовало от вас знать идентификатор Taco, который вы получили с помощью KeyHolder и PreparedStatementCreator.
Когда дело доходит до сохранения заказов, существует аналогичное обстоятельство. Необходимо не только сохранить данные заказа в таблице Taco_Order, но также и ссылки на каждый taco в заказе в таблице Taco_Order_Tacos. Но вместо того, чтобы использовать громоздкий PreparedStatementCreator, позвольте мне представить вам SimpleJdbcInsert, объект, который обертывает JdbcTemplate, чтобы упростить вставку данных в таблицу.
Начнем с создания JDBCOrderRepository, implementation OrderRepository. Но прежде чем писать реализацию метода save(), давайте сосредоточимся на конструкторе, где вы создадите пару экземпляров SimpleJdbcInsert для вставки значений в таблицы Taco_Order и Taco_Order_Tacos. В следующем листинге показан JDBCOrderRepository (без метода save ()).
Листинг 3.13 создание SimpleJdbcInsert из JdbcTemplate
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();
}
...
}
Как и JdbcTacoRepository, JdbcOrderRepository inject-ид JdbcTemplate через его конструктор. Но вместо назначения JdbcTemplate непосредственно переменной экземпляра конструктор использует его для создания нескольких экземпляров SimpleJdbcInsert.
Первый экземпляр, который назначен переменной экземпляра orderInserter, настроен для работы с таблицей Taco_Order и предполагает, что свойство id будет предоставлено или сгенерировано базой данных. Второй экземпляр, назначенный orderTacoInserter, настроен для работы с таблицей Taco_Order_Tacos, но не содержит никаких инструкций о том, как ID будут генерироваться в этой таблице.
Конструктор также создает экземпляр Jackson ObjectMapper и присваивает его переменной экземпляра. Хотя Jackson предназначен для обработки JSON, вы увидите, как мы перепрофилируем его, чтобы помочь вам сохранять заказы и связанные с ними тако.
Теперь давайте посмотрим, как метод save() использует экземпляры SimpleJdbcInsert. В следующем списке показан метод save(), а также несколько частных методов save(), которые делегируются для реальной работы.
Листинг 3.14 использование SimpleJdbcInsert для вставки данных
@Override
public Order save(Order order) {
order.setPlacedAt(new Date());
long orderId = saveOrderDetails(order);
order.setId(orderId);
List
for (Taco taco : tacos) {
saveTacoToOrder(taco, orderId);
}
return order;
}
private long saveOrderDetails(Order order) {
@SuppressWarnings("unchecked")
Map
values.put("placedAt", order.getPlacedAt());
long orderId =orderInserter
.executeAndReturnKey(values)
.longValue();
return orderId;
}
private void saveTacoToOrder(Taco taco, long orderId) {
Map
values.put("tacoOrder", orderId);
values.put("taco", taco.getId());
orderTacoInserter.execute(values);
}
Метод save() ничего не сохраняет. Он определяет поток для сохранения Order и связанных с ним объектов Taco и делегирует работу saveOrderDetails() и saveTacoToOrder().
SimpleJdbcInsert имеет несколько полезных методов для выполнения вставкиt: execute() и executeAndReturnKey(). Оба принимают Map
Такую Map легко создать, скопировав значения из Order в записи Map. Но у Order есть несколько свойств, и все они имеют одинаковое имя со столбцами, в которые они входят. Из-за этого в saveOrderDetails() я решил использовать Jackson ObjectMapper и его метод convertValue() для преобразования Order в Map(Я признаю, что это хакерское использование ObjectMapper, но у вас уже есть Jackson в classpath; Spring Boot’s web starter включает его. Кроме того, использование ObjectMapper для сопоставления объекта с Map намного проще, чем копирование каждого свойства из объекта в Map. Не стесняйтесь заменить использование ObjectMapper любым кодом, который вы предпочитаете, который строит Map, который вы передадите на вставку объектов.). После создания Map задайте для записи placedAt значение свойства placedAt объекта Order. Это необходимо, поскольку в противном случае ObjectMapper преобразует свойство Date в long, что несовместимо с полем placedAt в таблице Taco_Order.
С заполненным Map данными заказов, вы можете вызвать executeAndReturnKey() в orderInserter. Это сохраняет информацию о заказе в таблице Taco_Order и возвращает сгенерированный базой данных ID как объект типа Number, который преобразуется long с помощью вызова longValue(), и возвращается из метода.
Метод saveTacoToOrder() значительно проще. Вместо того чтобы использовать ObjectMapper для преобразования объекта в Map, создается Map и задаются соответствующие значения. Еще раз, ключи Map соответствуют именам столбцов в таблице. Вызов метода orderTacoInserter.execute() выполняет insert.
Теперь вы можете inject OrderRepository в OrderController и начать использовать его. Следующий листинг показывает OrderController, включая изменения для использования inject OrderRepository.
Листинг 3.15 использование OrderRepository в 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;