JPAstreamerでHibernate/JPAのクエリをJava Streamで表現する
JPAstreamerというオープンソースライブラリを使ってJPA、Hibernate、Springのクエリを表現してみます。
Java Streamは、効率的かつ簡潔で、しかも直感的にロジックをストリームで表現することができます。
3つのコード例から見てみます。
の3つです。
JPA CriteriaBuilder
void pagenation(EntityManager entityManager, int page, int pageSize) { final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); final CriteriaQuery<User> criteriaQuery = criteriaBuilder.createQuery(User.class); final Root<User> root = criteriaQuery.from(User.class); criteriaQuery.select(root); final TypedQuery<User> typedQuery = entityManager.createQuery(criteriaQuery) .setFirstResult((page - 1) * pageSize) .setMaxResults(pageSize); typedQuery.getResultList() .forEach(System.out::println); }
Spring Data JPA
interface UserRepository extends JpaRepository<User, Integer> { @Query("select user from User user") Page<User> findAllPaged(Pageable pageable); } ... @Autowired private UserRepository userRepository; void pagenation(int page, int pageSize) { userRepository .findAllPaged(PageRequest.of((page - 1) * pageSize, pageSize) .forEach(System.out::println); }
JPAstreamer
void pagenation(int page, int pageSize) { jpaStreamer.stream(User.class) .skip((page - 1) * pageSize) .limit(pageSize) .forEach(System.out::println); }
ストリームAPIが提供する宣言型のスタイルは、可読性が向上した短いコードを提供します。
さらに重要なのは、JPAstreamerが型安全性を提供することで、エラーの早期発見、コード品質の向上、時間の節約が可能になることです。
JPAstreamerの概要
Project Lombok などの有名な Java ライブラリと同様に、JPAstreamer はアノテーション・プロセッサを使用してコンパイル時にメタモデルを形成します。
アノテーション・プロセッサは、標準的なJPAアノテーションの@Entityでマークされたクラスを検査し、すべてのエンティティに対して、対応するクラスが生成されます。
生成されたクラスは、Predicate を形成するために使用できるフィールドとして、エンティティの属性を表します。
上記の User の3つの例では、クラスは例えば User$.firstName.startsWith("A") のような形になり、JPAstreamer のクエリオプティマイザーによって解釈されます。
JPAstreamerは、既存のJPAエンティティに基づいてフィールドを生成します。
@Entity @Table(name = "user", schema = "db-name") public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id", nullable = false, columnDefinition = "smallint(5)") private Integer userId; @Column(name = "first_name", columnDefinition = "varchar(255)") private String firstName; @Column(name = "last_name", columnDefinition = "varchar(255)") private String lastName; // … shortened for brevity
JPAstreamerは、既存のコードベースを変更したりすることなく、単にAPIを拡張して、Javaストリーム・クエリを処理するようにします。
さらに、メタモデルはデフォルトで、target フォルダ内の generated sources フォルダに置かれ、他のソース・コードと一緒にテストしたりする必要はありません。
試してみる
JPAstreamerをセットアップするには、Java 8(またはそれ以降)とHibernate、またはEclipseLink、OpenJPA、TopLinkなどのオブジェクト永続化を行うJPAプロバイダーを使用している必要があります。
JPAstreamerはLGPLでライセンスされており、Hibernateも同じライセンスを使用しているため、既存のHibernateプロジェクトで簡単に使用することができます。
インストールについて
JPAstreamerはMaven Central Repositoryで提供されており、既存のMavenやGradleのビルドに1つの依存関係を追加するだけでインストールが完了します。
クエリの作成
Springのユーザーは、例えば@Autowiredアノテーションを使ってDIを行うことができます。
@Autowired Private final JPAStreamer jpaStreamer;
JPAstreamerのインスタンスは、.stream()というメソッドにアクセスすることができ、ストリームしたいエンティティを表すクラスを受け取ります。例えば、上に示したように、Userテーブルは、次のように入力するだけでストリームすることができます。
jpaStreamer.stream(User.class)
これは、Stream
List<String> users = jpaStreamer.stream(User.class) .filter(User$.age.greaterOrEqual(20)) .map(u -> u.getFirstName() + " " + u.getLastName()) .collect(Collectors.toList());
この例は、20歳以上のユーザーの名前をリストにします。
なお、$User は、JPAstreamer のメタモデルの一部である生成されたエンティティを指します。このエンティティは、.filter()や.sort()などの操作のための Predicate や Comparators を形成するために使用されます。
long count = jpaStreamer.stream(User.class) .filter(User$.country.equal("Japan").and(User$.firstName.equal("Watanabe"))) .count();
これは別の例です。日本の渡辺という名前のユーザーをカウントする処理です。
上記のストリームではUserエンティティが1つもJVMに取り込まれることはありません。
代わりに、ストリーム・パイプライン全体がデータベースによって select count(*) where ...で実行され、その後、実際のカウントが直接Javaに返されます。
また、JPAstreamerがストリームを最適化するためには、ラムダではなく、必ずフィールドから派生した述語を使います。つまり、次のようにします。
filter($User.age.greaterOrEqual(20))
ではなく、
filter(u -> u.getAge() >= 20)
です。
Spring Bootで使ってみる
Spring Bootで試してみます。
Maven or Gradle で依存関係の設定を行います。
<dependencies> <dependency> <groupId>com.speedment.jpastreamer</groupId> <artifactId>jpastreamer-core</artifactId> <version>${jpa-streamer-version}</version> </dependency> </dependencies> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>3.2.0</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>${project.build.directory}/generated-sources/annotations</source> </sources> </configuration> </execution> </executions> </plugin> </plugins>
repositories { mavenCentral() } dependencies { compile "com.speedment.jpastreamer:jpastreamer-core:version" annotationProcessor "com.speedment.jpastreamer:fieldgenerator-standard:version" } sourceSets { main { java { srcDir 'src/main/java' srcDir 'target/generated-sources/annotations' } } }
エンティティを追加します。今回は User エンティティを作成します。
@Entity @Table(name = "user") public class User { @Id @GeneratedValue @Column(name = "user_id", nullable = false, updatable = false, columnDefinition = "smallint(5)") private Integer userId; @Column(name = "first_name", nullable = false, columnDefinition = "varchar(45)") private String firstName; @Column(name = "last_name", nullable = false, columnDefinition = "varchar(45)") private String lastName; public Integer getUserId() { return userId; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } }
ビュー用のモデルも追加します。
@JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) public class UserViewModel { private Integer userId; private String firstName; private String lastName; @JsonCreator public UserViewModel( @JsonProperty("userId") final Integer userId, @JsonProperty("firstName") final String firstName, @JsonProperty("lastName") final String lastName ) { this.userId = userId; this.firstName = firstName; this.lastName = lastName; } public Integer getUserId() { return userId; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public static UserViewModel from(final User user) { requireNonNull(user); return new UserViewModel( user.getUserId(), user.getFirstName(), user.getLastName() ); } }
Controller も追加します。
@RestController @RequestMapping("api/v1") public class UserController { private final JPAStreamer jpaStreamer; @Autowired public UserController(JPAStreamer jpaStreamer) { this.jpaStreamer = jpaStreamer; } @GetMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE) public Stream<UserViewModel> getUser() { return jpaStreamer.stream(User.class) .map(UserViewModel::from); } }
最初の行では、データオブジェクトの取得に使用できるJPAstreamerインスタンスを注入しています。次に、user テーブルのデータをリストアップする GetMappingがあります。ユーザーが /users に対して GET リクエストを実行すると、レスポンスはJPAstreamerのStream APIを使用して提供され、データベース・クエリを発行します。
.map(UserViewModel::from)という式は、ビューモデルを使用して、各Userエンティティを、不要なカラムを取り除いた凝縮されたフォーマットに単純にマッピングします。
パフォーマンスはどうなのか
JPAstreamerを使って、「N+1問題」を回避する方法を見てみます。
通常、この問題が発生すると、膨大な数のクエリが発生し、アプリケーションのパフォーマンスが大幅に低下します。
JPAstreamerでは、以下のように、stream configuration objectを提供することで、この問題を回避することができます。
StreamConfiguration<User> configuration = StreamConfiguration.of(User.class)
.joining(User$.city);
jpaStreamer.stream(configuration)
これにより、結果として得られるクエリは city と user を結合することになり、N+1セレクト問題を完全に回避することができます。元のエンティティには、任意の数の Column を結合することができます。