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);
Большой вопрос заключается в том, куда поместить это определение схемы. Как оказалось, Spring Boot содержит ответ на этот вопрос.
Если есть файл с именем schema.sql в корне classpath приложения, а затем SQL в этом файле будет выполняться в базе данных при запуске приложения. Поэтому содержимое листинга 3.8 следует поместить в проект в виде файла с именем schema.sql в папке src/main/resources.
Вам также необходимо предварительно загрузить базу данных с данными по ингредиентам. К счастью, Spring Boot также выполнит файл с именем data.sql из корня classpath при запуске приложения. Таким образом, вы можете заполнить базу данных данными ингредиентов, используя инструкции insert в следующем листинге, размещенном в src/main/resources/data.sql.
Листинг 3.9 Предзаполнение БД
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');
Несмотря на то, что вы только разработали хранилище для данных ингредиентов, вы можете запустить приложение Taco Cloud на этом этапе и посетить страницу проектирования, чтобы увидеть JdbcIngredientRepository в действии. Ну же… попробуйте. Когда вы вернетесь к чтению, вы напишете репозитории для сохранения Taco, Order и data.
3.1.4 Вставка данных
Вы уже имели представление о том, как использовать JdbcTemplate для записи данных в базу данных. Метод save() в JdbcIngredientRepository использовал метод update() JdbcTemplate для сохранения объектов ингредиентов в базе данных.
Хотя это был хороший первый пример, возможно, это было слишком просто. Как вы скоро увидите, сохранение данных может быть более сложным, чем то, что необходимо JdbcIngredientRepository. Два способа сохранения данных с помощью JdbcTemplate включают следующее:
Напрямую, используя метод update()
С помощью класса-оболочки SimpleJdbcInsert
Давайте сначала посмотрим, как использовать метод update(), когда задача сложнее, чем та, что была при сохранение Ingredient.
СОХРАНЕНИЕ ДАННЫХ С ПОМОЩЬЮ JDBCTEMPLATE
На данный момент единственное, что нужно сделать в репозиториях taco и order, это сохранение их соответствующие объекты. Чтобы сохранить объекты Taco, TacoRepository объявляет метод save():
package tacos.data;
import tacos.Taco;
public interface TacoRepository {
Taco save(Taco design);
}
Аналогичным образом OrderRepository также объявляет метод save:
package tacos.data;
import tacos.Order;
public interface OrderRepository {
Order save(Order order);
}
Кажется достаточно простым, верно? Не так быстро. Сохранение состава taco требует, чтобы вы также сохранили ингредиенты, связанные с этим taco в таблицу Taco_Ingredients. Точно так же сохранение заказа требует, чтобы вы также сохранили tacos, привязанные к заказу в таблице Taco_Order_Tacos. Это делает сохранение тако и заказов немного более сложным, чем то, что требовалось для сохранения ингредиента.
Для реализации TacoRepository необходим метод save(), который начинается с сохранения основных деталей состава taco (например, имени и времени создания), а затем вставляет одну строку в Taco_Ingredients для каждого ингредиента в объекте Taco. Ниже приведен полный класс JdbcTacoRepository.
Листинг 3.10 реализация TacoRepository с JdbcTemplate
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()
);
}
}
Как вы можете видеть, метод save() начинается с вызова private метода saveTacoInfo(), а затем использует ID taco, возвращенный из этого метода, для вызова saveIngredientToTaco(), который сохраняет каждый ингредиент. Дьявол кроется в деталях saveTacoInfo().
Когда вы вставляете строку в Taco, вам нужно знать идентификатор, сгенерированный базой данных, чтобы вы могли ссылаться на него в каждом из ингредиентов. Метод update(), используемый при сохранении данных ингредиента, не помогает вам получить сгенерированный идентификатор, поэтому вам нужен другой метод update().
Метод update () принимает PreparedStatementCreator и KeyHolder. Это KeyHolder, который предоставит сгенерированный идентификатор taco. Но чтобы использовать его, необходимо также создать PreparedStatementCreator.
Как видно из листинга 3.10, создание PreparedStatementCreator нетривиально. Начните с создания PreparedStatementCreatorFactory, предоставив ему SQL, который вы хотите выполнить, а также типы каждого параметра запроса. Затем вызовите newPreparedStatementCreator() на этой фабрике, передав значения, необходимые в параметрах запроса для создания PreparedStatementCreator.
С PreparedStatementCreator на руках, вы можете вызвать update(), передав в PreparedStatementCreator и KeyHolder (в этом случае экземпляр GeneratedKeyHolder). Как только update() закончено, можно возвратить ID taco, возвращая keyHolder.getKey().longValue().
Возвращаясь к save(), в цикле для каждого каждый Ingredient в Taco, вызывается saveIngredientToTaco(). В saveIngredientToTaco() методе используется простая форма update() для сохранения списка ингредиентв таблицу Taco_Ingredients.
Все, что осталось сделать с TacoRepository, - это внедрить его в DesignTacoController и использовать его при сохранении Taco. В следующем листинге показаны изменения, необходимые для внедрения репозитория.
Листинг 3.11 Внедрение и использование TacoRepository
@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;
}
…
}
Как вы можете видеть, конструктор принимает как IngredientRepository, так и TacoRepository. Он назначает переменные экземпляра, чтобы их можно было использовать в методах showDesignForm() и processDesign().