JPAstreamer とは
Speedment 社が提供している ORM ツールキットに speedment があります。 speedment は、データベース操作を Java Stream API を介して行える ORM ツールキットです。
speedment の ORM 部分に JPA を使うようにしたものが JPAstreamer です。 Java Stream API を介したデータベース操作を JPA 上で実現するライブラリになります。
JPAstreamer でのクエリ操作は以下のようになります。
jpaStreamer.stream(Film.class) .filter(Film$.rating.equal("G")) .sorted(Film$.length.reversed().thenComparing(Film$.title.comparator())) .skip(10) .limit(5) .forEach(System.out::println);
アノテーションプロセッサで Entity に応じたメタモデルを生成。 メタモデルを元に Stream API から JPQL を生成して JPA 上で実行 という流れになります。
アプローチとしては Querydsl と同じ感じですが、Querydsl は Java Stream 風な API を提供するのに対して、JPAstreamer では Java Stream API そのものを使ってクエリ操作が可能です。
Java Stream API そのものを使うため、例えば(現実世界では割と必要になることが多い)サブクエリが使えなかったり、機能面では Querydsl に一日の長があります。
JPAstreamer のバックエンドには JPA が居るため、複雑なクエリは JPA を直接使えば良いという割り切りさえできれば、JPAstreamer は選択肢の一つになるでしょう。
ただし、現時点で JPAstreamer は、名前空間 jakarta.persistence
には未対応であり、旧 javax.persistence
の JPAでしか利用できないので、注意が必要です。
Issue としては Add Support for JPA 3として挙げられています。
同様のライブラリとしては jpa-fluent-query などもあります。まぁ、こちらは開発途中なのですが。
JPAstreamer の始め方
JPAstreamer を利用するには、アノテーションプロセッサも含めて、以下の依存が必要です。
dependencies { implementation("com.speedment.jpastreamer:jpastreamer-core:1.1.3") annotationProcessor("com.speedment.jpastreamer:fieldgenerator-standard:1.1.3") }
バックエンドに Hibernate、 データベースに H2 を使う場合は以下のようになるでしょう。
dependencies { implementation("com.speedment.jpastreamer:jpastreamer-core:1.1.3") annotationProcessor("com.speedment.jpastreamer:fieldgenerator-standard:1.1.3") compileOnly("javax.persistence:javax.persistence-api:2.2") runtimeOnly("org.hibernate:hibernate-core:5.6.15.Final") runtimeOnly("org.glassfish.jaxb:jaxb-runtime:2.3.8") runtimeOnly("com.h2database:h2:2.1.214") }
JPA を直接利用する場合には annotationProcessor("org.hibernate:hibernate-jpamodelgen:5.6.15.Final")
も追加しておくと良いでしょう。
あとは、JPA を利用できるようにするだけです。
JPA の利用準備
サンプルとして Book
と Author
エンティティを作成します。
package example; import javax.persistence.*; @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String isbn; @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Author author; protected Book() {} public Book(String name, String isbn, Author author) { this.name = name; this.isbn = isbn; this.author = author; } // getter ... }
package example; import javax.persistence.*; @Entity public class Author { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String fullName; protected Author() {} public Author(String fullName) { this.fullName = fullName; } // getter ... }
resources/META-INF/persistence.xml
を作成します。
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"> <persistence-unit name="testUnit"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <class>example.Book</class> <class>example.Author</class> <properties> <property name="javax.persistence.schema-generation.database.action" value="create"/> <property name="javax.persistence.schema-generation.create-source" value="metadata"/> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/> <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.show_sql" value="true"/> </properties> </persistence-unit> </persistence>
ビルドすれば JPAstreamer 用のメタモデルとして Book$
と Author$
が生成されます。
Book$
を例に見てみると、以下のようになっています。
public final class Book$ { public static final StringField<Book> name = StringField.create( Book.class, "name", Book::getName, false); public static final ReferenceField<Book, Author> author = ReferenceField.create( Book.class, "author", Book::getAuthor, false); public static final ComparableField<Book, Long> id = ComparableField.create( Book.class, "id", Book::getId, false); public static final StringField<Book> isbn = StringField.create( Book.class, "isbn", Book::getIsbn, false); }
このメタモデルを .filter(Book$.name.equal("..."))
のように使うことで、Java Stream API 上での条件指定を行います。
クエリの実行
事前にクエリするデータを登録しておきます。
EntityManagerFactory emf = Persistence.createEntityManagerFactory("testUnit"); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(new Book("book1", "00", new Author("author1"))); em.persist(new Book("book2", "01", new Author("author1"))); em.persist(new Book("book3", "02", new Author("author2"))); em.getTransaction().commit();
JPAStreamer によるクエリは以下のように実行できます。
JPAStreamer jpaStreamer = JPAStreamer.of("testUnit"); jpaStreamer.stream(Book.class) .filter(Book$.name.startsWithIgnoreCase("bo")) .sorted(Book$.name.reversed()) .map(Book::getName) .forEach(System.out::println);
JPAStreamer
は EntityManagerFactory から JPAStreamer.of(emf)
のように取得することもできます。
以下のような出力が得られます。
book3 book2 book1
この時のSQLは以下が発行されます。
select book0_.id as id1_1_, book0_.author_id as author_i4_1_, book0_.isbn as isbn2_1_, book0_.name as name3_1_ from Book book0_ where lower(book0_.name) like ? order by book0_.name desc
JPAStreamer は、Stream 処理をストリーム・レンダラーにより解析し、等価なJPQLを生成します。
フィルタ条件にメタモデルを使用しない場合、全てのレコードが取得されるため注意してください。
例えば以下のように、book.getName()
を直接使用した場合、
jpaStreamer.stream(Book.class) .filter(book -> book.getName().startsWith("bo")) .sorted(Book$.name.reversed()) .map(Book::getName) .forEach(System.out::println);
SQLは以下となり、フィルタリングは取得したデータ全件に対して Stream API を介して処理されます。
select ... from Book book0_ order by book0_.name desc
Stream API と SQL の対応
JPAStreamer では、Stream 処理をストリーム・レンダラーにより解析し、等価なJPQLを生成します。
この時の対応は以下のようになります。
Stream API | SQL |
---|---|
stream() |
FROM |
filter() (before collecting) |
WHERE |
sorted() |
ORDER BY |
skip() |
OFFSET |
limit() |
LIMIT |
count() |
COUNT |
collect(groupingBY()) |
GROUP BY |
filter() (after collecting) |
HAVING |
distinct() |
DISTINCT |
map() |
SELECT |
concat(s0, s1).distinct() |
UNION |
flatmap() |
JOIN |
Join 処理
JPAStreamer でJOINを処理する場合は、では、StreamConfiguration
を使用する必要があります。
JPAStreamer jpaStreamer = JPAStreamer.of("testUnit"); StreamConfiguration<Book> conf = StreamConfiguration.of(Book.class) .joining(Book$.author); jpaStreamer.stream(conf) .filter(Book$.name.startsWithIgnoreCase("bo")) .sorted(Book$.name.reversed()) .forEach(System.out::println);
以下のような出力となります。
Book{id=1, name='book1', isbn='00', author=Author{id=1, fullName='author1'}} Book{id=2, name='book2', isbn='01', author=Author{id=2, fullName='author1'}} Book{id=3, name='book3', isbn='02', author=Author{id=3, fullName='author2'}}
SQLは以下のように Author が JOIN されていることが分かります。
select book0_.id as id1_1_0_, author1_.id as id1_0_1_, book0_.author_id as author_i4_1_0_, book0_.isbn as isbn2_1_0_, book0_.name as name3_1_0_, author1_.fullName as fullname2_0_1_ from Book book0_ left outer join Author author1_ on book0_.author_id=author1_.id where lower(book0_.name) like ?
これにより N+1 SELECT 問題を回避することができます。
JPAStreamer でのクエリは、リレーションの FetchType により挙動が異なりますので注意が必要です。
ここでの Book.author
は、FetchType.LAZY
と定義しています。
以下のようにクエリ時に book.getAuthor()
とアクセスした場合はエラーとなります。
jpaStreamer.stream(Book.class) .filter(Book$.name.startsWithIgnoreCase("bo")) .forEach(book -> System.out.println(book.getAuthor()));
Book.author
を、FetchType.EAGER
とすれば、エラーは解消されますが、N+1 SELECTとなります。
StreamConfiguration
で JOIN を定義すれば何れの場合でも上手くいきます。
CDI 統合
CDI 環境で JPAStreamer を利用する場合は以下の依存を追加します。
dependencies {
implementation("com.speedment.jpastreamer.integration.cdi:cdi-jpastreamer:1.1.3")
}
以下のように JPAStreamer のインジェクトが有効になります。
@ApplicationScoped public class DummyService { private final JPAStreamer jpaStreamer; @Inject public DummyService(final JPAStreamer jpaStreamer) { this.jpaStreamer = jpaStreamer; } }
Spring 環境でも同様に、インテグレーションが可能です。
dependencies {
implementation("com.speedment.jpastreamer.integration.spring:spring-boot-jpastreamer-autoconfigure:1.1.3")
}
@Autowired
でのインジェクトが可能です。
@Service public class DummyService { private final JPAStreamer jpaStreamer; @Autowired public DummyService(final JPAStreamer jpaStreamer) { this.jpaStreamer = jpaStreamer; } }
まとめ
Java Stream API で JPA を操作できる JPAstreamer の使い方について紹介しました。
Jakarta Persistence の easy to use が話題になることが多い昨今、潔い割り切りで使い易さを提供する JPAstreamer は、Jakarta EE コミュニティに対しても影響を与えるのではないでしょうか。
まぁ、JPAstreamer は今のところ JPA 2.2 までしか対応してないので、プロジェクトで使うかと言えば、使わないのですが。