Java EE 7 検証環境構築(7) JPA 問い合わせ(1) 名前付きクエリを使ってみる。テストでトランザクションも意識する

  1. Java EE 7 検証環境構築(1) WildFly + JBoss Tools で EARプロジェクトを作成し Arquillian で ユニットテストをグリーンにするところまで
  2. Java EE 7 検証環境構築(2) WildFly に DataSourceを作成
  3. Java EE 7 検証環境構築(3) JPAからMySQLに接続するユニットテストをArquillianで実行
  4. Java EE 7 検証環境構築(4) Java EE での DI(Dependency Injection) および CDI(Contexts and Dependency Injection)をながめる
  5. Java EE 7 検証環境構築(5) JBoss Toolsが生成したサンプルソースのCDIを確認する
  6. Java EE 7 検証環境構築(6) JPA エンティティの作成と挿入
  7. Java EE 7 検証環境構築(7) JPA 問い合わせ(1) 名前付きクエリを使ってみる。テストでトランザクションも意識する
  8. Java EE 7 検証環境構築(8) JPA 問い合わせ(2) 動的クエリとCriteria API を試す
  9. Java EE 7 検証環境構築(9) jBatch 概要をおさえる
  10. Java EE 7 検証環境構築(10) JBoss Tools で作成した EARプロジェクトをJava EE 6 から 7 に変更する
  11. Java EE 7 検証環境構築(11) jBatch用 プロジェクトの作成を行う
  12. Java EE 7 検証環境構築(12) jBatch 簡易サンプル作成と Arquillian でユニットテスト
  13. Java EE 7 検証環境構築(13) jBatch REST サービス経由で実行する
  14. Java EE 7 検証環境構築(14) WildFly の管理をGUIで行う
  15. Java EE 7 検証環境構築(15) WildFly を サービスとして設定する(Windows/Linux)
  16. Java EE 7 検証環境構築(16) WildFly と Apache を mod_jk で連携させる(Widows)

なんやかんやで、JPAを使ってエンティティを挿入することができたので、問い合わせを行う。

1.JPA問い合わせについて

1.1 JPQL (Java Persistence Query Language)

JPQLはリレーショナル・データベースに格納されているエンティティに問い合わせを実行するためにJPAで定義された言語。JPQLの構文はSQLとよく似ていますが、データベースのテーブルを直接操作するのではなく、エンティティオブジェクトを操作します。

JPQL 文法

  • JPAでは、SQLのかわりにJPQLを使用する 。
  • JPQLは動的、静的、ネイティブSQLも実行可能。
  • 静的クエリは、名前付きクエリ(Named Query)ともいい、アノテーション、XMLを使用して定義できる 。
  • JPQLではなく、DBMS ネイティブのSQLを指定することもできる (動的、静的)。

1.2 ネイティブクエリ

JPQLはデータベース間で移植可能で、あらゆる形式のエンティティも扱える多種多様な構文を持っています。しかし、その一方でJPAはデータベース固有の機能を利用できるようにネイティブクエリもサポートしています。ネイティブクエリはデータベース間での移植性は保証しません。JDBC直接呼び出しではなく、JPAのネイティブクエリを利用する主な理由は、クエリの結果がエンティティに自動変換されるためです。

1.3 名前付きクエリ(Named Query)

名前付きクエリは静的で変更できないという点で動的クエリとは異なります。名前付きクエリの静的な性質から、動的クエリのような柔軟性は実現できませんが、永続性プロバイダはアプリケーションの起動時にJPQL文字列をSQLに変換でき、クエリを実行するたびに毎回変換を行わないため、クエリの実行効率が高くなります。

  • 単独なら@NamedQuery、複数の場合@NamedQueriesでまとめる 。
  • または、対応するXMLディスクリプタ内のメタデータで定義する 。
  • 同様に@NamedNativeQuery で、ネイティブクエリも名前付きクエリとして定義できる。(@NamedNativeQueries で複数定義できる) 。
  • 一般的に、クエリ結果に直接対応するエンティティクラスに記述する。
  • クエリ名は永続性ユニットごとにスコープがあり、スコープ内で一意でなければならない(クエリ名の前にエンティティ名をつけるのが一般的)。
  • クエリ名文字列でタイプミスによる問題を軽減するために、クエリ名を定数を置き換えることもできる

2. 試す

ということで、まずは、この辺りまで(JPQL名前付きクエリ、ネイティブ名前付きクエリ)を試してみることにする。

動的JPQL、Criteria API(オブジェクト指向クエリ) などは別途確認する。

2.1 エンティティ

挿入の確認で作成した、エンティティに、名前付きクエリ(JPQL、ネイティブ) をアノテーションで付加する。Book エンティティにクエリ名を定数として持たせるのは若干違和感(Bookエンティティには本質的にクエリ名とか関係ないはず)がないではないが、メリットのほうが大きそうなので、定数化してみる。

@Entity
@NamedQueries({
    @NamedQuery(name=Book.QUERY_FIND_BY_ID,
                query="select b from Book b where b.id = :id"),
    @NamedQuery(name=Book.QUERY_SELECT_ALL,
                query="select b from Book b"),
    @NamedQuery(name=Book.QUERY_SELECT_BY_TITLE,
                query="select b from Book b where b.title like :title")})
@NamedNativeQuery(name=Book.QUERY_SELECT_MORE_EXPENSIVE,
                query="select * from book where price > :price",
                resultClass=Book.class)
public class Book {
    public static final String QUERY_FIND_BY_ID = "Book.findBookById";
    public static final String QUERY_SELECT_ALL = "Book.selectAllBooks";
    public static final String QUERY_SELECT_BY_TITLE = "Book.selectBooksByTitle";
    public static final String QUERY_SELECT_MORE_EXPENSIVE = "Book.selectMoreExpensiveBooks";

    public Book(){}
    
    public Book(String title, Float price, String description){
        this.title = title;
        this.price = price;
        this.description = description;
    }

   :省略
}

2.2 サービスを作成

それぞれの名前付きクエリを呼び出すサービスメソッドを定義してみる。

EntityManager の createNamedQueryは、Query を返し、Query のセッターは自分自身を返す。

メソッドチェイン(Method chaining) はSmalltalkで人気のあったプログラミングスタイルであり、他のJavaプログラミングスタイルに比べて読みにくくデバックがしにくいという批判もある。しかしながら、ほとんどの場合において、これは非常に便利である。 多くのJava開発者はSetterメソッドやadderメソッドは戻り値を返さないという意味でvoid型に宣言する。しかしこのプログラミングスタイルを適用する場合は、戻り値がオブジェクト自身を返すようにsetterメソッドやadderメソッドを宣言することになる。

なので、下記例では、Query のローカル変数を宣言していない。

@Stateless
public class BookService {

    @Inject
    private Logger log;

    @Inject
    private EntityManager em;
    
    public void insertBook(Book book) {
        em.persist(book);
    }
    
    public Book findBookById(Long id) {
        return (Book)em.createNamedQuery(Book.QUERY_FIND_BY_ID)
                .setParameter("id", id)
                .getSingleResult()
                ;
    }
    
    @SuppressWarnings("unchecked")
    public List selectAllBooks() {
        return em.createNamedQuery(Book.QUERY_SELECT_ALL).getResultList();
    }
    
    @SuppressWarnings("unchecked")
    public List selectBooksByTitle(String title) {
        return em.createNamedQuery(Book.QUERY_SELECT_BY_TITLE)
                .setParameter("title", title + "%")
                .getResultList()
                ;
    }
    
    @SuppressWarnings("unchecked")
    public List selectMoreExpensiveBooks(Float price) {
        return em.createNamedQuery(Book.QUERY_SELECT_MORE_EXPENSIVE)
                .setParameter("price", price)
                .getResultList()
                ;
    }
}

2.3 テストケース

Arquilian Java 永続化のテスト を参照する。Spring だと、@Rollback でテストメソッドのトランザクションを制御できた(デフォルトロールバック、@Rollback(false)でコミット)が。。。

@Inject UserTransaction utx; でユーザートランザクションはインジェクトでき(Mavenに依存関係を記述する必要 2.3.1 参照)、@Before が、テストメソッドの前、@After がテストメソッドの後に呼び出されるので、そこで、トランザクションの開始と、ロールバックをしてみる。

一応想定した、メソッドごとにトランザクション開始、ロールバックという動きはするのだが、もう少し簡単にできるとうれしい。

@RunWith(Arquillian.class)
public class BookServiceTest {
       @Deployment
        public static Archive<?> createTestArchive() {
            return ShrinkWrap.create(WebArchive.class, "test.war")
                    .addClass(Resources.class)
                    .addClasses(Book.class,BookService.class)
                    .addAsResource("META-INF/test-persistence.xml", "META-INF/persistence.xml")
                    .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
                    ;
        }

        @Inject
        Logger log;
        
        @Inject
        BookService bookService;

        @Inject
        UserTransaction utx;
        
        @Before
        public void beforeTest() throws Exception {
            utx.begin();
        }
        
        @After
        public void afterTest() throws Exception {
            utx.rollback();
        }
        
        @Test
        public void insertBookTest() {
            Book book = new Book("Insert Test",100f,"Test");
            bookService.insertBook(book);
            log.info(book.toString());
            assertNotNull(book.getId());
        }
        
        @Test
        public void findBookByIdTest() {
            createTestData();

            List allBooks = bookService.selectAllBooks();
            int pos = (int)(Math.random() * 100d) % allBooks.size(); 
            
            Book book1 = allBooks.get(pos);
            log.info(book1.toString());
            
            Book book2 = bookService.findBookById(book1.getId());
            log.info(book2.toString());
            
            assertEquals(book1, book2);
            assertTrue(book1 == book2);
        }
        
        
        @Test
        public void selectBooksByTitleTest() {
            createTestData();
            
            List books = bookService.selectBooksByTitle(SAMPLE_BOOK_TITLE_PREFIX);
            for (Book book : books) {
                log.info(book.toString());
                assertTrue(book.getTitle().startsWith(SAMPLE_BOOK_TITLE_PREFIX));
            }
        }
        
        @Test
        public void selectMoreExpensiveBooksTest() {
            createTestData();
            
            final float BASE_PRICE = 300f;
            
            List books = bookService.selectMoreExpensiveBooks(BASE_PRICE);
            for (Book book : books) {
                log.info(book.toString());
                assertTrue(book.getPrice() > BASE_PRICE);
            }
        }

        private final String SAMPLE_BOOK_TITLE_PREFIX = "SampleBook";
        private void createTestData() {
            for (int i=0; i<5; i++) {
                bookService.insertBook(
                    new Book(
                        String.format("%s%02d", SAMPLE_BOOK_TITLE_PREFIX, i),
                        i * 100f,
                        String.format("Description about %s%02d.", SAMPLE_BOOK_TITLE_PREFIX, i))
                );
            }
        }
}

2.3.1 UserTransaction を利用出来るようにする

pom.xml に以下を追記。

<dependency>
    <groupId>javax.transaction</groupId>
    <artifactId>jta</artifactId>
    <version>1.1</version>
    <scope>test</scope>
</dependency>
<\pre>

2.4 テスト結果

ここまで問題なく実行。

jpa_query_test


Follow me!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です