🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [51]일차
🚀51일차에는 Criteria API에 대해 배워보고, hr 데이터베이스를 통해 E-R Diagram 기반으로 엔티티를 설계하는 것을
실습할 수 있었다.
학습 목표 : ERD를 보고 엔티티를 설계할 수 있는 것과 관계매핑을 할 수 있음
학습 과정 : 회고를 통해 작성
Criteria API
- Java Persistence API의 일부로써 복잡한 검색 기능을 구현할때
- SQL이나 JPQL 문자열을 직접 작성하지 않고도 동적으로 쿼리를 생성하고 실행할 수 있게 해줌
- 따라서 개발자가 프로그램 코드 내에서 SQL과 유사한 연산을 수행할 수 있게하는 객체지향 API를 제공
구성요소
- 타입 안전성 : 컴파일 시점에 쿼리의 구문 오류를 잡아낼 수 있어서 실행 시간의 오류가능성을 줄여줌
- 동적쿼리생성 : 실행 시간에 쿼리 구성요소를 결정하여 사용자 입력에 따른 쿼리 변경이 유용
- 자바 코드 기반 구성
- CriteriaBuilder : 쿼리를 생성, 수정에 사용하는 팩토리 클래스
- CriteriaQuery : 생성할 쿼리를 정의, 반환할 엔티티 타입이나 WHERE, GROUP BY, ORDER BY 쿼리 조건 지정 가능
- Root : 쿼리의 FROM절에 해당하는 주 엔티티를 지정, Root 객체를 사용하여 쿼리의 기준이되는 엔티티 정의
▶️실습 - Criteria : User 엔티티 생성
- UserRepository 생성 (JpaRepository와 UserRepositoryCustom 을 모두 상속)
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom{ ... }
- UserRepositoryCustom 생성
public interface UserRepositoryCustom {
List<User> findUsersByName(String name);
}
- UserRepositoryCustomImpl 생성
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
@Autowired
private EntityManager entityManager;
@Override
public List<User> findUsersByName(String name) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> user = query.from(User.class);
// query.select(user).where(cb.equal(user.get("name"), name));
query.select(user).where(cb.like(user.get("name"), "%"+name+"%"));
return entityManager.createQuery(query).getResultList();
}
}
- 실제로 UserRepositoryCustom의 내부에서 수행할 findUsersByName()메소드 오버라이딩
- UserRepositoryCustomImpl 클래스는 UserRepositoryCustom 인터페이스를 구현
즉, 이 클래스는 사용자 정의 리포지토리 기능을 제공 - @Autowired private EntityManager entityManager;
➡️JPA에서 데이터베이스 작업을 수행하는 데 사용
@Autowired 어노테이션을 통해 Spring이 이 객체를 자동으로 주입 - CriteriaBuilder : JPA의 Criteria API를 사용하여 동적 쿼리를 생성하는 데 사용
- CriteriaQuery<User> query = cb.createQuery(User.class);
➡️CriteriaQuery 객체를 생성하여 User 엔티티에 대한 쿼리를 작성할 준비 - Root<User> user = query.from(User.class);
Root 객체는 쿼리의 시작점을 정의. ➡️User 엔티티를 기준으로 쿼리를 작성 - query.select(user).where(cb.like(user.get("name"), "%"+name+"%"));
➡️select 메서드를 사용하여 user 엔티티를 선택
where 메서드를 통해 이름이 주어진 name을 포함하는 사용자들을 찾는 조건을 설정 - 작성한 쿼리를 실행하고, 그 결과로 얻은 사용자 목록을 반환
main 테스트
@SpringBootApplication
public class Application implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Autowired
UserRepository userRepository;
@Override
public void run(String... args) throws Exception {
// Criteria API를 사용한 사용자 조회 예제
List<User> usersByNameCriteria = userRepository.findUsersByName("홍길동");
usersByNameCriteria.forEach(user -> log.info("Criteria API로 찾은 사용자: " + user.getName() + ", 이메일: " + user.getEmail()));
}
}
- UserRepository에서 findUsersByName 메서드를 호출하여
이름이 "홍길동"인 사용자 목록을 조회하고, 그 결과를 usersByNameCriteria에 저장 - 조회된 사용자 목록을 반복하여 각 사용자의 이름과 이메일을 로그에 출력
- Criteria API : 동적 쿼리 생성
➡️쿼리를 프로그래밍 방식으로 구성하여 실행 시 다양한 검색 조건을 유연하게 적용할 수 있음
특히 사용자 입력에 따라 변화하는 쿼리의 조건을 처리해야할때 유용
⚠️정적 쿼리 (=미리 정의된 쿼리)로만 모든 사용사례를 처리할 수 없기 때문
Criteria API의 동적 쿼리 생성 절차
- CriteriaBuilder 인스턴스 생성
➡️EntityManager로부터 CriteriaBuilder 객체를 얻음 (쿼리의 조건 및 구조를 정의하는데 사용) - CriteriaQuery 객체 생성
➡️CriteriaBuilder를 사용해 CriteriaQuery 객체를 생성, (반환될 결과의 형식을 정의) - Root 정의
➡️쿼리의 FROM절에 해당하는 Root객체를 생성 (쿼리의 메인 테이블을 나타냄) - 조건 추가 (WHERE 절)
➡️필요한 검색 조건을 CriteriaBuilder를 통해 생성하고 CriteriaQuery에 추가 - 쿼리 실행
➡️완성된 CriteriaQuery를 EntityManager를 통해 실행함
▶️실습 - 이름과 이메일 주소로 사용자 조회 메소드 추가
- 사용자 검색 동적 쿼리 생성 (Criteria 활용)
UserRepositoryCustom 인터페이스에 메소드 추가
public interface UserRepositoryCustom {
List<User> findUsersByName(String name);
List<User> findUsersDynamically(String name, String email);
}
UserRepositoryCustomImpl 메소드 기능 오버라이딩
@Override
public List<User> findUsersDynamically(String name, String email) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class); // createQuery는 대상이 될 엔티티를 넣어줌 (User.class)
Root<User> user = query.from(User.class);
List<Predicate> predicates = new ArrayList<>();
if(name != null){
predicates.add(cb.equal(user.get("name"), name)); // predicates는 List이므로 List의 add()메소드로 쿼리문을 인자로 받음
}
if(email != null){
predicates.add(cb.equal(user.get("email"), email));
}
query.select(user).where(cb.and(predicates.toArray(new Predicate[0])));
// 엔티티매니저로부터 쿼리를 생성하고 그 쿼리로는 지금까지 만든 query를 넣어준 후 결과 리스트로 반환하는 형태
return entityManager.createQuery(query).getResultList();
}
- List<Predicate> predicates = new ArrayList<>();
➡️Predicate : 조건값들을 담는 인터페이스
조건을 List에 담는데 Predicate는 조건을 리스트에 담도록 할 수 있음
(=조건이 여러개 있을 수 있으므로 리스트에 담는 것)
Predicate 인터페이스 내부의 BooleanOperator 메소드가 AND, OR등을 자동으로 붙여서
쿼리를 만들어주는 역할을 함 - Root<User> user = query.from(User.class);
➡️query.from(User.class); : 조회 대상이 되는 엔티티 User를 지정
➡️Root<User> : SQL의 FROM 절에 해당 >> User 엔티티를 기준으로 필드를 참조 - query.select(user).where(cb.and(predicates.toArray(new Predicate[0])));
➡️predicates리스트의 toArray를 통해 배열로 꺼내와서 0번째 인덱스로 만들어줄 것
이 배열에는
- cb.equals(user.get("name"), name)
- cb.equals(user.get("email"), email) 가 저장되어있음
이들을 and() 메소드로 AND 조건으로 붙임 (=여러 조건들을 AND 연산으로 붙여줌)
만약 or() 메소드이면 OR 조건으로 toArray()에 저장된 여러 조건들을 OR 연산으로 붙여줌
and()일때 경우의 수
1) name != null, email == null
select u from User u where u.name = name; // 쿼리 생성
2) name == null, email != null
select u from User u where u.email = email; // 쿼리 생성
3) name != null, email != null
select u from User u where u.name = name and u.email = email; // 쿼리 생성
4) name == null, email == null
select u from User u; // 쿼리 생성
자동으로 이러한 쿼리들이 만들어지므로 동적 쿼리라고 부르는 것
- cb.and(predicates.toArray(new Predicate[0]))
➡️toArray() : 리스트의 요소들을 배열로 변환
🚀toArray(new Predicate[0]) : List<Predicate] 를 Predicate[] 배열로 변환
(= 0은 배열의 크기가 아닌 배열 타입을 맞추는 역할)
➡️Predicate 리스트를 and()로 묶어 WHERE name = ? AND email = ? 형식으로 설정
만약 or()를 사용하면 WHERE name = ? OR email = ?
predicates 리스트의 toArray를 통해 배열로 꺼내와서 0번째 인덱스로 만들어준다는 것
(=리스트를 Predicate[] 배열로 변환)
ex. predicates 리스트에 두 개의 조건이 있다고 가정
predicates.add(cb.equal(user.get("name"), "Milos"));
predicates.add(cb.equal(user.get("email"), "rogers@exam.com"));
- cb.or(predicates.toArray(new Predicate[0]))
➡️이를 통해 SQL 쿼리로 변환 (동적 쿼리 생성)
SELECT * FROM user
WHERE name = 'Milos' OR email = 'rogers@exam.com';
❓toArray(new Predicate[0])을 사용하는 이유
➡️가변적인 리스트 크기에 적합함
만약 toArray(new Predicate[predicates.size()])처럼 리스트 크기만큼 배열을 미리 할당하면,
불필요한 메모리 할당이 발생할 수도 있음.
반면, toArray(new Predicate[0])을 넘기면 Java가 내부적으로 적절한 크기의 배열을 생성하여 반환하여 효율적임
main 테스트
// Criteria 활용 이름, 이메일 주소에 따른 사용자 조회
userRepository.findUsersDynamically("Milos", null)
.forEach(user -> log.info("user :: {} {}", user.getName(), user.getEmail()));
➡️상황 1) ("Milos", null)
name != null, email == null
메소드 내부 : cb.equal(user.get("name"), "Milos") 조건 추가
동적 쿼리 생성 : WHERE name = 'Milos'
출력:
user :: Milos Milos@exam.com
user :: Milos Milos@example.com
>> "Milos"인 사용자 2명이 반환
➡️상황 2) ("Milos", "exam.com")
name != null, email != null
userRepository.findUsersDynamically("Milos", "exam.com");
메소드 내부 :
cb.equal(user.get("name"), "Milos") 조건 추가
cb.equal(user.get("email"), "exam.com") 조건 추가
동적 쿼리 생성 : WHERE name = 'Milos' AND email = 'exam.com'
출력:
user :: Milos Milos@exam.com
>> "Milos"이면서 "exam.com" 이메일을 가진 사용자만 반환
➡️상황 3) ("Milos", "rogers@exam.com")
name != null, email != null
// OR 조건일 경우
query.select(user).where(cb.or(predicates.toArray(new Predicate[0])));
userRepository.findUsersDynamically("Milos", "rogers@exam.com");
메소드 내부 :
cb.equal(user.get("name"), "Milos") 조건 추가
cb.equal(user.get("email"), "rogers@exam.com") 조건 추가
동적 쿼리 생성 : WHERE name = 'Milos' AND email = 'rogers@exam.com'
출력:
user :: Milos Milos@exam.com
user :: Milos Milos@example.com
user :: Morgan rogers@exam.com
>> 이름이 "Milos"인 사용자 2명과 이메일이 "rogers@exam.com"인 사용자 1명 모두 반환
🚀hr 데이터베이스 실습
hr DB의 ERD
Region
@Entity
@Getter@Setter@NoArgsConstructor
@Table(name = "regions")
public class Region {
@Id
@Column(name = "region_id")
private Integer id;
@Column(name = "region_name")
private String name;
}
Employee
@Entity
@Getter@Setter@NoArgsConstructor
@Table(name = "employees")
public class Employee {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "employee_id")
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
private String email;
@Column(name = "phone_number")
private String phoneNumber;
@Column(name = "hire_date")
private LocalDate hireDate;
@ManyToOne
@JoinColumn(name = "job_id")
private Job job;
private Double salary;
@Column(name = "commission_pct")
private Double commissionPct;
@ManyToOne
@JoinColumn(name = "manager_id")
private Employee manager;
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
@OneToMany(mappedBy = "manager")
private Set<Employee> subordinates;
@OneToMany(mappedBy = "employee")
private Set<JobHistory> jobHistories;
}
- 다른 엔티티들과 관계 매핑이 많은 Employee 엔티티까지 생성
❓Employee에서 Job엔티티와의 관계가 @ManyToOne인 이유
➡️하나의 직업에 여러 사원이 그 직업을 가질 수 있으므로 이러한 관계 정의
❓Employee와 Department와의 관계
// foreignKey = @ForeignKey(name = "FK_department_manager"), nullable = true
@ManyToOne
@JoinColumn(name = "manager_id")
private Employee manager;
- 한 관리자 사원이 여러 부서를 관리할 수 있다는 것을 표현
- @ForeignKey처럼 외래키 어노테이션을 사용할 수도 있음
EmployeeRepository 메소드 추가
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByLastName(String lastName);
}
main 테스트
@Bean
public CommandLineRunner run(EmployeeRepository employeeRepository){
return args -> {
List<Employee> findLastName = employeeRepository.findByLastName("Patel");
findLastName.forEach(employee -> log.info("[마지막 이름이 Patel]인 고객 정보 :: {} {} 님", employee.getFirstName(), employee.getLastName()));
};
}
- 정상적으로 출력
🚀실습 - 연봉 12000이상 사원 조회
// JPA 실습
List<Employee> findMoreSalary = employeeRepository.findBySalaryGreaterThanEqual(12000d);
findMoreSalary.forEach(employee -> log.info("[연봉 12000 이상]인 고객 : {}의 연봉 [{}]", employee.getFirstName(), employee.getSalary()));
🚀실습 - 사원번호가 176번인 사원 조회
// 2. 사원 번호로 조회
Optional<Employee> findEmpId = employeeRepository.findById(176);
if(findEmpId.isPresent()){
Employee employee = findEmpId.get();
log.info("[사원번호 176]인 사원 : {}", employee.getLastName());
}
⚠️오류해결
전체 사원번호로 조회하면 176번인 사원이 나오지 않는데
-- 전체 사원번호 조회
select employee_id from employees;
-- 1-1) 사원번호가 176번인 사원 조회
select employee_id, last_name from employees where employee_id = 176;
-- 1-2) 사원번호가 176번인 사원 조회
select * from employees where employee_id = 176;
- 176번인 사원을 직접 접근하도록 조회하면 데이터가 있는 것을 확인 가능
➡️1번 방법. 인덱스 손상 확인
-- 인덱스 손상 확인
check table employees; analyze table employees;
- 인덱스 매핑이 손상되었는지 확인해보았지만 문제 없음
➡️2번 방법. 정렬 후 출력
-- 정렬 후 전체 사원 출력
SELECT employee_id FROM employees ORDER BY employee_id ASC;
- 100~149번대까지밖에 사원이 출력되지 않았음
➡️3번 방법. 출력 행 제한 확인
-- 출력 행 제한 확인
select @@max_allowed_packet, @@sql_select_limit;
- 이미 충분한 limit를 가지고 있으므로 문제 없음
- 총 데이터 개수 확인 ➡️select count(*) from employees; -- 107개
✅4번 방법. 총 데이터 출력 제한 늘리기
-- 총 데이터 출력 제한 10000개로 늘리기
select employee_id from employees limit 10000;
-- 전체 사원 번호 재조회
select employee_id from employees;
- 176번 사원도 포함되어 전체 사원의 id가 나오는 것을 확인 가능
- limit으로 출력행의 제한을 늘려줌으로써 해결할 수 있음
🚀회고 결과 :
이번 회고에서는 Criteria API에 대한 이해가 부족해서 추가 공부를 하였다.
아직 사용에 익숙치 않지만 개념의 이해를 하는 것에 많은 시간 공부한 것 같다.
또한 실습을 진행하면서 생겼던 오류를 해결할 수 있었다.
- MySQL 데이터 출력 제한 늘리기 (limit 사용)
- ERD로 엔티티 설계
느낀 점 :
Docker의 버전 오류로 포트 충돌이 일어나서 제대로 실습을 진행하지 못했지만
오류 해결을 겨우 할 수 있었다. 오류 해결 과정은 별도의 회고로 작성해야할 것 같다.
Criteria API를 활용한 다른 실습들을 진행하여 더 Criteria에 대해 알아봐야겠다고 느꼈다.
향후 계획 :
- Criteria API 추가 공부
- Docker 포트 충돌 문제 - 다른 해결법
- MySQL Query 연습
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_53일차_"CURL" (0) | 2025.02.25 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_52일차_"RESTful API" (0) | 2025.02.21 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_50일차_"Spring Data JPA" (0) | 2025.02.19 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_49일차_"JPA 상속 관계 매핑" (1) | 2025.02.18 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_48일차_"JPA 관계형 테이블" (1) | 2025.02.14 |