今回の狙い
今回はデータベース接続。Javaなので、H2データベースでも良いのだが、定番での動作を早めに確認しておきたいので、MySQLにつなぐこととする。動作させるコードは、2015年8月投稿記事のPlay Framework (java)で簡単な検索アプリケーションを開発する を使わせていただくこととした。
元ソースコード : https://github.com/rubytomato/actor-search-play-example
この検索アプリactor-search-play-exampleは、Play Framework 2.4.2+MySQL 5.6.25上で実装されている。お試し中のPlay2.5.2とバージョンが近いこともあり、Play2.5固有のconf設定や、ハマりどころを知るにはぴったりと考えた。本アプリのつくりについては、上記事にしっかり解説されているので、そちらを参考にしていただきたい。(特に、今回のPlay2.5記事を追体験したい人は必読)。
前々回に、"Play 2.5で人柱する"的なことを書いたが、リリースノートを斜め読みする限りでは、2.5で導入された新機能は非同期周りのバックエンドやJava 8対応といったところであり、普通に同期的なDBアクセスしているかぎりでは、はまらないものと予想している(...いた)。
動作結果
結論から。appフォルダ以下のソースコードを変更しなくとも、Play2.5で概ね動作した(コンパイル時、ビュー周りに一部非推奨とのワーニングが出た。このあたりは次回以降検証)。
[warn] /Users/Apple/play252/actor-search-play-example/app/views/create.scala.html:25: method get in object Messages is deprecated: see corresponding Javadoc for more information.
[warn] <label for="nameField.id" class="col-sm-2 control-label">@Messages.get("actor.name")</label>
[warn] ^
[warn] /Users/Apple/play252/actor-search-play-example/app/views/create.scala.html:37: method get in object Messages is deprecated: see corresponding Javadoc for more information.
[warn] <label for="heightField.id" class="col-sm-2 control-label">@Messages.get("actor.height")</label>
(以下、たくさん)
...このあたりと関わってか一部にハマりどころがあり、今回は、ネタ的なPlay2.5はまり道その1となった。
順を追って書く。
モデルからデータベースへのマイグレーションを行うツールEvolutionを使ったため、activate runした初回起動時のwebアクセス時にEvolutionの適用(apply)を促す画面が出る。
ボタンをクリックすると、conf/evolutions/default内のsqlのUps部分が実行される(以下に引用)。
create table actor (
id bigint auto_increment not null,
name varchar(30) not null,
height integer,
blood varchar(255),
birthday datetime(6),
birthplace_id integer,
update_at datetime(6) not null,
constraint pk_actor primary key (id))
;
create table prefecture (
id integer auto_increment not null,
name varchar(6) not null,
constraint pk_prefecture primary key (id))
;
Evolutions実行後、Playのモデルに対応するテーブルがDB中に作られ、アプリ動作画面が立ち上がる。initボタンで初期データ投入、dropしてデータ1件消去と、いったあたりは正常に動作している(検証のため、データ名を一部アルファベットに変更してある):
##動作しなかったところ
俳優登録画面がValidationエラーが出て動作しなかった。血液型データが足りないとか俳優ではないとかそういった理由ではもちろんなく、かつ、アルファベットにしても登録出来ない(i18nの問題ではない、か)。
こちらは、以下の"Play2.4と2.5の差分 解説"の中で修正することとしたい。
Play2.4と2.5の差分 解説
2.4のアプリが概ね正常に動作したわけで、コード量としての差分は少ない。
① conf/application.confの書き方
Play2.5では、
## Internationalisation
play.i18n {
langs = [ "ja","en" ]
}
といった風に、共通点を中括弧でくくる書き方がディフォルトとなった。2.4アプリのapplication.confと同様の書き方でも動作はするが、2.5流の中括弧を用いた方が見通しが良いため、こちらを用いることとした。
手厚すぎるコメントを省いたapplication.confの全体は以下:
## Akka
akka {}
## Secret key
play.crypto.secret = "changeme"
## Modules
play.modules {
# If there are any built-in modules that you want to disable, you can list them here.
#enabled += my.application.Module
# If there are any built-in modules that you want to disable, you can list them here.
#disabled += ""
}
## IDE
# Depending on your IDE, you can add a hyperlink for errors that will jump you
# directly to the code location in the IDE in dev mode. The following line makes
# use of the IntelliJ IDEA REST interface:
#play.editor=http://localhost:63342/api/file/?file=%s&line=%s
## Internationalisation
play.i18n {
# The application languages
langs = [ "ja","en" ]
# Whether the language cookie should be secure or not
#langCookieSecure = true
# Whether the HTTP only attribute of the cookie should be set to true
#langCookieHttpOnly = true
}
## Play HTTP settings
# ~~~~~
play.http {
session { }
flash { }
}
## Netty Provider
play.server.netty {}
## WS (HTTP Client)
play.ws {
ssl { }
}
## Cache
play.cache {}
## Filters
play.filters {
cors { }
csrf { }
headers { }
hosts { }
}
## Evolutions
play.evolutions {
# You can disable evolutions for a specific datasource if necessary
# db.default.enabled = false
}
## Database Connection Pool
play.db {
prototype {
# Sets a fixed JDBC connection pool size of 50
#hikaricp.minimumIdle = 50
#hikaricp.maximumPoolSize = 50
}
}
## JDBC Datasource
db {
default.driver=com.mysql.jdbc.Driver
default.url="jdbc:mysql://localhost/play_db?useUnicode=true&characterEncoding=utf8"
default.username=test_user
default.password=passwd
# You can turn on SQL logging for any datasource
#default.logSql=true
}
# これは非推奨になっていると書いてる人もいるが、一応既述(あとで調査)
ebean.default = ["models.*"]
やや長いが、Play2.5のディフォルトでのconf項目が以下のとおりであることは知っておいたほうが良いだろう。
- Akka
- Secret key
- Modules
- IDE
- Internationalisation
- Play HTTP settings
- Netty Provider
- WS (HTTP Client)
- Cache
- Filters
- Evolutions
- Database Connection Pool
- JDBC Datasource
- その他
注意点として、JDBC Datasourceのところでのjdbc接続を"jdbc:mysql://localhost/play_db"とのみ書いた場合、MySQLが文字化けした。そのため、"jdbc:mysql://localhost/play_db?useUnicode=true&characterEncoding=utf8"とutf8決め打ち指定。
あと、起動時に、jdbc周りのワーニングが出ている。
[warn] There may be incompatibilities among your library dependencies.
[warn] Here are some of the libraries that were evicted:
[warn] * com.typesafe.play:play-java-jdbc_2.11:2.4.0 -> 2.5.2
[warn] Run 'evicted' to see detailed eviction warnings
こちらは、とりあえず動作しているので気にしないこととする。
②project/plugins.sbt
基本的には、play pluginのバージョンを2.5.2とするだけ
// The Play plugin
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.2")
その他のプラグインのバージョンは必要に応じ、変更。
③バリデーション周り*(入門記事らしい(?)、ネタ記録)*
バリデーションが動作しなかったため、確認しておく。Play Javaでは、 play.data.validation.ValidationErrorをインポートしたクラスでバリデーションを行う。
動作していないコードはviews/form/ActorForm.javaの以下の部分:
if (name == null || name.length() == 0) {
errors.add(new ValidationError("name", Messages.get("actor.name.required")));
}
...が慣れてないので、何が問題かぱっと見分からない。。
ValidationErrorが発生した際には、conf/messagesに登録されたメッセージが呼び出される(多言語対応等の際にはこのあたりをいじることとなる)。**actor.name.required=**のところを変更すれば表示されるメッセージは変更される。Playはディフォルトで動的更新なので、メッセージは起動中にも変更可能。
...が、なんとなくいいくるめられそうなメッセージにはなったものの、本質的なところは何も変わらない。
仕方ないので、nameのバリデーションコードをコメントアウトして、アクセスする(JavaコードもPlay実行中に動的に変更可能)。...無事、登録できるが、MySQLに、データが投入されていない。
mysql> select * from actor;
+----+--------------+--------+-------+----------------------------+---------------+----------------------------+
| id | name | height | blood | birthday | birthplace_id | update_at |
+----+--------------+--------+-------+----------------------------+---------------+----------------------------+
| 1 | Mr.Tamba | 175 | O | 1922-07-17 00:00:00.000000 | 13 | 2016-04-25 08:47:32.069000 |
| 5 | 山口果林 | NULL | NULL | 1947-05-10 00:00:00.000000 | 13 | 2016-04-25 08:47:32.069000 |
| 6 | 佐分利信 | NULL | NULL | 1909-02-12 00:00:00.000000 | 1 | 2016-04-25 08:47:32.070000 |
| 7 | 緒方拳 | 173 | B | 1937-07-20 00:00:00.000000 | 13 | 2016-04-25 08:47:32.070000 |
| 8 | 松山政路 | 167 | A | 1947-05-21 00:00:00.000000 | 13 | 2016-04-25 08:47:32.070000 |
| 9 | 加藤嘉 | NULL | NULL | 1913-01-12 00:00:00.000000 | 13 | 2016-04-25 08:47:32.070000 |
| 10 | 菅井きん | 155 | B | 1926-02-28 00:00:00.000000 | 13 | 2016-04-25 08:47:32.070000 |
| 11 | 笠智衆 | NULL | NULL | 1904-05-13 00:00:00.000000 | 43 | 2016-04-25 08:47:32.080000 |
| 12 | 殿山泰司 | NULL | NULL | 1915-10-17 00:00:00.000000 | 28 | 2016-04-25 08:47:32.080000 |
| 13 | 渥美清 | 173 | B | 1928-03-10 00:00:00.000000 | 13 | 2016-04-25 08:47:32.080000 |
| 14 | | NULL | | NULL | NULL | 2016-04-25 10:19:36.714000 |
| 15 | | NULL | | NULL | NULL | 2016-04-25 10:20:04.393000 |
+----+--------------+--------+-------+----------------------------+---------------+----------------------------+
データの永続化は、app/controller/Application.javaのsaveメソッドで行っている。
こちらのコードは以下のようになっている。
public Result save() {
logger.info("Application#save");
Form<ActorForm> formData = Form.form(ActorForm.class).bindFromRequest();
if (formData.hasErrors()) {
flash("error", Messages.get("actor.validation.error"));
return badRequest(create.render("retry", formData));
} else {
Actor actor = Actor.convertToModel(formData.get());
Ebean.execute(()->{
SqlRow row = Ebean.createSqlQuery("SELECT MAX(id) AS cnt FROM actor").findUnique();
Long cnt = row.getLong("cnt");
actor.id = cnt == null ? 1L : (cnt + 1L);
actor.save();
});
flash("success", Messages.get("actor.save.success"));
}
return redirect(routes.Application.index());
}
おそらくは、フォームからモデルへのコンバートを行うactor = Actor.convertToModel(formData.get()))のところがうまく動作していないのだろう。とりあえず、データベース投入時のflashメッセージのところをflash("success", Messages.get("actor.save.success")+ actor.toString());と書き換え、データベース投入時のactorの中身を確認する:
予想通りactorの中身はnullだらけ。
Actor.convertToModelメソッドは、app/models/Actor.javaに存在する。
public static Actor convertToModel(ActorForm form) {
Actor actor = new Actor();
actor.id = StringUtils.isNotEmpty(form.id) ? Long.valueOf(form.id) : null;
actor.name = form.name;
actor.height = StringUtils.isNotEmpty(form.height) ? Integer.valueOf(form.height) : null;
actor.blood = form.blood;
actor.birthday = StringUtils.isNotEmpty(form.birthday) ? DateParser.parse(form.birthday) : null;
actor.birthplaceId = StringUtils.isNotEmpty(form.birthplaceId) ? Integer.valueOf(form.birthplaceId) : null;
return actor;
}
何らかの理由で、ActorForm formから、データが取れていないものと思われる。
試しに、name等を決め打ちにしてみる。
this.name = "白井黒子" //name;
this.height = 160 //height;
おっとセミコロンを忘れたので怒られた。
このあたりも、ブラウザのリロード時に動的に指摘してくれる。
気を取り直して、
this.name = "白井黒子"; //name;
this.height = 160; //height;
これで、フォールに入れた名前が司波深雪であろうが、千葉エリカであろうが、データベースには白井黒子しばりでname登録されるはず...なのだが、今回も、nameはlengthが0の文字列が登録されている。
どうやら、Actor.convertToModelあたりの動作に問題があるようだ。こんな基本的なところの動作に躓いくとはおそるべし...とコードを見直すと、まぬけのなことにActorコンストラクタの方を書き換えていた。。
=> Play Javaの(PHP/rubyのWAF並みの)動的側面のイメージ例としてまぬけなところも残しておく。。
気を取り直して、今度はloggerを使ってApplication#save内の動作を確認する。saveメソッド内でのbindFromRequest()呼び出しがきになるので:
public Result save() {
logger.info("Application#save");
Form<ActorForm> formData = Form.form(ActorForm.class).bindFromRequest();
logger.info("初春、bindFromRequest()メソッド後のformDataは" +formData.toString()+"ですのっ");
(略)
...深夜作業が長くなってきたためか、ジャッジメントも出動だ。
コンソールの出力結果:
[info] - controllers.Application - Application#create
[info] - controllers.Application - Application#save
[info] - controllers.Application - 初春、bindFromRequest()メソッド後のformDataは
Form(of=class views.form.ActorForm, data={birthday=, name=婚后光子, birthplaceId=, id=, blood=, height=}, value=Optional[ActorForm [id=, name=, height=, blood=, birthday=, birthplaceId=]], errors={})ですのっ
[info] - controllers.Application - Application#index
おおっ、formData内ではnameが正しく取れている。ということは、saveメソッド内の問題と特定されたわけだ(おそらくは、Actor.convertToModelメソッド)。
Actor.convertToModelメソッドにloggerを埋め込んでみる:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Entity(name = "actor")
public class Actor extends Model {
private static final Logger logger = LoggerFactory.getLogger(Actor.class);
(略)
public static Actor convertToModel(ActorForm form) {
Actor actor = new Actor();
actor.id = StringUtils.isNotEmpty(form.id) ? Long.valueOf(form.id) : null;
if (form.name.length() > 0) {
logger.info("form.nameはこうですの :" +form.name);
} else {
logger.info("初春、どうやら、form.nameに値がセットされていなようですわ。");
}
actor.name = form.name;
logger.info("ちなみに、actor.nameですの :" +actor.name);
}(略)
}
[info] - controllers.Application - Application#create
[info] - controllers.Application - Application#save
[info] - models.Actor - 初春、どうやら、form.nameに値がセットされていなようですわ。
[info] - models.Actor - ちなみに、actor.nameはこうですの :
[info] - controllers.Application - Application#index
見えてきた、直前のformData.get()が怪しいのでlogger出動:
logger.info("白井さん、どうやら、formData.get()が怪しいですね。出力します :" +formData.get());
[info] - controllers.Application - Application#save
[info] - controllers.Application - 白井さん、どうやら、formData.get()が怪しいですね。出力します :ActorForm [id=, name=, height=, blood=, birthday=, birthplaceId=]
やはり。さて、問題の切り分けができたので、コード修正はジャッチメントに引き継ごう:
(略)
import javax.inject.Inject;
import play.data.FormFactory;
public class Application extends Controller {
private static final Logger logger = LoggerFactory.getLogger(Application.class);
@Inject
FormFactory formFactory;
(略)
public Result save() {
logger.info("Application#save");
Form<ActorForm> formData = formFactory.form(ActorForm.class).bindFromRequest();
if (formData.hasErrors()) {
flash("error", Messages.get("actor.validation.error"));
return badRequest(create.render("retry", formData));
} else {
logger.info("白井さん、佐天さん、formFactory導入で問題解決です。" +formData.get()+" えへっ");
(略)
ログ:
[info] - controllers.Application - Application#init
[info] - controllers.Application - Application#index
[info] - controllers.Application - Application#create
[info] - controllers.Application - Application#save
[info] - controllers.Application - 白井さん、佐天さん、formFactory導入で問題解決です。ActorForm [id=, name=佐天涙子, height=160, blood=, birthday=, birthplaceId=] えへっ
[info] - controllers.Application - Application#index
Play2.5でのフォーム周りの変更点 解説
振り返って見るに、 Play2.4 -> Play2.5に移行する方法と解決したエラーたちで既に書かれている通り、今回のはまり道は、Formはファクトリーメソッドを使って作るようになったという変更に依存する模様。
Play2.5のコントローラーでは、formFactoryを呼び出して、フォームを扱うと覚えておこう、昨晩の自分:
import javax.inject.Inject;
import play.data.FormFactory;
...
@Inject
FormFactory formFactory;
...
Form<ActorForm> formData = formFactory.form(ActorForm.class).bindFromRequest();
...なぜかコンパイルエラーが出なかったため、時間外の深夜にloggerフル出勤となってしまった。。
ちなみに、Form.form(ActorForm.class)...のところで呼び出されるFormは、Playにおける他のHTMLビューと同様に、scalaのDSLとして、views/create.scala.html"内に記述されている。
name関連部分のみを引用しておく:
@(message: String, actorForm: Form[views.form.ActorForm])
@import play.i18n._
@import models.Prefecture
@main("Actor Create", "俳優登録") {
..(略)..
<!-- name -->
<div class="form-group">
@defining(actorForm("name")) { nameField =>
<label for="nameField.id" class="col-sm-2 control-label">@Messages.get("actor.name")</label>
<div class="col-sm-10">
<input type="text" id="@nameField.id" class="form-control" name="@nameField.name" value="@nameField.value">
@if(nameField.hasErrors) {
<span class="help-block">@nameField.errors.mkString(", ")</span>
}
</div>
}
</div>
※ create.scala.htmlは、Application.javaのcreateメソッド呼び出し時に出力(render)される:
public Result create() {
logger.info("Application#create");
ActorForm form = new ActorForm();
Form<ActorForm> formData = Form.form(ActorForm.class).fill(form);
return ok(create.render("Actor Create", formData));
}
見た目はほぼHTMLだが、scalaコードが記述可能なテンプレートとなっている。
...冒頭に書いたとおり、HTMLテンプレート周りでもワーニング出まくりだが、力尽きたので、そのあたりは次回以降に。
感想
単に既存ソースを軽く動作させてみるつもりが、エディタ(atom)+ブラウザ(chrome)でがっつりとplayをいじくることとなった。フルスタックなWAFであるPlayはディフォルトでいろいろする準備を整えてくれているので、それに乗ることができれば、はまり道の道中も対話的に楽しめる。
...じつは、playを立ち上げる前の準備で、brew経由で導入したMySQL 5.7の初期設定の方にも相当はまっていたのだが、こちらは近時、いろんな人がブログで書いてくれているので、割愛。
##追記
[Play2.4 -> Play2.5に移行する方法と解決したエラーたち]
(http://qiita.com/skliber/items/e98c70ca5b73180c0cca)を読んでで気づいたが、sbtのバージョンも上げておこう。
- project/build.properties
sbt.version=0.13.8
↓
sbt.version=0.13.11
##追記続き。
どちらかというとMySQL5.7のせいで完全に寝不足なので、ポエムを書いた。
いつかは、Play 2.5で本番サービス!?