반응형
도서 'Spring in Action' 제 5판을 보고 책 내용과 그 이외의 부족한 부분을 채워가며 공부한 내용입니다.
키워드 : 스키마, Jdbc, JdbcTemplate, PreparedStatementCreator, SimpleJdbcInsert
1. 스키마 정의하고 데이터 추가하기
<설명>
- Ingredient : 식자재 정보를 저장
- Taco : 사용자가 식자재를 선택하여 생성한 타코 디자인에 관한 정보를 저장
- Taco_Ingredinets : Taco와 Ingredient 테이블 간의 관계, 1(Taco) 대 多(Ingredient) 관계
- Taco_Order : 주문 정보 저장
- Taco_Order_Tacos : Taco_Order과 Taco 테이블 간의 관계, 1(Taco_Order) 대 多(Taco) 관계
<shema.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,
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);
<data.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');
- JdbcTemplate을 사용해서 데이터를 저장하는 방법
- 직접 update() 메서드를 사용
- SimpleJdbcInsert 래퍼(wrapper) 클래스를 사용
1. PreparedStatementCreator 사용
<TacoRepository 인터페이스>
package tacos.data;
import tacos.Taco;
public interface TacoRepository {
Taco save(Taco design);
}
<TacoRepository 구현하기>
...
@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() 메서드
- Taco 테이블에 각 식자재를 저장하는 saveTacoInfo()메서드를 호출
- saveTacoInfo()에 의해 반환된 타코ID를 사용해서 타코와 식자재의 연관 정보를 저장하는 saveIngredientToTaco()를 호출
- saveTacoInfo()
- 실행할 SQL 명령과 각 쿼리 매개변수의 타입을 인자로 전달하여 PreparedStatement Creater Factory 객체를 생성하는 것으로 시작
- 해당 객체의 newPreparedStatementCreater()를 호출하며, 이 때 PreparedStatementCreater를 생성하기 위해 쿼리 매개변수의 값을 인자로 전달
- update()가 PreparedStatementCreater 객체와 KeyHolder 객체를 인자로 받음
- KeyHolder(GeneratedKeyHolder 인스턴스) = 생성된 타코 ID 제공
- keyHolder.getKey().longValue() -> 타코 ID 반환
- saveIngredientToTaco()
- Taco 객체의 List에 저장된 각 Ingredient 객체를 반복 처리
- update()를 사용
- 타코 ID와 Ingredient 객체 참조를 Taco_Ingredient 테이블에 저장
<DesignTacoController>
@Slf4j
@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
private final IngredientRepository ingredientRepo;
private TacoRepository tacoRepo;
@Autowired
public DesignTacoController(IngredientRepository ingredientRepo, TacoRepository tacoRepo) {
this.ingredientRepo = ingredientRepo;
this.tacoRepo = tacoRepo;
}
@ModelAttribute("order")
public Order order() {
return new Order();
}
@ModelAttribute("taco")
public Taco taco() {
return new Taco();
}
@GetMapping
public String showDesignFrom(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));
}
model.addAttribute("taco", new Taco());
return "design";
}
private List<Ingredient> filterByType(
List<Ingredient> ingredients, Type type){
return ingredients
.stream()
.filter(x -> x.getType().equals(type))
.collect(Collectors.toList());
}
@PostMapping
public String processDesign(@Valid Taco design, Errors errors, @ModelAttribute Order order) {
if (errors.hasErrors()) {
return "design";
}
Taco saved = tacoRepo.save(design);
order.addDesign(saved);
return "redirect:/orders/current";
}
}
<코드 분석>
- DesignTacoController 생성자에서 IngredientRepository와 TacoRepository 객체 모두 인자로 받음
- @SessionAttribute("order")
- 주문은 다수의 HTTP 요청에 걸쳐 존재
- 다수의 타코를 생성하고 그것들을 하나의 주문으로 추가할 수 있기 위해
- @SessionAttribute 어노테이션을 주문과 같은 모델 객체에 지정하면 세션에서 계속 보존되면서 다수의 요청에 걸쳐 사용가능
- 주문은 다수의 HTTP 요청에 걸쳐 존재
- @ModelAttribute
- 해당 객체가 모델에 생성되도록 해줌.
- 매개변수에서의 @ModelAttribute
- 해당 매개변수(여기선 order)의 값이 모델로부터 전달되어야 한다는 것
- 스프링 MVC가 해당 매개변수에 요청 매개변수를 바인딩하지 않아야 한다는 것
- processDesign()
- 전달된 데이터의 유효성 검사 후 주입된 TacoRepository를 사용해 타코를 저장
- 세션에 보존된 Order에 Taco 객체를 추가
2. SimpleJdbcInsert 사용하기
<OrderRepository 인터페이스>
package tacos.data;
import tacos.Order;
public interface OrderRepository {
Order save(Order order);
}
해야할 것
- 주문 데이터를 Taco_Order 테이블에 저장
- 해당 주문의 각 타코에 대한 id도 Taco_Order_Tacos 테이블에 저장
<OrderRepository 구현하기>
...
@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;
}
private long saveOrderDetails(Order order) {
@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);
}
}
<코드 분석>
- JdbcOrderRepository에서도 생성자를 통해 JdbcTemplate이 주입
- 이 때 인스턴스 변수에 직접 지정하는 대신, JdbcOrderRepository생성자에서 JdbcTemplate을 사용해서 2 개의 SimpleJdbcInsert 인스턴스를 생성
- orderInserter
- Taco_Order 테이블에 주문 데이터를 추가하기 위해 구성
- Order객체의 id속성값은 데이터베이스가 생성해 주는 것을 사용
- orderTacoInserter
- Taco_Order_Tacos 테이블에 해당 주문 id 및 이것과 연관된 타코들의 id를 추가하기 위해 구성
- 어떤 id값들을 Taco_Order_Tacos테이블의 데이터에 생성할 것인지는 지정하지 않음
- 이미 생성된 주문 id 및 이것과 연관된 타코들의 id를 우리가 저장하기 때문
- objectMapper
- 기존에는 JSON을 처리하기 위한 것이지만
- orderInserter
- 이 때 인스턴스 변수에 직접 지정하는 대신, JdbcOrderRepository생성자에서 JdbcTemplate을 사용해서 2 개의 SimpleJdbcInsert 인스턴스를 생성
- save()
- 실제로 저장 x
- Order 및 이것과 연관된 Taco 객체들을 저장하는 처리를 총괄
- 실제 저장은 saveOrderDetails(), saveTacoToOrder() 메서드를 호출해 저장
- SimpleJdbcInsert
- 데이터 추가 메서드 2개
- 둘 다 Map<String, Object>를 인자로 받음
- 해당 Map의 key는 데이터가 추가되는 테이블의 열 이름과 대응, Map의 값은 해당열에 추가되는 값
- execute()
- executeAndReturnKey()
- objectMapper.convertValue(order, Map.class)
- Order를 Map으로 변환한 것
- Map이 생성되면 key가 placedAt인 항목의 값을 Order객체의 placedAt 속성 값으로 변경
- orderInserter.executeAndReturnKey(values).longValue();
- 해당 주문 데이터가 orderInserter(Taco_Order) 테이블에 저장된 후 데이터베이스에서 생성된 ID가 Number객체로 반환
- 연속으로 longValue()를 호출하여 saveOrderDetails() 메서드에서 반환하는 long 타입으로 변환 가능
- saveTacoToOrder()
- 객체를 Map으로 변환하기 위해 ObjectMapper를 사용하는 대신 우리가 Map을 생성하고 항목에 적합한 값을 설정
- Map의 키는 테이블의 열 이름과 동일
<OrderController>
...
@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();
return "redirect:/";
}
}
OrderRepository를 OrderController에 주입
<코드 분석>
- processOrder
- 주입된 OrderRepository의 save()메서드를 통해서 제출된 Order 객체를 저장(Order 객체도 세션에 보존될 필요가 있음)
- SessionStatus를 인자로 받아 이것의 setComplete()를 호출해 세션을 재설정
<IngredientByIdConverter>
...
@Component
public class IngredientByIdConverter
implements Converter<String, Ingredient> {
private IngredientRepository ingredientRepo;
@Autowired
public IngredientByIdConverter(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
@Override
public Ingredient convert(String id) {
return ingredientRepo.findById(id);
}
}
- 데이터 타입 변환이 목적
- Converter 인터페이스의 convert() 메서드를 구현(String타입의 식자재 ID를 사용해서 데이터베이스에 저장된 특정 식자재 데이터를 읽은 후 Ingredient 객체로 변환하기 위해 컨버터가 사용)
- 해당 컨버터로 변환된 Ingredient 객체는 다른곳에서 List로 저장
<코드 분석>
- @Component
- 스프링에 의해 자동 생성 및 주입되는 빈으로 생성
- @Autowired
- 생성자에 Autowired 지정
- IngredientRepository 인터페이스를 구현한 빈(JdbcIngredientRepository) 인스턴스가 생성자의 인자로 주입)
- Converter<String, Ingredient>에서 String은 변환할 값의 타입, Ingredient는 변환된 값의 타입
- 생성자에 Autowired 지정
- convert()
- JdbcIngredientRepository 클래스 인스턴스의 findById() 메서드 호출
- findById()메서드에서 mapRowToIngredient() 메서드를 호출하여 결과 세트의 행 데이터를 속성 값으로 갖는 Ingredient 객체를 생성하고 반환
참고 자료
1. 크레이그 월즈, Spring in Action, Fifth Edition(출판지 : 제이펍, 2020)
반응형
'Spring' 카테고리의 다른 글
JDBC - 기본 구조와 API (0) | 2022.05.20 |
---|---|
Spring 데이터로 작업하기 3 (0) | 2022.05.18 |
Spring 데이터로 작업하기 1 (0) | 2022.05.18 |
Spring 웹 애플리케이션 개발 3 (0) | 2022.05.11 |
Spring 웹 애플리케이션 개발 2 (0) | 2022.05.11 |