사용자 도구

사이트 도구


java:hibernate:gotchas

차이

문서의 선택한 두 판 사이의 차이를 보여줍니다.

차이 보기로 링크

양쪽 이전 판 이전 판
다음 판
이전 판
java:hibernate:gotchas [2015/02/13 11:32]
kwon37xi
java:hibernate:gotchas [2023/07/03 14:54] (현재)
kwon37xi [to-many 관계에 대해서 fetch join은 안하는게 낫다]
줄 1: 줄 1:
-====== Hibernate/JPA Gotchas ======+====== Hibernate/JPA Gotcha ======
 하이버네이트/JPA에 관해 항상 기억해둬야 할 사항들을 정리해 둔다. 하이버네이트/JPA에 관해 항상 기억해둬야 할 사항들을 정리해 둔다.
  
줄 5: 줄 5:
 hashCode와 equals를 항상 구현해야 한다. hashCode와 equals를 항상 구현해야 한다.
  
-이때 DB 프라이머리키(ID)가 아닌 **항상 변치 않고 해당 객체를 대표하는 의미를 가질 수 있는 비즈니스 키(business key)를 hashCode와 equals의 대상으로 삼아야 한다.**+이때 DB Primary Key(ID)가 아닌 **항상 변치 않고 해당 객체를 대표하는 의미를 가질 수 있는 비즈니스 키(business key)를 hashCode와 equals의 대상으로 삼도록 노력한다.**
 이유는 Set등에 신규 객체를 넣을 경우 신규 객체는 아직 프라이머리키가 지정되지 않은 상태이기 때문에 ID가 모두 ''null'' 혹은 ''0''이며 이 경우 Set에 저장할 때 equals가 항상 true 여서 이전의 값을 뒤에 저장한 값이 계속 덮어써버리는 현상이 발생하기 때문이다. 이유는 Set등에 신규 객체를 넣을 경우 신규 객체는 아직 프라이머리키가 지정되지 않은 상태이기 때문에 ID가 모두 ''null'' 혹은 ''0''이며 이 경우 Set에 저장할 때 equals가 항상 true 여서 이전의 값을 뒤에 저장한 값이 계속 덮어써버리는 현상이 발생하기 때문이다.
 +
 +하지만 실제로 해보면 인공키를 hashCode의 기준으로 삼을 수 밖에 없는 경우도 많다. Set/Map 과 함께 사용할 때 주의를 기울여야한다.
  
 따라서 ID를 equals/hashCode의 기준으로 삼을 경우 매우 주의해야 한다. 관련 참조 - [[http://community.jboss.org/wiki/EqualsAndHashCode|Hibernate equals and hashCode]] 따라서 ID를 equals/hashCode의 기준으로 삼을 경우 매우 주의해야 한다. 관련 참조 - [[http://community.jboss.org/wiki/EqualsAndHashCode|Hibernate equals and hashCode]]
  
 +===== to-many 관계에 대해서 fetch join은 안하는게 낫다 =====
 +  * to-many 관계에 대해서 fetch join 을 하면 부모측이 자식측(many)의 갯수만큼 중복으로 결과 갯수가 늘어난다.
 +  * 이를 해결하려면 ''distinct'' 를 꼭 해야한다.
 +  * to-many 관계에 대해서 ''limit'' 을  걸게 되면 그 일반 SQL에서는 자식의 갯수에 대한 limit 이 돼 버린다. 이렇게 되면 실제 조회를 원하는 데이터(parent)의 갯수가 의도대로 나오지 않게 된다.
 +    * 때문에 하이버네이트는 SQL에 limit을 걸리 않고 limit 없이 전체 데이터를 조회한 뒤에 메모리에서 parent 데이터를 원하는 limit 갯수만큼 끊어서 반환한다.
 +    * 이는 엄청난 성능저하로 이어진다.
 +  * 결과적으로 특별한 이유가 없다면 **to-many 관계에 대해서는 fetch join을 하지 말고 항상 ''Hibernate.initialize()'' 등으로 초기화를 하되 batch size 를 지정하는 식으로 한다.**
 +    * [[java:hibernate:performance|Hibernate Performance Tuning]] ''hibernate.default_batch_fetch_size=30''
 +    * ''default_batch_fetch_size''를 지정하면 N+1 이 아니라 N 번 조회할 것은 in 쿼리로 batch_size 만큼의 인자를 넣어서 한 방에 조회한다.
 +  * [[java:hibernate:configuration|Hibernate Configurations]] - ''hibernate.query.fail_on_pagination_over_collection_fetch=true'' 옵션을 켜서 미리 에러를 내고 확인해서 고쳐두는게 좋다.
 ===== One-To-One 과 Many-To-One 관계를 조심하라 ===== ===== One-To-One 과 Many-To-One 관계를 조심하라 =====
 One-To-One과 Many-To-One 관계에서 One 측은 ''not null''이라고 명시하지 않는이상 **Lazy Loading이 작동하지 않는다.** One-To-One과 Many-To-One 관계에서 One 측은 ''not null''이라고 명시하지 않는이상 **Lazy Loading이 작동하지 않는다.**
 이에 관해서는 [[java:jpa:one-to-one|JPA One-To-One]]을 참조한다. 이에 관해서는 [[java:jpa:one-to-one|JPA One-To-One]]을 참조한다.
 +
 +===== 관계 필드에 존재하지 않는 엔티티에 대한 값을 넣지 말라 =====
 +A -> B 관계가 있을 때 A에 있는 B를 가리키는 컬럼에 존재하지 않는 B에 대한 값(특히 ''0'')을 넣으면 A 를 조회할 때 A 자체가 조회가 안된다(Foreign Key가 안 걸려 있으면 존재하지 않는 값 지정이 가능함).
 +
 +존재하지 않는 값은 **''null''**로 넣어야지 ''0''이나 ''공백'' 같은 값을 사용하면 안된다.
  
 ===== toString 메소드를 조심하라 ===== ===== toString 메소드를 조심하라 =====
줄 18: 줄 35:
  
 ===== OpenSessionInView 패턴 사용시 HTML 주석을 사용치 말라 =====  ===== OpenSessionInView 패턴 사용시 HTML 주석을 사용치 말라 ===== 
 +무엇보다 가급적 **OpenSessionInView 패턴을 사용하지 말라**. 처음엔 편하지만 나중에는 불명확한 쿼리 실행으로 튜닝이 오히려 어렵고 복잡해진다.
 +
 toString 메소드에서와 같은 현상이 OpenSessionInView 패턴 사용시 뷰에서 발생할 수 있다. toString 메소드에서와 같은 현상이 OpenSessionInView 패턴 사용시 뷰에서 발생할 수 있다.
 더이상 필요없는 부분 특히 하이버네이트 도메인 객체를 호출하는 부분을 주석처리할 때 **HTML 주석을 사용하지 말라.** HTML주석은 서버사이드는 그대로 동작한다. 따라서 레이지 로딩으로 지정한 값을 HTML 주석 부분에서 호출하면 그대로 값이 로딩되어 출력된다. 더이상 필요없는 부분 특히 하이버네이트 도메인 객체를 호출하는 부분을 주석처리할 때 **HTML 주석을 사용하지 말라.** HTML주석은 서버사이드는 그대로 동작한다. 따라서 레이지 로딩으로 지정한 값을 HTML 주석 부분에서 호출하면 그대로 값이 로딩되어 출력된다.
줄 23: 줄 42:
 항상 **템플릿 엔진의 주석(JSP의 경우 <%-- --%>)을 사용하라.** 항상 **템플릿 엔진의 주석(JSP의 경우 <%-- --%>)을 사용하라.**
  
-===== 쿼리 로그를 남겨라 =====+===== 개발 환경에서는 쿼리 로그를 남겨라 =====
 <code properties> <code properties>
 hibernate.show_sql=true hibernate.show_sql=true
줄 31: 줄 50:
 [[java:hibernate:log|Hibernate Log 남기기]]를 참조한다. [[java:hibernate:log|Hibernate Log 남기기]]를 참조한다.
  
 +단, 실 운영환경에서는 로그를 남기는 순간 매우 느려지므로 로그 레벨을 높여야 한다.
 ===== 쿼리에 진짜로 ":"로 들어간다면? ===== ===== 쿼리에 진짜로 ":"로 들어간다면? =====
 Native SQL에 '':''를 넣어야 할 필요가 있다면 Escape을 해줘야 한다. Native SQL에 '':''를 넣어야 할 필요가 있다면 Escape을 해줘야 한다.
줄 45: 줄 65:
   * [[https://hibernate.atlassian.net/browse/HHH-6776|Hibernate inserts duplicates into @OneToMany collection]]   * [[https://hibernate.atlassian.net/browse/HHH-6776|Hibernate inserts duplicates into @OneToMany collection]]
   * [[https://hibernate.atlassian.net/browse/HHH-5855|The method "merge" from the EntityManager causes a duplicated "insert" of a child entity.]]   * [[https://hibernate.atlassian.net/browse/HHH-5855|The method "merge" from the EntityManager causes a duplicated "insert" of a child entity.]]
-  * Lazy ''@OneToMany'' 컬렉션에 ''add(자식객체)''를 할 경우 두번 insert 되는 문제가 있다.+  * Lazy ''@OneToMany'' 컬렉션에 ''add(자식Entity)''를 하면서 ''entityManger.merge(부모Entity)''를 호출 할 경우 두번 insert 되는 문제가 있다.
   * 이 현상은 ''@OneToMany'' 컬렉션이 Lazy Loading일 경우 컬렉션의 내용이 로딩이 안된 상태에서 add를 하면 발생하는 것 같다.   * 이 현상은 ''@OneToMany'' 컬렉션이 Lazy Loading일 경우 컬렉션의 내용이 로딩이 안된 상태에서 add를 하면 발생하는 것 같다.
-  * 따라서 자식 엔티티를 부모에 추가할 때는 미리 Lazy 컬렉션을 로딩하거나, 아니면 부모 엔티티를 수정하지 말고, 자식 엔티티에 부모 엔티티의 관계를 맺고, 따로 insert 하는 방식으로 처리한다.+  * 재현 
 +    * 부모 엔티티를 읽어온다. ''(A a = em.find(A.class, id))'' 
 +    * 부모측 컬렉션에 자식을 추가한다.''(a.getBs.add(new B()))'' 
 +    * 명시적으로 merge를 호출하여 저장한다. ''(em.merge(a))'' 
 +  * 따라서 자식 엔티티를 부모에 추가할 때는  
 +    * 미리 Lazy 컬렉션을 로딩하거나, ''(Hibernate.initialize(a.getBs())'' 
 +    * 아니면 부모 엔티티를 수정하지 말고, 자식 엔티티에 부모 엔티티의 관계를 맺고, 따로 insert 하거나 
 +    * 아니면 ''entityManager.merge(entity)''를 수행하지 말고 트랜잭션 종료시에 자동 저장되도록 처리한다. 
 + 
 +===== JPQL Positional Parameter Bug ===== 
 +  * JPQL 방식의 Positional Parameter(''?1, ?2, ..'') 형태를 사용할 경우 각종 버그가 발생한다. 사용하지 말 것. 
 +  * [[https://hibernate.atlassian.net/browse/HHH-9871|[HHH-9871] JPA positional parameter mapping bug with" same java type and samve value but different custom type" - Hibernate JIRA]] 
 +  * [[https://hibernate.atlassian.net/browse/HHH-9900|[HHH-9900] Different JPQL queries might be mapped to the same QueryKey]] 
 + 
 +===== @Where 에서 true, false 등에 대해 잘못된 반응 ===== 
 +  * ''@OneToMany''에 ''@org.hibernate.annotations.Where(clause = "deleted = false")''와 같은 조건을 주었을 때, SQL이 잘못 생성된다.<code sql> 
 +select ... from address address0_  
 +where  ( address0_.deleted=address0_.FALSE) -- 이 부분 
 +  and address0_.contact_id=? 
 +</code> 
 +  * [[http://stackoverflow.com/questions/10730640/is-there-a-way-to-stop-hibernate-from-corrupting-boolean-literals-in-where-anno|java - Is there a way to stop Hibernate from corrupting boolean literals in @Where annotations?]] 
 +  * ''true'', ''false'' 등이 Dialect에 keyword로 등록이 되어있지 않아서 생기는 현상이다. 
 +  * 조건을 ''delete = 0'' 형태로 주거나 DB 종류에 따라 문자열 '''true/false'''를 ''true/false''로 인식하면 문자열로 사용하는 방식으로 회피 가능하다. 
 +  * 혹은 사용중인 Dialect 에 ''true'', ''false''를 추가한다.<code java> 
 +public class ImprovedHSQLDialect extends HSQLDialect { 
 + 
 +    public ImprovedHSQLDialect() { 
 +        super(); 
 +        registerKeyword("true"); 
 +        registerKeyword("false"); 
 +        // registerKeyword("unknown"); 
 +    } 
 +
 +</code> 
 +  * [[https://hibernate.atlassian.net/browse/HHH-2775|[HHH-2775] true and false are not escaped in where attributes (@Where) - Hibernate JIRA]] 
 + 
 +===== SQL / JPQL / HQL Keyword "in", "by" 같은 Keyword 가 JPA Entity package 에 들어가면 파싱 오류 발생 ===== 
 +  * [[https://stackoverflow.com/questions/29230286/spring-jpa-in-word-in-package-name-of-entity-class-results-in-jpql-error|java - Spring JPA - "in" word in package name of Entity Class - Results in JPQL Error]] 
 +  * [[https://hibernate.atlassian.net/browse/HHH-11784|[HHH-11784] HQL query fails if entity is inside a package which starts with "in" or "by" - Hibernate JIRA]] 6.0 에서 해결 될 수도 있음. 
 +  * Entity 의 package name 에 ''in''이 들어 있을 경우, 이를 가지고 full package name으로 JPQL을 생성하면 ''in''을 JPQL Keyword로 인식해서 동작하지 않는 현상이 발생했다. 보통, join 같은 복잡한 쿼리에서 발생했다. 
 +  * **패키지 이름에서 ''in'' 같은 JPQL, SQL Keyword 단어가 안들어가게** 할 것. 
 +  * [[https://www.mkyong.com/hibernate/how-to-use-database-reserved-keyword-in-hibernate/|How to use database reserved keyword in Hibernate?]] : ''@Column(name="`desc`")'' 처럼 DB에 적합한 escape 문자 써 줄것. 
 + 
 +===== null id in entry ===== 
 +> HHH000099: an assertion failure occurred (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session): org.hibernate.AssertionFailure: null id in ''com.mycompany.XXX'' entry (don't flush the Session after an exception occurs)
  
 +  * [[https://stackoverflow.com/questions/10855542/org-hibernate-assertionfailure-null-id-in-entry-dont-flush-the-session-after|mysql - org.hibernate.AssertionFailure: null id in entry (don't flush the Session after an exception occurs) - Stack Overflow]]
 +  * 핵심은 **"동일 트랜잭션 안에 저장이 실패한, 혹은 나도 모르게 직접 쿼리를 통해  DB에서 삭제된 Entity가 EntityManager 1차 캐시에 남아있는 상황"은 존재해서는 안된다.**
 +  * 하나의 트랜잭션에서
 +    * 쓰기 작업시 에러가 발생해서 ''XXX'' 엔티티가 저장이 안됐는데, 이를 ''try/catch'' 블록에서 잡아서 그 후속 작업을 진행했거나,
 +    * 혹은, entity 삭제가 아니 직접 쿼리로 엔티티를 삭제하거나 한 상황에서
 +    * ''Session/EntityManager'' 입장에서는 잘못된 Entity 가 여전히 1차 캐시에 존재하는 상태이기 때문에
 +    * 나중에 flush 가 일어날 때 잘못된 Entity 혹은 DB상에 존재하지도 않지만 기존에 존재했던 것처럼 표시된 Entity 때문에 오류가 발생하게 된다.
 +  * 쓰기 작업과 그 후속 읽기 작업 트랜잭션을 분리해서 ''Session/EntityManager''가 쓰레기 엔티티를 갖지 않게 하거나
 +  * 혹은 직접 쿼리를 통한 삭제 작업의 경우에는 ''session.clear()'' 혹은 ''entityManager.clear()''를 통해 쓰레기 Entity 를 1차 캐시에서 모두 제거해야 한다.
 ===== 참조 ===== ===== 참조 =====
   * [[http://www.devinprogress.info/2011/08/hibernate-gotchas.html|Development in progress...: Hibernate Gotchas!]]   * [[http://www.devinprogress.info/2011/08/hibernate-gotchas.html|Development in progress...: Hibernate Gotchas!]]
   * 기타 직접 겪은 것들을 정리한다.   * 기타 직접 겪은 것들을 정리한다.
java/hibernate/gotchas.1423794756.txt.gz · 마지막으로 수정됨: 2015/02/13 11:32 저자 kwon37xi