관련 내용
<JPA 영속성 컨텍스트부터 다양한 매핑까지 완전히 이해하기>
- JPA 기본 동작과 영속성 원리 이해하기
- JPA 엔티티 매핑 - 테이블, 컬럼, 기본키
- JPA 연관 관계 매핑과 고급 매핑
- JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기
JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기
- JPA 값 타입 이해하기
- JPQL, fetch join 중심으로 이해하기
JPA JPQL, fetch join 중심으로 이해하기
개요 목적
이번 시간에는 JDBC로 작성된 배달 서비스 (음식 정보를 DB 테이블에 추가 하는 과정) 코드를
JPA 기술을 사용해서 코드를 변경해보겠다.
JPA를 활용하면 더 간편한 코드로 데이터를 CRUD 할 수 있고, 트랜잭션을 관리 할 수 있다.
하지만 JPA를 편리하게 사용하려면 JPA 관련 기초 지식을 많이 알아야 한다.
JPA의 전반적인 이해를 얻고 싶다면, 위에 관련 내용 JPA 시리즈를 보면 좋다.
변경된 Repository 코드 (쿼리를 날리는 방식의 차이)
음식을 등록하기
- JDBC
//connection을 직접 주입받아서
public void add(Connection connection, CompanyFoodDto companyFoodDto) throws SQLException {
//SQL Query 직접 작성해서 보내는 방식으로 음식을 등록한다.
String sql = "INSERT INTO company_food "
+ "(member_id, name, price) VALUES(?,?,?)";
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setLong(1, companyFoodDto.getMemberId());
preparedStatement.setString(2, companyFoodDto.getName());
preparedStatement.setBigDecimal(3, companyFoodDto.getPrice());
preparedStatement.executeUpdate();
}
}
- JPA
간단한 쿼리의 경우(CRUD) JPA는 영속성 컨텍스트를 사용해서 훨씬 간단한 코드로 동작을 수행할 수 있다.
//엔티티 매니저를 주입받고
public void add(EntityManager em, CompanyFoodDto dto, BigDecimal price) {
CompanyFoodEntity companyFoodEntity = new CompanyFoodEntity();
companyFoodEntity.setMemberId(dto.memberId);
companyFoodEntity.setName(dto.name);
companyFoodEntity.setPrice(dto.price)
//직접 쿼리를 작성하는게 아니라
//엔티티 매니저를 사용해 영속성 컨텍스트에 엔티티를 추가한다.
em.persist(companyFoodPriceDto);
}
중복된 음식 찾기
- JDBC
public Optional<CompanyFoodEntity> findByNameAndMemberId(Connection connection, Long memberId
, String findName) throws SQLException {
//SQL Query 형식에 맞게, select where 조건 절을 입력
String sql = "SELECT * FROM company_food WHERE member_id=? AND name=?";
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setLong(1, memberId);
preparedStatement.setString(2, findName);
//ResultSet을 값이 존재하면 CompanyFoodEntity 값을 돌려 받는다.
try (ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
CompanyFoodEntity companyFoodEntity = getCompanyFoodEntity(resultSet);
return Optional.ofNullable(companyFoodEntity);
}
}
}
return Optional.empty();
}
- JPA
복잡한 select의 경우 JPA는 JPQL을 사용하여, 쿼리를 구성하고 결과 값을 받을 수 있다.
public Optional<CompanyFoodEntity> findByNameAndMemberId(EntityManager em, Long memberId,
String findName) {
//JPA의 JPQL을 사용해서 select where 절 작성
String jpql = "SELECT f FROM CompanyFoodEntity f WHERE f.name=:name AND f.memberId=:memberId";
try {
//createQuery로 값지 존재한다면, CompanyFoodEntity 값을 돌려 받는다.
CompanyFoodEntity findCompanyFood = em.createQuery(jpql, CompanyFoodEntity.class)
.setParameter("name", findName).setParameter("memberId", memberId)
.getSingleResult();
return Optional.ofNullable(findCompanyFood);
//getSiggleResult로 값이 존재 하지 않는 예외가 발생하면
//empty 조건에 만족하는 값 없음을 반환한다.
} catch (NoResultException e) {
log.info("ex", e);
return Optional.empty();
}
}
변경된 서비스 코드(트랜잭션 적용 방식의 차이)
- JDBC
@Service
@RequiredArgsConstructor
public class CompanyFoodService {
private final CompanyFoodRepository companyFoodRepository;
public void addFood(String memberId, String foodName, String price)
throws SQLException {
//JDBC는 트랜잭션을 시작할 때 서비스 코드에서 Connection을 획득
Connection connection = companyFoodRepository.connectJdbc();
CompanyFoodDto companyFoodDto = new CompanyFoodDto
(null, Long.valueOf(memberId), foodName, new BigDecimal(price));
try {
//setAutoCommit(false)로 트랜잭션 시작을 알린다.
connection.setAutoCommit(false);
//repository 코드들은 connection을 서비스 코드에서 받아서
//일관된 트랜잭션=세션=커넥션을 유지한다.
validateDuplicateFoodName(connection, memberId, foodName);
companyFoodRepository.add(connection, companyFoodDto);
//모든 레포지토리가 성공적으로 동작하면 커밋한다.
connection.commit();
} catch (DuplicatedFoodNameException e) {
connection.rollback();
throw new DuplicatedFoodNameException();
} finally {
if (connection != null) {
connection.close();
}
}
}
private void validateDuplicateFoodName(Connection connection, Long memberId, String foodName)
throws SQLException {
companyFoodRepository.findByNameAndMemberId(connection, memberId, foodName)
.ifPresent(m -> {
throw new DuplicatedFoodNameException();
});
}
- JPA
JPA는 트랜잭션을 유지할 대 Connection을 직접 전달하는 방식을 사용하지 않는다.
EntityManger가 트랜잭션 동기화 매니저에 Connection을 보관하여, EntityManager를 전달하는 방식으로 트랜잭션을 유지할 수 있다.
@Service
@RequiredArgsConstructor
public class CompanyFoodService {
@Value("${persistenceName:@null}")
private String persistenceName;
private final CompanyFoodRepository companyFoodRepository;
public void addFood(Long memberId, String foodName, BigDecimal price) throws Exception {
CompanyFoodDto companyFoodDto = new CompanyFoodDto
(null, memberId, foodName, new Timestamp(System.currentTimeMillis());
//persistance data를 주입 받은(driver, user, password, url)
//엔티티매니저 팩토리를 생성한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory(persistenceName);
//팩토리에서 하나의 쓰레드에서 유지할 엔티티 매니저를 생성한다.
EntityManager em = emf.createEntityManager();
//엔티티 매니저.getTrasaction()으로 트랜잭션을 생성
EntityTransaction tx = em.getTransaction();
try {
//트랜잭션을 시작한다.
tx.begin();
//JDBC와 다르게 connection이 아닌 EntityManager를 주입 받아서
//트랜잭션을 유지 한다.
validateDuplicateFoodName(em, memberId, name);
companyFoodRepository.add(em, companyFoodDto, price);
tx.commit();
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
em.close();
emf.close();
}
}
private void validateDuplicateFoodName(EntityManager em, Long memberId, String foodName) {
companyFoodRepository.findByNameAndMemberId(em, memberId, foodName)
.ifPresent(m -> {
throw new DuplicatedFoodNameException();
});
}
}
'Web Sever 개발과 CS 기초 > 스프링' 카테고리의 다른 글
Querydsl 이해와 사용법 (0) | 2023.04.28 |
---|---|
Spring Data JPA 이해와 사용법 (0) | 2023.04.28 |
JPA JPQL, fetch join 중심으로 이해하기 (0) | 2023.04.28 |
JPA 값 타입 이해하기 (0) | 2023.04.28 |
JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기 (0) | 2023.04.28 |