[JPA/Hibernate] Hibernate 기초

2021. 10. 12. 13:03Spring/JPA

1. Hibernate란?

Hibernate는 객체 지향 프로그래밍과 관계형 데이터베이스 설계의 관점 차이를 해결하여 개발자가 더 객체 지향 프로그래밍에 집중할 수 있도록 해주는 ORM 중 하나이다. Java 진영에서는 JPA가 표준 인터페이스로 있으며 구현체 중 가장 많이 사용되는 것이 Hibernate이다.

2. 환경 설정 파일

gradle 또는 maven 프로젝트는 기본적으로 src/main 디렉터리에서 javaresources 패키지로 나뉘는데 resources에 대부분의 환경 설정 파일 및 정적 파일들이 포함된다. Hibernate의 환경 설정도 resources에서 작성하면 된다. 환경 설정의 대표적인 파일 형식으로 properties와 xml이 있다.

파일 이름은 hibernate.properties, hibernate.cfg.xml로 작성하면 된다. 또는 resources/META-INF 디렉터리 안에 persistence.xml로 작성할 수도 있다. properties와 xml의 차이점은 xml에서는 사용하는 DB와의 매핑 정보 클래스패스를 추가할 수 있다는 점이다.

이제 작성된 파일을 살펴볼 것인데 properties가 읽기에 편하므로 properties로 확인하자. 최근에는 yml이라는 파일 형식도 사용하는데 상당히 괜찮다.

hiberante.dialect=org.hibernate.dialect.H2Dialect
#hibernate.connection.driver_class=org.h2.Driver
#hibernate.connection.url=jdbc:h2:file:./build/music
#hibernate.connection.username=sa
#hibernate.connection.password=

hibernate.hbm2ddl.auto=update
hibernate.show_sql=true
hibernate.format_sql=true

hibernate.current_session_context_class=thread

#hibernate.connection.pool_size=1

#HikariCP
hibernate.hikari.minimumIdle=4
hibernate.hikari.maximumPoolSize=4
hibernate.hikari.idleTimeout=30000
hibernate.hikari.dataSourceClassName=org.h2.jdbcx.JdbcDataSource
hibernate.hikari.dataSource.user=sa
hibernate.hikari.dataSource.password=
  • hibernate.dialect
    • 대부분의 DB는 벤더마다 쿼리 형식이 다르다. 기존에는 DB 환경을 마이그레이션 할 때 해당 DB 쿼리를 전부 수정해야 했다. hibernatedialect 옵션으로 사용할 DB만 지정해주면 자동으로 쿼리를 해당 DB에 맞게 변경해준다.
  • hibernate.connection.driver_class
    • 연결할 DB의 드라이버를 명시한다.
  • hibernate.connection.url
    • 연결할 DB의 url을 명시한다.
  • hibernate.connection.username
    • 연결할 DB 계정의 username을 명시한다.
  • hibernate.connection.password
    • 연결할 DB 계정의 password를 명시한다.
  • hibernate.hbm2ddl.auto
    • Hibernate가 실행될 때 ddl 옵션을 설정한다.
    • create
      • 실행 시 같은 이름의 테이블이 존재하면 drop하고 다시 create한다.
    • create-drop
      • 위의 create와 같으나 프로그램 종료 시 모든 테이블을 drop한다.
    • update
      • 실행 시 변경된 부분만 반영하며 기존 테이블을 수정하지 않는다.
    • validate
      • 엔티티와 테이블이 잘 매핑되었는지만 확인한다.
    • none
      • 아무런 작업도 하지 않는다.
  • hibernate.show_sql
    • Hibernate에서 실행하는 SQL 문을 출력할지 여부를 지정한다. true시 콘솔에 출력된다.
  • hibernate.format_sql
    • show_sql로 출력되는 쿼리를 포매팅해주는 옵션이다. 가독성이 좋아지므로 show_sql을 사용한다면 함께 지정하는 편이 좋다.
  • hibernate.current_session_context_class=thread
    • 이 옵션은 DB와의 세션 컨텍스트에 관련된 옵션이다. 자세한 설명은 글 후반부에 나온다.
  • hibernate.connection.pool_size
    • 커넥션 풀의 크기를 지정한다.

HikariCP

HikariCP는 DB의 커넥션풀을 관리하는 라이브러리이다. 서버 애플리케이션이 DB로 쿼리를 할 때마다 커넥션을 맺고 DB에서 쿼리 실행 후 결과를 리턴하고 커넥션을 종료하는 작업은 복잡할 뿐만 아니라 자원 소모를 많이 하는 작업이다. 이를 해결하기 위해 미리 커넥션을 맺어둔 인스턴스를 만들어놓고 요청이 있을 때 해당 인스턴스를 사용하고 반납하는 방식을 사용하는데 이게 커넥션풀이다. Hibernate에서 HikariCP를 사용하면 Hibernate의 옵션도 아래와 같이 지정할 수 있다.

  • hibernate.hikari.minimumIdle
    • 풀에서 유지해줄 유휴 상태의 커넥션 최소 개수를 설정한다. 최적의 성능과 응답성을 요구한다면 이 값은 설정하지 않는 게 좋다고 hikariCP github에 나와있다.
  • hibernate.hikari.maximumPoolSize
    • 유휴(idle) 상태와 사용 중인 커넥션을 포함하여 풀이 허용하는 최대 크기를 명시한다. 풀이 이 크기에 도달하고 유휴 커넥션이 없을 때 connectionTimeout이 지날 때까지 getConnection() 호출은 블록킹 된다.
  • hibernate.hikari.idleTimeout
    • 풀에서 유휴 상태로 유지시킬 최대 시간 시간을 설정한다. 이 값은 minimumIsdle 값이 maximumPoolSize 값보다 작은 경우에만 동작하도록 되어 있다. 기본값은 600000(10분)이다.
  • hibernate.hikari.connectionTimeout
    • 풀에서 커넥션을 얻을 때 걸리는 최대 시간을 명시한다. 기본값은 30000(30초)이다. 시간 안에 커넥션을 맺지 못하면 예외가 발생한다.
  • hibernate.hikari.dataSourceClassName
    • hibernate.connection.driver_class 와 같은 옵션이다.
  • hibernate.hikari.dataSource.user
    • hibernate.connection.username 와 같다.
  • hibernate.hikari.dataSource.password
    • hibernate.connection.password 와 같다.

xml 파일로 작성하면 아래와 같다.

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-c.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="hibernate-dialect">org.hibernate.dialect.H2Dialect</property>
        <!--
        <property name="hibernate.connection.drive_class">org.h2.Driver</property>
        <property name="hibernate.connection.url">jdbc:h2:file:./build/music</property>
        <property name="hibernate.connection.username">sa</property>
        <property name="hibernate.connection.password"></property>
        -->
        <property name="hibernate.hbm2ddl.auto">update</property>
        <property name="hibernate.show_sql">true</property>
        <property name="hibernate.format_sql">true</property>

        <property name="hibernate.current_session_context_class">thread</property>

        <property name="hibernate.hikari.minimumIdle">4</property>
        <property name="hibernate.hikari.maximumPoolSize">4</property>
        <property name="hibernate.hikari.idleTimeout">30000</property>
        <property name="hibernate.hikari.dataSourceClassName">org.h2.jdbcx.JdbcDataSource</property>
        <property name="hibernate.hikari.dataSource.url">jdbc:h2:file:./build/music</property>
        <property name="hibernate.hikari.dataSource.user">sa</property>
        <property name="hibernate.hikiar.dataSource.password"></property>

        <!-- 사용하는 엔티티를 작성한 xml 클래스패스 지정 -->
        <mapping resource="com/example/demo/Track.hbm.xml"/>
    </session-factory>

</hibernate-configuration>

3. 매핑

Hibernate 매핑은 자바 코드로 작성한 객체와 관계형 데이터베이스의 각종 테이블 간의 관계 및 테이블 스키마 등을 매핑하는 것이다. 각 테이블을 Hibernate를 사용하여 자바 코드로 작성한 클래스를 엔티티(Entity)라고 부른다. 클래스를 엔티티로 만들기 위해서는 클래스에 javax.persistence.Entity 어노테이션을 선언하면 된다. 또는 xml 파일에서 등록할 수도 있다. 엔티티를 작성할 때 주의해야 할 점은 반드시 기본 생성자를 만들어줘야 한다는 것이다. 어플리케이션과 DB 간의 작업을 사용할 때 그 사이에서 사용되는 객체가 엔티티이므로 엔티티 작성은 매우 중요하다. 아래는 엔티티 예제이다.

@Data
@Table(name = "Track")
@Entity
public class Track implements java.io.Serializable {

    @Id
    @Column(name = "TRACK_ID")
    @GenenratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "filePath", nullable = false)
    private String filePath;
    private LocalTime playTime = LocalTime.of(0, 0, 0);
    private LocalDate added = LocalDate.now();
    private short volume = 0;

    public Track() {}

    public Track(String title, String filePath, LocalTime playTime) {
        this.title = title;
        this.filePath = filePath;
        this.playTime = playTime;
    }

    public Track(String title, String filePath, LocalTime playTime, short volume) {
        this.title = title;
        this.filePath = filePath;
        this.playTime = playTime;
        this.volume = volume;
    }
}

LocalTimeLocalDate, LocalDateTime은 Java 8 부터 추가된 자바 시간 형식이다. 이전에는 DateCalendar를 사용해서 시간을 다루었는데 여기에는 여러 문제가 있었기 때문에 Java 8에서 이 문제들을 해결한 패키지가 나왔다. 이것들을 사용할 때는 별다른 어노테이션을 선언하지 않아도 되지만 DateCalendar를 사용하는 경우에는 @Temporal 어노테이션을 지정해주어야 한다. 참고로 Hibernate 5, Java 8 이상 환경일 때만 LocalTime, LocalDtae, LocalDateTime을 사용할 수 있다.

@Temporal(TemporalType.TIME)
private Date playTime;

@Temporal(TemporalType.DATE)
private Date added;

xml로 작성하면 아래와 같다.

<!-- Track.hbm.xml -->
<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class name="com.example.demo.Track" table="Track">
        <meta attribute="class-description">
            Represents a single playable track in the music database.
            @author Jum Elliott (with help from Hibernate)
        </meta>

        <id name="id" type="long" column="TRACK_ID">
            <meta attribute="scope-set">protected</meta>
            <generataor class="native">
        </id>

        <property name="title" type="string" not-null="true"/>

        <property name="filePath" type="string" not-null="true"/>

        <property name="playTime" type="java.time.LocalTime">
            <meta attribute="field-description">Playing time</meta>
        </property>

        <property name="added" type="java.time.LocalDate">
            <meta attribute="field-description">When the track was created</meta>
        </property>

        <property name="volume" type="short" not-null="true">
            <meta attribute="field-description">How loud to play the track</meta>
        </property>
    </class>
</hibernate-mapping>

만약에 xml로 작성한 파일을 java 디렉터리에 포함시킬 것이라면 build.gradle에서 설정을 해주어야 한다. 기본적으로 파일을 읽어들일 때 java 디렉터리에서는 java 파일만 읽기 때문이다. 아래 옵션은 resources 디렉터리에 src/main/java 디렉터리를 추가하는 것이다.

allprojects { 

    sourceSets {
        main.resources.srcDir 'src/main/java'
        test.resources.srcDir 'src/test/java'
    }
}

4. 트랜잭션

Hibernate로 쿼리를 작성하려면 먼저 세션을 생성해야 한다. 이는 하나의 트랜잭션 과정 동안 유지되는 세션이다. 그리고 세션 생성은 Hibernate에서 제공하는 팩토리로 하며 이 팩토리 객체는 싱글턴이다. 즉 하나의 애플리케이션에서 하나의 팩토리 객체만 존재한다.

var sessionFactory = HibernateUtil5.getSessionFactory();

세션 팩토리 객체를 통해 세션을 생성하면 다음으로 트랜잭션의 범위를 지정한다. 먼저 session.beginTransaction()을 통해 트랜잭션의 시작을 알린다. 그리고 쿼리를 작성하고 성공적으로 오류가 없다면 tx.commit()을 통해 실제 DB에 쿼리한다. 만약 작성한 쿼리 중 에러가 발생하면 catch 문에서 tx.rollback()을 통해 해당 트랜잭션을 롤백한다. 그리고 finally 문에서 세션을 종료한다.

여기서 사용되는 세션 팩토리와 세션은 JPA 표준에서 EntityManagerFactoryEntityManager로 매칭된다.

var sessionFactory = HibernateUtil5.getSessionFactory();
var session = sessionFactory.openSession();
Transaction tx = null;

try {
    tx = session.beginTransaction();

    // 쿼리 작성

    tx.commit();
} catch (RuntimeException e) {
    if (tx != null) tx.rollback();
    throw e;
} finally {
    // 세션을 종료하지 않으면 자동으로 종료되지 않아 자원을 계속 사용한다.
    session.close();
}

// 세션을 종료하지 않으면 자동으로 종료되지 않아 자원을 계속 사용한다.
sessionFactory.close();

그리고 우리가 작성하는 쿼리가 바로 session.beginTransaction()tx.commit() 사이에 들어간다. insert 문은 save()를 통해 수행할 수 있다. 예를 들어 예제처럼 Track이라는 엔티티를 저장하려고 한다. 애플리케이션이 실행됐을 때 DB에는 Track이라는 테이블이 생겨있을 것이다. 우리는 이 테이블에 객체를 저장할 것이다. Track 엔티티는 위에서 작성한 것과 같다.

save()session 인스턴스의 메서드이다. 아래와 같이 사용할 수 있다.

import java.time.LocalTime;

import org.hibernate.Transaction;

import com.example.demo.Track;

public class CreateTest {

    public static void main(String[] args) {

        var sessionFactory = HibernateUtil5.getSessionFactory();
        var session = sessionFactory.openSession();
        Transaction tx = null;

        try {
            tx = session.beginTransaction();

            var track = new Track("Dynamite",
                                  "vol2/album610/track02.mp3",
                                  LocalTime.of(00,03,30));

            session.save(track);

            track = new Track("Permission To Dance",
                              "vol2/album611/track01.mp3",
                              LocalTime.of(00,04,31));

            session.save(track);

            track = new Track("My Universe",
                              "vol2/album612/track00.mp3",
                              LocalTime.of(00,05,32));

            session.save(track);

            tx.commit();
        } catch (RuntimeException e) {
            if (tx != null) tx.rollback();
            throw e;
        } finally {
            session.close();
        }

        sessionFactory.close();
    }
}

여기서 save()를 한다고 바로 DB에 쿼리가 발생되는 것이 아니다. JPA는 내부적으로 캐시를 가지고 있고 save()와 같은 쿼리를 저장해놓는다. 그리고 commit()이 수행되면 이때 실제 DB에 쿼리가 수행된다. 실제로 tx.commit()을 주석 처리하고 프로그램을 실행시키면 콘솔에 아무런 쿼리도 발생하지 않는다.

이제 앞에서 넘어간 hibernate.current_session_context_class=thread 에 대한 설명을 하도록 하겠다.

먼저 세션 객체는 2가지 방법으로 만들 수 있다.

  • sessionFactory.openSession()
  • sessionFactory.getCurrentSession()

getCurrentSession() 메서드를 사용하려면 위 옵션을 작성해주어야 하는데 이 메서드는 어떤 역할을 하는 걸까? 바로 한 트랜잭션 동안 하나의 세션으로 유지되도록 도와주는 세션을 만들어준다. 이게 뭐가 좋은 것인지 의문이 들 수 있다. 그전에 세션이 어떤 역할을 하는지부터 알아보자.

세션을 가장 흔하게 접할 수 있는 기능은 바로 로그인 유지 기능이다. HTTP는 기본적으로 stateless하기 때문에 클라이언트와 서버 간에 정보를 유지시킬 수 없다. 그래서 특별한 기능 없이는 로그인을 하더라도 해당 서비스의 다른 페이지에 접속하면 바로 로그인이 해제된다. 이러한 현상을 막기 위해 서버에 세션값이라는 클라이언트마다 구분되는 값을 저장해두고 요청 시 쿠키값을 확인해서 서버에 저장된 해당 세션값과 매핑되는 사용자는 로그인한 상태로 유지하도록 할 수 있는 것이다. 즉 세션은 한 작업을 수행하면서 각각의 클라이언트를 구분하기 위한 컨텍스트라고 볼 수 있다. DB에서 세션은 한 트랜잭션 동안 클라이언트를 구분하기 위한 컨텍스트라고 할 수 있다. 그래서 트랜잭션을 사용하기 위해서는 세션이 필요로 하는 것이다. 세션은 당연히 자원을 소모한다. 따라서 하나의 트랜잭션에는 하나의 세션만 사용하는 것이 좋다. 그러나 openSession()으로 생성하다 보면 close()를 하지 않아 자원을 계속 쓰는 세션이 남을 수 있다. 이는 실수로 발생할 수 있는 확률이 높다

(꼭 하나씩 빼먹는다)

. 그렇기 때문에 트랜잭션 종료 시에 자동으로 세션을 종료하는 getCurrentSession()을 사용하는 편이 바람직하고 이를 위해서는 위 옵션을 thread로 설정해야 하는 것이다.

openSession()을 통해서 얻은 세션은 트랜잭션 시작 후 다시 openSession()으로 세션을 생성하면 다른 세션이 생성된다. 그러나 getCurrentSession()으로 생성된 세션은 트랜잭션이 종료되기 전까지 동일한 세션 객체를 리턴한다.

Session session1 = sessionFactory.openSession();
Transaction tx = session1.beginTransaction();
Session session2 = sessionFactory.openSession();
(session1 == session2); // false
Session session1 = sessionFactory.getCurrentSession();
Transaction tx = session1.beginTransaction();
Session session2 = sessionFactory.getCurrentSession();
(session1 == session2); // true

그리고 openSession()으로 생성한 세션의 경우 트랜잭션이 종료되더라도 세션은 종료되지 않는다. 그러나 getCurrentSession()은 트랜잭션 종료 시 세션도 자동으로 종료된다. 따라서 openSession()으로 만든 세션을 사용할 때는 위에서처럼 반드시 finally 문에 session.close()를 작성해주어야 한다. 그렇지 않으면 불필요한 자원을 계속 소모하는 세션이 남게 된다. getCurrentSession()으로 만든 세션은 자동 종료되기 때문에 session.close()를 하게 되면 에러가 발생한다.

5. 객체 상태

JPA/Hibernate를 사용할 때 객체는 4가지 상태로 구분된다.

transient(비영속)

객체가 막 생성된 상태이다. 즉 순수한 자바의 인스턴스로만 존재하는 상태이다.

persistence(영속)

객체가 영속성 컨텍스트에 등록된 상태이다. 즉 영속성 컨텍스트에서 관리하는 객체가 된 상태이다.

transient 또는 detached 상태의 객체를 persistence로 만들기 위해서는 아래와 같은 메서드를 사용한다.

  • persist(Entity)
  • save(Entity)
  • saveOrUpdate(Entity)
    • transient 또는 detached 객체를 인자로 받고 그 객체를 persistence로 변경한다. transient 객체라면 save(), detached 객체라면 update()가 실행된다.

JPA에는 persist()만 존재한다.

detached(준영속)

객체가 영속성 컨텍스트에서 분리되어 관리되지 않는 상태이다.

persistence 객체를 detached로 만들려면 아래와 같은 메서드를 사용한다.

  • evict(Entity)
    • 특정 엔티티를 detached 상태로 변경한다.
  • clear()
    • 현재 세션의 모든 엔티티를 detached 상태로 변경한다.
    • 즉 영속성 컨텍스트에 저장된 모든 데이터를 준영속 상태로 변경한다.

JPA에는 evict() 대신 detach() 가 있다.

detached 객체를 다시 persistence로 만들려면 아래와 같은 메서드를 사용한다.

  • merge(Entity)
    • 기존 엔티티를 인자로 받은 엔티티로 덮어쓴다. 기존 엔티티가 없으면 영속성 컨텍스트에 등록한다.
  • update(Entity)
    • detached 객체의 식별자를 사용해서 persistence 객체로 변경한다. 이때 이미 같은 식별자로 영속성 컨텍스트에 등록된 객체가 있다면 예외가 발생한다.

JPA에는 merge()만 존재한다.

merge()update()로 객체를 수정하기 보다는 영속성 컨텍스트의 변경 감지(dirty checking)를 활용하는 편이 바람직하다.

remove(삭제)

객체가 영속성 컨텍스트와 DB에서 삭제된 상태이다. 물론 바로 DB에서 삭제되는 것은 아니고 delete()flush()되어야 한다.

  • delete(Entity)
    • 해당 엔티티를 삭제한다.

delete 객체를 다시 persistence로 만들려면 persist()를 사용한다. 객체가 복구되는 것은 아니고 인자로 넘어온 인스턴스를 영속성 컨텍스트에 등록하는 것과 같다.

JPA에서 remove()와 같다.

6. 단일 쿼리

JPA/Hibernate에서 단일 쿼리는 작성하는 방법은 다음과 같다.

  1. session.get(entity.class, id)
  2. session.load(entity.class, id)

두 방법 모두 DB에서 한 개의 객체를 가져오는 방법으로 사용되지만 차이점이 있다. get()은 직접 DB를 조회하여 바로 값을 가져온다. 그리고 이때 값이 없으면 null을 반환한다. 반면 load()는 실행 시 해당 클래스의 프록시 객체를 만들고 참조시켜놓는다. 이때 해당 객체가 없다면 ObjectNotFoundException()을 발생시킨다. 그리고 나서 해당 엔티티의 값을 조회할 때(e.g. track.getName() 실행 시) DB에 쿼리하고 그 값을 영속성 컨텍스트에 등록한다. 이런 방식을 지연 로딩(lazy loading)이라고 한다. 각각에 메서드는 JPA에서 find(), getReference()로 사용할 수 있다.

7. 다중 쿼리

단일 쿼리는 한 객체만을 조회하는 쿼리이다. 여러 데이터를 조회하고 싶으면 어떻게 할까? JPA/Hibernate에서는 이를 위해 JPQL/HQL이라는 객체 쿼리 언어를 지원한다. 사용법은 간단하다. session.createQuery()를 사용하면 된다.

import java.util.List;

import org.hibernate.Transaction;

import com.example.demo.Track;

public class QueryTest {

    public static void main(String[] args) {

        var sessionFactory = HibernateUtil5.getSessionFactory();
        var session = sessionFactory.openSession();
        Transaction tx = null;

        try {

            tx = session.beginTransaction();

            List<Track> tracks = session.createQuery("from Track t").getResultList();
            for (Track track : tracks) {
                System.out.printf("Track(name: %s)\n", track.getName());
            }

            tx.commit();
        } catch (RuntimeException e) {
            if (tx != null) tx.rollback();
            throw e;
        } finally {
            session.close();
        }

        sessionFactory.close();
    }
}

조회한 결과는 getResultList() 또는 list()로 가져올 수 있다.

여러 가지 절도 추가할 수 있다.

public static List trackNoLongerThan(Time length, Session session) {
    Query query = session.createQuery("from Track as t " +
                                      "where t.palyTime <= ?");
    query.setParameter(0, length, Hibernate.TIME);
    return query.list();
}

위와 같이 ?로 나타내면 query.setParamter()를 통해서 파라미터를 지정해줄 수 있다.

  • setParameter(순서(오름차순), 넣을 값, 타입)

8. NamedQuery

JPA/Hibernate에서는 엔티티에 쿼리를 지정해놓고 메서드에서 사용할 수 있다.

많이 쓰이는 방법은 아니다.

@NamedQuery(
    name = "com.example.demo.tracksNoLongerThan",
    query = "from Track as t where t.playTime <= :length"
)
@Table(name = "TRACK")
@Entity
public class Track implements Serializable {
    // ...
public class QueryTest {

    public static List<Track> tracksNoLongerThan(LocalTime length, Session session) {
        Query<Track> query = 
                            session.createNamedQuery("com.example.demo.tracksNoLongerThan",
                                                                                Track.class);
        query.setParaemter("length", length);
        return query.list();
    }
}

장점

  • 컴파일 시 SQL 문법 오류를 잡아낼 수 있다.

9. 매핑 파일에 쿼리 등록하기

Track.hbm.xml 파일에 쿼리를 등록해놓을 수도 있다.

<query name="com.example.demo.tracksNoLongerThan">
    <![CDATA[
        from Track as track
        where track.playTime <= :length 
      ]]>
</query>

사용 방법은 NamedQuery와 같다. 이렇게 작성하면 쿼리 작성 시 띄어쓰기와 같은 실수를 줄여줄 수 있다.