이 글은 크레이그 윌즈의 "스프링 인 액션(5판)"을 읽고 간략히 정리한 글이다.
데이터를 저장하고 관리하는 법
JDBC
JDBC 의존성을 추가한 후, 인터페이스와 구현 클래스를 둔다.
public interface IngredientRepository {
Iterable<Ingredient> findAll();
Ingredient findById(String id);
Ingredient save(Ingredient ingredient);
}
@Repository
public class JdbcIngredientRepository implements IngredientRepository {
private JdbcTemplate jdbc;
@Autowired
public JdbcIngredientRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
}
이때 @Repository는 스프링이 정의하는 애노테이션이다. 자동으로 찾아서 빈으로 생성해준다.
그리고 @Autowired 애노테이션을 통해 자동으로 JdbcTemplate을 주입한다.
JDBC로 쿼리 실행
@Override
public Iterable<Ingredient> findAll() {
return jdbc.query("select id, name, type from Ingredient",
this::mapRowToIngredient);
}
@Override
public Ingredient findById(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")));
}
- jdbc의 query 메서드를 사용해 쿼리를 실행한다. List 객체를 반환한다.
- 첫번째 인자는 query문, 두번째 인자는 RowMapper 메서드, 세번째 인자는 SQL문의 파라미터다.
- 스프링 RowMapper 인터페이스를 구현한 mapRowToIngredient 메서드를 사용해, 실행 결과를 Ingredient 객체로 변환한다. 해단 함수는 ResultSet의 row 개수만큼 호출된다.
- queryForObject는 하나의 객체만 반환할 때 사용한다.
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;
}
- update 메서드를 사용해 데이터를 추가한다.
스키마 정의 & 데이터 추가
schema.sql이라는 이름의 파일을 classpath 루트 경로에 두면 애플리케이션이 시작될 때, 해당 파일의 SQL이 사용 중인 데이터베이스에서 자동 실행된다. (src/main/resources/schema.sql)
JdbcTemplate를 사용해서 데이터를 저장하는 방법
- 직접 update() 메서드 사용
- SimpleJdbcInsert Wrapper 클래스 사용
update 메서드
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의 역할은 insert할 taco id=key를 생성하는 것이다. 이를 위해 PreparedStatementCreator와 KeyHolder를 사용한다.
- 이후에는 for문을 사용해 taco의 각 ingredient를 저장한다. (saveIngredientToTaco 반복 호출)
@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(Taco design, Errors errors, @ModelAttribute Order order) {
if (errors.hasErrors()) {
return "design";
}
Taco saved = tacoRepo.save(design);
order.addDesign(saved);
return "redirect:/orders/current";
}
}
- @SessionAttributes 애노테이션은 세션에서 해당 애트리뷰트를 유지할 수 있도록 해준다. 여기서는 order 객체에 사용되어 주문이 세션에서 계속 보존되도록 해준다. (여러 타코를 생성하고 이를 주문에 추가하기 위해서)
- @ModelAttribute 애노테이션은 Model에 해당 객체가 생성되도록 해준다. (아니면 setAttributes로 추가해주어야 한다.)
SimpleJdbcInsert
SimpleJdbcInsert: 데이터를 더 쉽게 테이블에 추가하기 위해 JdbcTemplate을 래핑한 객체
@Repository
public class JdbcOrderRepository implements OrderRepository {
private SimpleJdbcInsert orderInserter;
private SimpleJdbcInsert orderTacoInserter;
private ObjectMapper objectMapper;
@Autowired
public JdbcOrderRepository(JdbcTemplate jdbc) {
orderInserter = new SimpleJdbcInsert(jdbc)
.withTableName("Taco_Order")
.usingGeneratedKeyColumns("id");
orderTacoInserter = new SimpleJdbcInsert(jdbc)
.withTableName("Taco_Order_Tacos");
objectMapper = new ObjectMapper();
}
..................
private long saveOrderDetails(Order order) {
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);
}
}
- 각 테이블별로 SimpleJdbcInsert를 생성한다.
- SimpleJdbcInsert는 execute(), executeAndReturnKey() 메서드를 사용해 데이터를 추가한다.
- Map을 인자로 받아 Map의 키에 해당하는 column에 데이터를 추가한다.
변경된 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(Model model) {
model.addAttribute("order", new Order());
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:/";
}
}
- @SessionAttributes가 추가되었다. 이 애노테이션에 설정한 이름에 해당하는 모델 정보를 자동으로 세션에 넣어준다.
- processOrder 메서드에서 OrderRepository의 save 메서드를 통해 데이터를 저장하고, SessionStatus.setComplete()를 통해 세션을 재설정한다. 완료 후에도 정보를 계속 세션에 저장해둘 필요는 없으니 이를 제거하는 것.
데이터의 타입을 바꿔주는 컨버터 클래스
애플리케이션에서 DB에 있는 데이터를 읽은 후 모델 클래스로 변환하기 위해 컨버터가 사용된다.
@Component
public class IngredientByIdConverter implements Converter<String, Ingredient> {
private IngredientRepository ingredientRepo;
....................
@Override
public Ingredient convert(String id) {
return ingredientRepo.findById(id);
}
}
- (여기서는) 식자재 id를 가지고 엔티티 타입으로 변경해주는 메서드를 제공한다.
예시코드 (다른곳에서 가져온 예시)
@GetMapping("/post/{id}")
public String getPost(@PathVariable("id") Post post) {
return post.getTitle();
}
- 이런식으로 id를 자동으로 엔티티로 바꿔주는 것이다.
중간에 에러 하나.
책에서는 애플리케이션 실행 후 localhost:8080/h2-console에 접속해서 jdbc:h2:mem:testdb로 접속하라 하는데, 접속하면 생성된 테이블이 없음. 애플리케이션 실행 로그를 보면 이렇게 접속 URL이 나와있는데 해당 URL을 입력해야 함.
H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:0fa68783-8131-40f9-b787-080789632f88'
스프링 데이터 JPA
스프링 데이터 프로젝트 종류
- 스프링 데이터 JPA : 관계형 데이터베이스 JPA 퍼시스턴스
- 스프링 데이터 MongoDB : 몽고 디비 ""
- 스프링 데이터 Neo4 : Neo4j 그래프 데이터베이스 ""
- 스프링 데이터 Redis : 레디스 키-값 스토어 ""
- 스프링 데이터 Cassandra : 카산드라 데이터베이스 ""
스프링 데이터에서는 Repository 인터페이스를 기반으로 해당 인터페이스를 구현하는 Repository를 자동으로 생성해준다.
도메인 객체에 애노테이션 추가
@Data
@RequiredArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Ingredient {
@Id
private final String id;
- JPA 매칭 애노테이션을 추가한다. (@Entity)
- id 속성에 @Id를 지정한다.
- JPA에서는 인자가 없는 생성자를 가져야 한다. (@NoArgsConstructor 추가. public이나 protected여야 한다.)
JPA Repository 선언
스프링 데이터에서는 CrudRepository 인터페이스를 확장할 수 있다.
public interface IngredientRepository extends CrudRepository<Ingredient, String> { }
이렇게만 선언하면 된다. 구현 클래스를 작성하지 않아도 자동으로 JPA가 생성해준다.
다만 JPA에서는 위에서처럼 data.sql을 통해 데이터를 넣을 수 없으므로 따로 넣어줘야 한다.
'공부 > Spring' 카테고리의 다른 글
[스프링 인 액션] Chapter 5 구성 속성 사용하기 (0) | 2022.07.03 |
---|---|
[스프링 인 액션] Chapter 4 스프링 시큐리티 (0) | 2022.06.16 |
[스프링 인 액션] Chapter 2 웹 어플리케이션 개발하기 (0) | 2022.04.18 |
[스프링 인 액션] Chapter 1 스프링 시작하기 (0) | 2022.04.08 |
[스프링 프레임워크 Core] 스프링 IoC 컨테이너와 빈 (0) | 2020.06.06 |