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 を結合することができます。
【Play Framework】Play FrameworkとReactで何か作ってみる Part3【React】
前回の続きです。
ログイン画面を作成し、ログイン処理まで実装しました。
このログイン画面のフォームをReactで生成してみました。
ログイン画面イメージ
ボタンやテキスト間の幅が狭いですが、細かいデザインは一旦無視しています。
メールアドレスとパスワードの入力と、ログイン保持のチェックボックスを配置しています。
ログインボタン下のテキストとリンクは、Reactでは生成していません。
login.scala.html
@import b3.vertical.fieldConstructor @(loginForm: Form[Login], rememberme: Form[Boolean])(implicit messages: Messages, flash: Flash, request: RequestHeader) @main(Messages("Login"), None) { <div class="centeritems mdl-grid"> <div class="mdl-layout-spacer"></div> <div class="mdl-cell mdl-cell--4-col"> <h1>@Messages("LogIn")</h1> @loginForm.globalError.map { error => <div class="alert alert-warning" role="alert"> @error.message </div> } <!-- ここにフォームを描画 --> <div id="signIn"></div> <div> <p>@Messages("AreYouNewUser") <a href="@routes.SignUpController.index()">@Messages("SignUp")</a></p> </div> </div> <div class="mdl-layout-spacer"></div> </div> <script type="text/javascript"> $('#loginSubmit').click(function(){ var label = $('label.mdl-js-checkbox'); var hasClass = label.hasClass('is-checked') if(hasClass){ $('input.mdl-checkbox__input').val(true) } else { $('input.mdl-checkbox__input').val(false) } }); </script> <script type="text/jsx"> var FormApp = React.createClass({ getInitialState: function () { return { data: { email: '', password: '', rememberMe: '' } }; }, handleChange: function (event) { var data = this.state.data; switch (event.target.name) { case 'email': data.email = event.target.value; break; case 'password': data.password = event.target.value; break; case 'rememberMe': data.password = event.target.value; break; } this.setState({ data: data }); }, render: function () { return ( @b3.formCSRF(routes.AuthController.login()) { <div> <div className="mdl-textfield mdl-js-textfield"> <input name="@loginForm("email").id" className="mdl-textfield__input" type="text" value={this.state.email} onChange={this.handleChange} /> <label className="mdl-textfield__label" >@Messages("Email")</label> </div> </div> <div> <div className="mdl-textfield mdl-js-textfield"> <input name="@loginForm("password").id" className="mdl-textfield__input" type="password" value={this.state.password} onChange={this.handleChange} /> <label className="mdl-textfield__label" >@Messages("Password")</label> </div> </div> <div> <div> <label className = "mdl-checkbox mdl-js-checkbox"> <input name="@rememberme("rememberme").id" type = "checkbox" value={this.state.password} onChange={this.handleChange} className = "mdl-checkbox__input" /> <span className = "mdl-checkbox__label">@Messages("RememberMe")</span> </label> </div> </div> <button id="loginSubmit" type="submit" className="mdl-button mdl-js-button mdl-button--raised mdl-button--colored"> @Messages("LogIn") </button> } ); } }); React.render(<FormApp />, document.getElementById('signIn')); </script> }
Playのビューテンプレートの話は省略して、Reactを使用している部分を説明します。
script type="text/jsx" 以下で、フォームを作成し、フォームを描画しています。
Reactの書き方はものすごく汚い状態になってしまっているので改善の余地はありますが、とりあえず、Reactで生成することはできました。
次回も、Reactを使ったビューについて書きたいと思います。
【Play Framework】Play FrameworkとReactで何か作ってみる Part2【React】
前回の続きです。
前回は何を作るかは決めてなかったですが、
Play Framework、Scala、Reactで、TeraTailっぽいものを作ろうかと思います。
現状の画面
簡単なトップページとユーザー登録画面ができています。
フロントエンドは、Material Design Lite(MDL)を使用しています。
Bootstrapは業務で使用しまくりでさすがに飽きたので、MDLを採用しました。
トップページ
【Play Framework】Play FrameworkとReactで何か作ってみる Part1【React】
久しぶりの更新になります。
Reactを勉強したいと思い立ったので、Play FrameworkとReactを合わせて何か作ってみようかと思います。
とりあえず、この記事ではHello World表示するまでやってみます。
作るもの
何も決まってないですが、とりあえず掲示板を作ってみます。
投稿、編集、削除がまで一通り実装してみます。
現時点でReactについては、名前しか知らない状態なので作る過程で少し変わってくるかもです。
準備
build.sbtやらapplication.confやらは置いておいてviewについて以下の通りにやっています
main.scala.html
@(title: String)(content: Html) <!DOCTYPE html> <html lang="en"> <head> <title>@title</title> <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")"> <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")"> <script src="http://fb.me/react-0.13.3.js"></script> <script src="http://fb.me/JSXTransformer-0.13.3.js"></script> </head> <body> @content <script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script> </body> </html>
index.scala.html
@() @main("Home") { <div id="application"></div> <script type="text/jsx"> var HelloWorld = React.createClass({ render: function() { return ( <p>Hello!World!</p> ); } }); var m = React.render(<HelloWorld />, document.getElementById('application')); </script> }
実行
http://localhost:9000/にアクセスすると、Hello!World!が表示されます。
【Scala】Scala&Play Frameworkで掲示板を作ってみる Part3【Play Framework】
久しぶりの更新になります。
今回はModelについて書いていきます。
Message.scala
package models import java.time.ZonedDateTime import scalikejdbc._, jsr310._ import skinny.orm._ case class Message(id: Option[Long], body: String, title: String, createAt: ZonedDateTime, updateAt: ZonedDateTime) object Message extends SkinnyCRUDMapper[Message] { override def tableName = "messages" override def defaultAlias: Alias[Message] = createAlias("m") override def extract(rs: WrappedResultSet, n: ResultName[Message]): Message = autoConstruct(rs, n) private def toNamedValues(record: Message): Seq[(Symbol, Any)] = Seq( 'body -> record.body, 'title -> record.title, 'createAt -> record.createAt, 'updateAt -> record.updateAt ) def create(message: Message)(implicit session: DBSession): Long = createWithAttributes(toNamedValues(message): _*) def update(message: Message)(implicit session: DBSession): Int = updateById(message.id.get).withAttributes(toNamedValues(message): _*) }
createとupdateメソッドは、ヘルパーメソッドです。
とりあえず今回はモデルだけの説明になります。
次回で完成させていきたいと思います。
【Scala】Scala&Play Frameworkで掲示板を作ってみる Part2【Play Framework】
前回の続きです。
今回はルーティングとコントローラーについて、書いていきます。
routes
GET / controllers.Default.redirect(to = "/messages") # メッセージ一覧画面の表示 GET /messages controllers.MessageController.index # メッセージ詳細画面の表示 GET /messages/:id/get controllers.MessageController.showDetail(id: Long) # メッセージ作成画面の表示 GET /messages/create controllers.MessageController.showCreate # メッセージ編集画面の表示 GET /messages/:id/update controllers.MessageController.showUpdate(id: Long) # メッセージの作成 POST /messages/create controllers.MessageController.create # メッセージの更新 POST /messages/update controllers.MessageController.update # メッセージの削除 POST /messages/:id/delete controllers.MessageController.delete(id: Long) GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
ルーティングについては、上記の通りです。
トップページは、
controllers.Default.redirect(to = "/messages")
によって、リダイレクト処理を行っています。
その他は、DBへの読み書きの処理についてのルーティングです。
MessageController
package controllers import java.time.ZonedDateTime import javax.inject.{ Inject, Singleton } import forms.MessageForm import models.Message import play.api.i18n.{ I18nSupport, Messages, MessagesApi } import play.api.mvc.{ Action, AnyContent, Controller } import scalikejdbc.AutoSession @Singleton class MessageController @Inject()(val messagesApi: MessagesApi) extends Controller with I18nSupport with MessageControllerSupport { def index: Action[AnyContent] = Action { implicit request => val result = Message.findAll() Ok(views.html.index(result)) } def showDetail(messageId: Long): Action[AnyContent] = Action { implicit request => val message = Message.findById(messageId).get Ok(views.html.show(message)) } def showCreate: Action[AnyContent] = Action { implicit request => Ok(views.html.create(form)) } def showUpdate(messageId: Long): Action[AnyContent] = Action { implicit request => val result = Message.findById(messageId).get val filledForm = form.fill(MessageForm(result.id, result.body, result.title)) Ok(views.html.edit(filledForm)) } def create: Action[AnyContent] = Action { implicit request => form .bindFromRequest() .fold( formWithErrors => BadRequest(views.html.create(formWithErrors)), { model => implicit val session = AutoSession val now = ZonedDateTime.now() val message = Message(None, model.body, model.title, now, now) val result = Message.create(message) if (result > 0) { Redirect(routes.MessageController.index()) } else { InternalServerError(Messages("CreateMessageError")) } } ) } def update: Action[AnyContent] = Action { implicit request => form .bindFromRequest() .fold( formWithErrors => BadRequest(views.html.edit(formWithErrors)), { model => implicit val session = AutoSession val result = Message .updateById(model.id.get) .withAttributes( 'body -> model.body, 'title -> model.title, 'updateAt -> ZonedDateTime.now() ) if (result > 0) Redirect(routes.MessageController.index()) else InternalServerError(Messages("UpdateMessageError")) } ) } def delete(messageId: Long): Action[AnyContent] = Action { implicit val session = AutoSession val result = Message.deleteById(messageId) if (result > 0) { Redirect(routes.MessageController.index()) } else { InternalServerError(Messages("DeleteMessageError")) } } }
このコントローラーの中で、下記をミックスインしています。
MessageControllerSupport
package controllers import forms.MessageForm import play.api.data.Forms._ import play.api.data._ import play.api.mvc.Controller trait MessageControllerSupport { this: Controller => protected val form = Form( mapping( "id" -> optional(longNumber), "body" -> nonEmptyText, "title" -> nonEmptyText )(MessageForm.apply)(MessageForm.unapply) ) }
次回はモデルについて書いていきたいです。
【Scala】Scala&Play Frameworkで掲示板を作ってみる Part1【Play Framework】
タイトルどおりです。
掲示板アプリを作って、ScalaとPlay Frameworkの勉強です。
概要
簡単な掲示板アプリです。
メッセージの作成、編集、削除ができます。
DBへの書き込みであったり、データの抽出といったところを学習したいと思います。
準備
用意したテーブルは、以下のとおりです。
messagesテーブル
+----+---------+---------------+---------------+---------------+ | id | body | title | create_at | update_at | +----+---------+---------------+---------------+---------------+
build.sbt
MySQL用の依存関係を追加していきます。
libraryDependencies ++= Seq( "org.scalikejdbc" %% "scalikejdbc" % "2.5.2", "org.scalikejdbc" %% "scalikejdbc-config" % "2.5.2", "org.scalikejdbc" %% "scalikejdbc-test" % "2.5.2" % Test, "org.skinny-framework" %% "skinny-orm" % "2.3.7", "org.scalikejdbc" %% "scalikejdbc-play-initializer" % "2.5.+", "ch.qos.logback" % "logback-classic" % "1.2.3", "org.scalikejdbc" %% "scalikejdbc-jsr310" % "2.5.2", "mysql" % "mysql-connector-java" % "6.0.6", "com.adrianhurt" %% "play-bootstrap" % "1.1-P25-B3" )
dev.conf
プロジェクト直下にenvディレクトリを作成し、その中にdev.confファイルを作成します。
そして、以下の通り記述します。
jdbcDriver = "com.mysql.cj.jdbc.Driver" jdbcUrl = "jdbc:mysql://localhost:3306/【DB名】?autoReconnect=true&useSSL=false" jdbcUserName = "【ユーザー】" jdbcPassword = "【パスワード】"
application.conf
データベース接続情報を設定します。
include file("./env/dev.conf") play.modules { enabled += "scalikejdbc.PlayModule" } db { default.driver=${jdbcDriver} default.url=${jdbcUrl} default.username=${jdbcUserName} default.password=${jdbcPassword} } scalikejdbc { global { loggingSQLAndTime.enabled = true loggingSQLAndTime.singleLineMode = true loggingSQLAndTime.logLevel = DEBUG loggingSQLAndTime.warningEnabled = true loggingSQLAndTime.warningThresholdMillis = 5 loggingSQLAndTime.warningLogLevel = warn } }
とりあえず、今回は以上です。
次回は、ルーティングとかを書いていきたいです。