🔶코딩테스트
🔶JPA
🔶값 타입
🔶객체지향 쿼리 언어
≣ 목차
/ 오늘의 TIL /
코딩테스트
https://school.programmers.co.kr/learn/courses/30/lessons/120902?language=java#
숏코딩
import java.util.Arrays;
class Solution {
public int solution(String my_string) {
return Arrays.stream(
my_string.replaceAll("- ", "-")
.replaceAll("[+] ", "")
.split(" "))
.mapToInt(Integer::parseInt)
.sum();
}
}
풀이
class Solution {
public int solution(String my_string) {
String[] str = my_string.split(" ");
int result = Integer.parseInt(str[0]);
boolean oper = true;
for (int i = 1; i < str.length; i++) {
if (str[i].equals("-")) oper = false;
else if (str[i].equals("+")) oper = true;
else {
if (oper) { //+
result += Integer.parseInt(str[i]);
} else { //-
result -= Integer.parseInt(str[i]);
}
}
}
return result;
}
}
JPA
프록시
- em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- 최초 1회 초기화 요청 시, 영속성 컨텍스트에서 DB를 조회하고 실제 엔티티를 생성해준다.
- 초기화가 되지 않은 상태에서 실제 데이터를 요청하면 프록시 내부에 있는 target 필드를 통해 실제 엔티티 내부에 있는 필드 값을 조회한다.
- 초기화가 된 후는 프록시 내부에 있는 필드 값을 가져온다.
JPA의 동일성 보장: proxy 조회 후 find를 하면 둘다 proxy가 된다.
프록시 초기화 불가: em.detach(), em.close()로 영속성 컨텍스트를 제거 후 실제 데이터를 불러오려고 하면 세션이 없다고 표시된다.
org.hibernate.LazyInitializationException
이미 생성된 객체에서 find를 통해 가져온 객체끼리는 ==로 비교 가능하다.
getReference로 객체를 가져와서 타입 체크 시 프록시기 때문에 ==비교 대신, instance of를 사용해야 한다.
m1 instanceof Member
즉시 로딩과 지연 로딩
- 지연 로딩(LAZY)를 이용하면 프록시로 조회한다. [권장]
- @ManyToOne, @OneToOne은 default가 즉시 로딩이므로 LAZY로 설정한다.
@ManyToOne(fetch = FetchType.LAZY)
- 즉시 로딩(EAGER)를 이용하면 join으로 한 번에 조회하고, proxy가 필요 없어진다. 초기화도 필요 없다.
- 예상하지 못한 SQL이 발생할 가능성이 있다.
- JPQL에서 N+1 문제를 일으킨다.
영속성 전이, 고아 객체
영속성 전이 + 고아 객체를 둘다 사용하는 경우
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화하고 em.remove()로 제거할 수 있다.
- 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
- DDD의 Aggregate Root 개념을 구현할 때 유용
cascade
Parent가 persist되면 cascade(영속성 전이)를 통해 함께 persist 된다.
연관 관계 매핑과 관련이 없다.
단일 엔티티에 완전히 종속적일 때만 사용할 것을 권장한다.
- cascade 종류
- ALL: 모두 적용
- PERSIST: 영속
- REMOVE: 삭제
//Child
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
//Parent
@OneToMay(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
고아 객체
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 제거한다.
orphanRemoval이 true로 되어있으면, 컬렉션에서 빠진 것은 삭제된다.
참조하는 곳이 하나일 때 사용해야 한다.
특정 엔티티가 개인 소유할 때 사용한다.
cascade all과 동일한 영향을 미친다.
//Parent
@OneToMay(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
기본 값 타입
엔티티 타입
@Entity로 정의하는 객체
데이터 변경 시 식별자로 추적 가능
값 타입
기본값 타입
int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
데이터 변경 시 식별자가 없으므로 추적 불가
생명주기를 엔티티에 의존한다.
공유하면 안된다.
- primitive type 기본 타입은 항상 값을 복사하기 때문에 절대 값을 공유하지 않는다.
- 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경하면 안된다.
//primitive type
int c = 10;
int d = c;
c = 20;
System.out.println(c); //20
System.out.println(d); //10
//============================
//Wrapper class
Integer a = new Integer(10);
Integer b = a;
a.setValue(20);
System.out.println(a); //20
System.out.println(a); //20
임베디드 타입(복합 값 타입)
새로운 값 타입을 직접 정의할 수 있다.
JPA는 임베디드 타입이라고 한다.
기본 생성자 필수
재사용이 가능하고 높은 응집도를 가진다.
의미있는 비즈니스 메서드를 만드는 데 도움된다.
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
Member 내에 필드로 존재하던
등록 날짜, 수정 날짜 -> Period 클래스로 생성
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
}
public class Member {
@Embedded
private Period workPeriod;
}
길 번호, 주소명 -> Address 클래스로 생성
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
public class Member {
@Embedded
private Address homeAddress;
}
@AttributeOverride 를 통해 컬럼명 속성을 재정의 할 수 있다.
값 타입과 불변 객체
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다. (side effect 가능성)
Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setUsername("memeber1");
member.setHomeAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setUsername("memeber2");
member2.setHomeAddress(address);
em.persist(member2);
//member, member2 객체 둘다 city가 "newCity"로 변경된다.
member.getHomeAddress().setCity("newcity");
따라서 값을 복사해서 사용하는 것이 안전하다.
Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setUsername("memeber1");
member.setHomeAddress(address);
em.persist(member);
//값 복사 객체 생성
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setUsername("memeber2");
member2.setHomeAddress(copyAddress); //복사한 객체 삽입
em.persist(member2);
//member 객체만 city가 "newCity"로 변경된다.
member.getHomeAddress().setCity("newcity");
직접 정의한 값 타입은 객체 타입이다.
따라서 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없고, 객체의 공유 참조는 피할 수 없다.
객체 타입은 참조를 전달한다. (깊은 복사)
객체 타입 참조 해결 방안 (불변 객체 설계)
객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단할 수 있다.
참조로 인한 사이드 이펙트를 없앨 수 있다.
값 타입은 불변 객체(immutable object)로 설계할 것
생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 된다.
Integer, String은 자바가 제공하는 대표적인 불변 객체이다.
값 타입의 비교
- 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
- 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
- 값 타입은 a.equlas(b)로 비교해주어야 한다.
- equlas를 재정의하면, 객체의 필드가 동일한 객체는 논리적으로 같은 객체로 판단된다.
- 오버라이드 시, 해시코드도 같이 구현해주어야 함(자바 컬렉션 사용) - 해시 값을 사용하는 컬렉션을 사용할 때 문제 발생 가능성
int a = 10;
int b = 10;
System.out.println((a == b)); //true
Address address1 = new Address("city", "street", "10000");
Address address2 = new Address("city", "street", "10000");
System.out.println((address1.equals(address2))); //true
System.out.println((address1 == address2)); //false
//Address
@Override
public boolean equlas(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(city, address.getCity()) && //getter로 호출해야 proxy가 계산할 수 있음
Objects.equals(street, address.getStreet()) &&
Objects.equals(zipcode, address.getZipcode());
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
값 타입 컬렉션
식별자 id 같은 개념을 넣어서 pk로 사용하면 값 타입이 아니고 엔티티가 되어버린다.
그래서 값 타입은 테이블에 값들만 저장이 되고 이걸 묶어서 pk로 구성하게 된다.
-> 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
수정할 때 생성자를 통해 통째로 갈아끼워야 한다. setter 금지!
값 타입이기 때문에 상위 클래스를 persist하면 같은 라이프 사이클이 돌아가서 모두 persist 된다.
값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다.
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 관련된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 그래서 권장X
대신에 일대다[1:N] 관계를 고려한다.
값 타입을 하나 이상 저장할 때 @ElementCollection을 사용한다.
컬렉션들은 기본적으로 지연(Lazy)로딩이다.
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME") //값이 1개이므로 예외적으로 @Column 세팅
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDREDSS"
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
객체지향 쿼리 언어
JPQL
객체 지향 SQL, 추상화 SQL(특정 SQL에 종속되지 않는다)
엔티티 객체를 대상으로 쿼리를 진행한다.
<->SQL: 데이터베이스 테이블을 대상으로 쿼리를 진행한다.
기본 문법과 쿼리 API
TypeQuery, Query
- TypeQuery: 반환 타입이 명확할 때 사용
- Query: 반환 타입이 명확하지 않을 때 사용
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);
Query query3 = em.createQuery("select m.username, m.age from Member m");
결과 조회 API
- query.getResultList(): 결과가 하나 이상일 때. 결과 없는 경우 빈 리스트 반환
- query.getSingleResult(): 결과가 하나일 때 단일 객체 반환. 결과 없는 경우 NoResultException, 둘 이상이면 NonUniqueResultException
파라미터 바인딩
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
query.setParameter("username", "member1");
프로젝션
//엔티티 타입
select m from Member m
select t from Member m join m.team t
//임베디드 타입 (값 타입의 한계는 address는 m에 종속되어 있다.)
select m.address from Member m
//스칼라 타입 가능
select m.username, m.age from Member m
- 엔티티 프로젝션: 영속성 컨텍스트에서 관리된다.
- 스칼라 타입 프로젝션
- Query
- Object[]
- new (생성자처럼 사용 가능)
List<MemberDTO> result = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
.getResultList();
MemberDTO memberDTO = result.get(0);
System.out.println("memberDTO.getUsername() = " + memberDTO.getUsername());
페이징 API
dialect에 따라 변화해서 작성된다.
- setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult): 조회할 데이터 수
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(1)
.setMaxResults(10)
.getResultList();
조인
//내부 조인
select m from Member m [INNER] join m.team t
//외부 조인
select m from Member m left [OUTER] join m.team t
//세타 조인
select count(m) from Member m, Team t where m.username = t.name
ON 절
- 조인 대상 필터링
//jpql
select m, t from Member m left join m.team t on t.name = 'a'
//sql
select m.*, t.* from Member m left join Team t on m.TEAM_ID=t.id and t.name='a'
- 연관관계 없는 엔티티 외부 조인
//jpql
select m, t from Member m left join team t on m.username = t.name
//sql
select m.*, t.* from Member m left join Team t on m.username = t.name
서브 쿼리
jpa는 from 절의 서브쿼리를 제공하지 않는다. -> 조인으로 풀 수 있으면 풀어서 해결. 안되면 native sql
select 서브쿼리는 가능하다. hibernate에서 지원한다.
where, having 절에서 사용 가능하다.
조건식
case 식
//기본 case식
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
//단순 case식
select
case t.name
when '팀A' then '110%'
when '팀B' then '120%'
else '105%'
end
from Team t
coalesce: 하나씩 조회해서 null이 아니면 반환
nullif: 두 값이 같으면 null 반환, 다르면 첫 번째 값 반환
//사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username, '이름 없는 회원') from Member m
//사용자 이름이 관리자면 Null을 반환하고 나머지는 본인의 이름을 반환
select nullif(m.username, '관리자') from Member m
기본 함수
trim
lower, upper
length
abs, sort, mod
index (권장X)
//concat
select concat('a', 'b') from Member m
//substring
select substring(m.username, 2, 3) from Member m
//locate
select locate('de', 'abcdefg') from Member m //4번째 표시
//size
select size(t.members) from Team t //컬렉션 크기
사용자 정의 함수도 호출할 수 있으나, 하이버네이트는 사용 전 방언에 추가해주어야 한다.
사용하는 db 방언 상속받아서 등록
select group_concat(m.username) from Member m
select function('group_concat', m.username) from Member m
경로 표현식
묵시적 내부 조인 사용 X
- 상태 필드: 경로 탐색 끝. 탐색 X
- m.username
- 단일 값 연관 경로: 묵시적 내부 조인 발생. 탐색 O
- m.team(.name...)
- 컬렉션 값 연관 경로: 묵시적 내부 조인 발생. 탐색 X
- t.members (List는 그대로 탐색 종료)
- 명시적인 조인 작성 `select m from Team t join t.members m`
페치 조인(fetch join)
즉시 로딩과 비슷함~! 사용 안 하면 N+1 문제 발생된다.
별칭을 줄 수 없다. 하이버네이트는 가능하지만 가급적 사용하지 않는다.
둘 이상의 컬렉션은 페치 조인 할 수 없다.
페이징 API 사용 불가(일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능)
@BatchSize로 미리 가져올 크기 제한 가능
persistence.xml로도 미리 설정 가능
<property name="hibernate.default_batch_fetch_size" value="100"/>
1. 페치 조인을 사용해서 엔티티를 조회한다.
2. 페치 조인 후 애플리케이션에서 dto로 바꿔서 화면을 반환한다.
3. new operation으로 dto로 스위칭해서 가져온다.
지연 로딩 < 페치 조인 우선순위
일대다 조인은 중복해서 출력되기 때문에 유의할 것(데이터 뻥튀기 이슈)
jpql distinct 기능
- sql에 distinct를 추가
- 애플리케이션에서 엔티티 중복 제거 (같은 식별자를 가진 엔티티 제거)
sql 조인 종류가 아니고, jpql에서 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 sql 한 번에 함께 조회하는 기능이다.
//jpql
select m from Member m join fetch m.team
//sql
select M.*, T.* from member M inner join team T on M.TEAM_ID = T.ID
엔티티 직접 사용
sql에서 해당 엔티티의 기본 키 값을 사용한다.
-> 엔티티를 파라미터로 전달하거나 식별자를 직접 전달하는 것 모두 동일한 쿼리.
(중요!) 외래 키 값에서 m.team = :team 을 조건으로 가져올 때, m.team은 @JoinColumn(name = "TEAM_ID")의 TEAM_ID를 뜻한다.
//jpql
select count(m.id) from Member m
select count(m) from Member m
//sql
select count(m.id) as cnt from Member m
Named 쿼리
정적 쿼리. 어노테이션, XML에 정의
애플리케이션 로딩 시점에 초기화 한 후 재사용한다.
애플리케이션 로딩 시점에 쿼리를 검증한다.
나중에 Spring Data JPA에서 interface에서 @Query로 사용할 수 있다!!
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
벌크 연산
너무 많은 sql을 실행해야 할 때 유용하다.
쿼리 한 번으로 여러 테이블의 로우를 변경할 수 있다.(엔티티)
결과는 영향받은 엔티티 수를 반환한다.
스프링 데이터 JPA에서 나중에 @Modifying 사용하면 된다.
//flush 자동 호출
//영속성 컨텍스트 값 업데이트 안됨 -> clear 필요!
int resultCount = em.createQuery("update Member m set m.age = 20")
.executeUpdate();
(중요!)
영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날리기 때문에,
벌크 연산을 먼저 실행하거나
벌크 연산 수행 후 영속성 컨텍스트를 초기화해야 한다.
Criteria
String sql이 아니기 때문에 컴파일 시점에 문법 오류를 찾을 수 있고, 동적 쿼리를 작성하는 데 용이하다.
그런데 잘 안 쓰긴 함... 이런게 있다 정도 알아두면 될듯.
너무 복잡하고 실용성이 없다.
-> QuerDSL 사용 권장!
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
Root<Member> m = query.from(Member.class);
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();
QueryDSL
동적쿼리 작성 편리
단순하고 쉬움
컴파일 시점에 문법 오류를 찾을 수 있음
private final JPAQueryFactory queryFactory;
public void hello() {
QMember m = QMember.member;
List<Member> result = queryFactory
.select(m)
.from(m)
.where(m.name.like("kim"))
.fetch();
}
Native Query
sql query를 작성한다.
em.createNativeQuery("sql").getResultList();
JDBC, SpringJdbcTemplate
JDBC 커넥션을 직접 사용하거나 JdbcTemplate, Mybatis 등을 함게 사용할 수 있다.
영속성 컨텍스트를 적절한 시점에 강제로 flush 할 필요가 있다.
-> jpa를 우회해서 sql을 실행하기 직전에 영속성 컨텍스트 수동 플러쉬
em.flush();
dbconn.executeQuery("select * from member");
* flush는 commit, create query가 실행되면 동작한다.
'Blog > TIL' 카테고리의 다른 글
[240606] queue 복습1 (0) | 2024.06.07 |
---|---|
[240605] 문제를 잘 파악하자 (0) | 2024.06.05 |
[240603] JPA의 핵심, 연관관계의 주인 설정 (0) | 2024.06.03 |
[240602] JPA 영속성 컨텍스트 (0) | 2024.06.02 |
[240601] 코딩 테스트 문제 풀기 (0) | 2024.06.01 |
댓글