반응형
3.1 JDBC를 사용해서 데이터 읽고 쓰기
- 스프링의 JDBC 지원은 JdbcTemplate 클래스에 기반을 둠.
- JDBCTemplate은 JDBC를 사용할 때 요구되는 모든 형식적이고 상투적인 코드없이 개발자가 관계형 데이터베이스에 대한 SQL 연산을 수행할 수 있는 방법을 제공.
- JDBCTemplate을 사용하지 않고 데이터베이스 쿼리하면 데이터베이스 Connection 생성, 명령문 생성, 연결과 명령문 및 결과 세트를 닫고 클린업 하는 코드들로 쿼리 코드가 둘러싸여 복잡.
JDBCTemplate 및 H2 데이터베이스 의존성 추가
- 우리 프로젝트의 classpath에 추가 => 스프링 부트의 JDBC starter의 의존성을 빌드 명세에 추가하면 간단.
<properties>
...
<h2.version>1.4.196</h2.version>
</properties>
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
...
</dependencies>
Repository 인터페이스 정의하기
public interface IngredientRepository {
Iterable<Ingredient> findAll();
Ingredient findById(String id);
Ingredient save(Ingredient ingredient);
}
JDBCTemplate을 사용해서 데이터베이스 쿼리하기.
@Repository
public class JdbcTacoRepository implements TacoRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTacoRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@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();
jdbcTemplate.update(psc, keyHolder);
return keyHolder.getKey().longValue();
}
private void saveIngredientToTaco(Ingredient ingredient, long tacoId) {
jdbcTemplate.update("insert into Taco_Ingredients (taco, ingredient) values (?, ?)", tacoId, ingredient.getId());
}
}
*.sql
- 애플리케이션 classpath의 루트 경로에 있으면 애플리케이션이 시작될때 *.sql 파일의 SQL이 사용중인 데이터베이스에서 자동으로 실행된다.
- src/main/resources
JdbcTemplate을 이용해서 id 및 createdAt 저장
@Repository // 스프링 컴포넌트 검색에서 이 클래스를 자동으로 찾아서 스프링 애플리케이션 컨텍스트의 빈으로 생성해 줌.
public class JdbcIngredientRepository implements IngredientRepository {
private JdbcTemplate jdbcTemplate;
@Autowired // JdbcIngredientRepository 빈이 생성되면, 스프링이 해당 빈을 JDBCTemplate에 주입한다. (사실 @Autowired 안해줘도 되지만 책 따라함)
public JdbcIngredientRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public Iterable<Ingredient> findAll() {
return jdbcTemplate.query("select id, name, type from Ingredient", this::mapRowToIngredient);
}
private Ingredient mapRowToIngredient(ResultSet resultSet, int rowNum) throws SQLException {
return new Ingredient(resultSet.getString("id"), resultSet.getString("name"), Ingredient.Type.valueOf(resultSet.getString("type")));
}
@Override
public Ingredient findById(String id) {
return jdbcTemplate.queryForObject("select id, name, type from Ingredient where id=?", this::mapRowToIngredient, id);
}
@Override
public Ingredient save(Ingredient ingredient) {
jdbcTemplate.update("insert into Ingredient (id, name, type) values (?, ?, ?)", ingredient.getId(), ingredient.getName(), ingredient.getType().toString());
return ingredient;
}
}
SimpleJdbcInsert
- 주문 데이터를 Taco_Order 테이블에 저장하는 것은 물론이고, 해당 주문의 각 타코에 대한 id도 Taco_Order_Tacos 테이블에 저장해야 함.
- 그러나 이 경우는 복잡한 PreparedStatementCreator 대신 SimpleJdbcInesrt를 사용할 수 있음.
- SimpleJdbcInsert는 데이터를 더 쉽게 테이블에 추가하기 위해 JdbcTemplate을 래핑한 객체.
@Repository
public class JdbcOrderRepository implements OrderRepository {
private final SimpleJdbcInsert orderInserter; // Taco_Order 테이블에 주문 데이터를 추가하기 위해 구성, 이떄 Order 객체의 id 속성 값은 데이터베이스가 생성해 주는 것을 사용.
private final SimpleJdbcInsert orderTacoInserters; // Taco_Order_Tacos 테이블에 해당 주문 id 및 이것과 연관된 타코들의 id를 추가하기 위해 구성.
private final ObjectMapper objectMapper;
public JdbcOrderRepository(JdbcTemplate jdbcTemplate) {
this.orderInserter = new SimpleJdbcInsert(jdbcTemplate)
.withTableName("Taco_Order")
.usingGeneratedKeyColumns("id");
this.orderTacoInserters = new SimpleJdbcInsert(jdbcTemplate)
.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);
return orderInserter.executeAndReturnKey(values)
.longValue();
}
private void saveTacoToOrder(Taco taco, long orderId) {
Map<String, Object> values = new HashMap<>();
values.put("tacoOrder", orderId);
values.put("taco", taco.getId());
orderTacoInserters.execute(values);
}
}
Converter
@Component // 스프링에 의해 자동 생성 및 주입되는 빈으로 생성.
public class IngredientByIdConverter implements Converter<String, Ingredient> {
private final IngredientRepository ingredientRepository;
public IngredientByIdConverter(IngredientRepository ingredientRepository) {
this.ingredientRepository = ingredientRepository;
}
@Override
public Ingredient convert(String id) {
return ingredientRepository.findById(id);
}
}
- 데이터의 타입을 변환해주는 컨버터.
- 우리가 Converter에 지정한 타입 변환이 필요할때 convert() 메소드가 자동 호출됨.
- 우리 애플리케이션에서는 String 타입의 식자재 ID를 사용해서 데이터베이스에 저장된 특정 식자제 데이터를 읽은 후 Ingredient 객체로 변환하기 위해서 컨버터가 사용.
3.2 스프링 데이터 JPA를 사용해서 데이터 저장하고 사용하기
- 스프링 데이터 프로젝트는 여러 개의 하위 프로젝트로 구성되는 다수 규모가 큰 프로젝트.
- 대부분의 하위 프로젝트는 다양한 데이터베이스 유형을 사용한 데이터 퍼세스턴스에 초점을 둠.
- 스프링 데이터 JPA: 관계형 데이터베이스의 JPA Persistence
- 스프링 데이터 MongoDB: 몽고 문서형 데이터베이스의 Persistence
- 스프링 데이터 Neo4: Neo4j 그래프 데이터베이스의 Persistence
- 스프링 데이터 Redis: Redis 키-값 스토어의 Persistence
- 스프링 데이터 Cassandra: 카산드ㅏ 데이터베이스의 Persistence
- 스프링 데이터에서는 Repository 인터페이스를 기반으로 이 인터페이스를 구현하는 Repository를 자동 생성해준다.
스프링 데이터 JPA를 프로젝트에 추가하기
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- JPA 스타터를 통해서 스프링 부트 애플리케이션에서 사용할 수 있다.
- 이 스타터 의존성에는 스프링 데이터 JPA는 물론이고, JPA를 구현한 Hibernate까지도 포함된다.
만일 다른 JPA 구현 라이브러리 (EclipseLink)를 사용한다면 아래와 같이 의존성 추가.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>hibernate-entitymanager</groupId>
<artifactId>org.hibernate</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.5.2</version>
</dependency>
도메인 객체에 애노테이션 추가하기
@Data // 사실 안쓰는걸 추천..
@RequiredArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED, force = true)
// JPA에서는 기본생성자를 가져야 한다 + 초기화가 필요한 final 속성이 있으므로 force 속성을 true => Lombok이 자동 생성한 생성자에서 그 속성들을 null로 설정.
@Entity // JPA 매핑 어노테이션을 우리 도메인 객체에 추가해야 한다.
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
}
}
JPA Repository 선언하기
public interface IngredientRepository extends CrudRepository<Ingredient, String> {
}
- 애플리케이션이 시작될 때, 스프링 데이터 JPA가 각 인터페이스 구현체를 자동으로 생성해줌.
JPA Repository 커스터마이징하기.
List<Order> findByDeliveryZip(String deliveryZip);
본질적으로 스프링 데이터는 일종의 DSL을 정의하고 있어서 퍼시스턴스에 관한 내용이 리퍼지터리 메소드의 시그니처에 표현된다.
반응형
'Application > Spring Framework' 카테고리의 다른 글
[스프링 인 액션 정리] 6장, 7장. REST 서비스 생성 & 사용하기 (0) | 2021.01.07 |
---|---|
[스프링 인 액션 정리] 5장. 구성 속성 사용하기 (0) | 2021.01.07 |
[스프링 인 액션 정리] 4장. 스프링 시큐리티 (0) | 2021.01.06 |
[스프링 인 액션 정리] 1. 스프링 시작 (0) | 2021.01.04 |
[Spring] 생성자 주입을 사용해야 하는 이유 (0) | 2020.10.25 |