概要
PlayframeworkとJavaで一覧検索&CRUDするサンプルアプリケーションをさっくり作成します。
完成予定図
環境
- Windows10 64bit
- Java version 1.8.0_92
- Eclipse 4.6.0(Neon)
- Playframework 2.5.8
- Ebean 3.0.2
- Lombok 1.16.10
※DBは今回はH2 Databaseでいきます。
ソースコード
※説明では同じソースを複数回参照します。全体像が知りたいという人は直接上記レポジトリを参照して下さい。
開発
事前準備
Windows/JDK/Playframework/Eclipse/Lombokのインストールは省略します。
プロジェクトの作成
$ activator new play-java8-sample
$ cd play-java8-sample
→ 「5) play-java」を選択します。
不要ファイルの削除
生成された雛形プロジェクトにはサンプルアプリケーションのソースが入っているため、整理します。
下記のファイル・ディレクトリを削除します。
- libexec/
- bin/
- app/
- test/
- public/javascripts/*.js
- public/stylesheets/*.css
下記のディレクトリを再作成します。
- app/models/
- app/views/
- app/controllers/
- app/tables/
- app/assets/javascripts
- app/assets/stylesheets
- test/
routesファイルの内容を減らします。
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
Ebean使用の準備
project/plugins.sbt の末尾に、下記の内容を追記します。
(コメントに書かれている内容を転記)
addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "3.0.2")
build.sbtのプロジェクト設定でPlayEbeanのプラグインを有効化します。
lazy val root = (project in file(".")).enablePlugins(PlayJava)
.enablePlugins(PlayEbean) // この行を追加
application.confにEbeanの設定を追加します。
db {
default.driver = org.h2.Driver
default.url = "jdbc:h2:mem:play"
default.username = sa
default.password = ""
}
ebean.default = "tables.*"
※playの初期設定ではEbeanのエンティティクラスは「models」パッケージに置くようになっていますが、個人的な好みで「tables」パッケージに置くようにしています。
Lombokのライブラリ追加
build.sbtへ、Lombokのライブラリを追加します。1
libraryDependencies += "org.projectlombok" % "lombok" % "1.16.10"
LESSの設定
build.sbtへ、LESSの設定を追記します。
// LESS compile setting
includeFilter in (Assets, LessKeys.less) := "*.less"
excludeFilter in (Assets, LessKeys.less) := "_*.less"
Eclipseプロジェクト作成
project/plugins.sbt の末尾に、下記の内容を追記します。2
// Eclipse
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0")
コマンドを入力して.projectを生成します。
$ activator update
$ activator "eclipse with-source=true"
※初回はダウンロードに時間がかかります。気長に待ちましょう。
※他のライブラリを追加するなどした場合は、都度、上記のコマンドを再実行します。(Eclipseの再起動は必要ありません)
出力フォルダがほしいので、一旦コンパイルします。
$ activator compile
Eclipseで、ビルドパスのクラス・フォルダーに、「target/scala-2.11/classes」を追加します。
(テンプレートやroutesなど、Scalaでコンパイルされたクラスを参照するため)
アプリケーションの開発(概要)
最終的なディレクトリ/ファイル構成はこんな感じです。(意識しなくてもいいものは省いています)
│
│ build.sbt
│
├─app
│ ├─assets
│ │ ├─javascripts
│ │ │ main.coffee
│ │ │
│ │ └─stylesheets
│ │ main.less
│ │
│ ├─controllers
│ │ Application.java
│ │ UserController.java
│ │
│ ├─models
│ │ UserForm.java
│ │ UserItem.java
│ │
│ ├─tables
│ │ │ BaseTable.java
│ │ │ FindDecorator.java
│ │ │ T_User.java
│ │ │
│ │ └─find
│ │ All.java
│ │ Fuzzy.java
│ │ Where.java
│ │
│ └─views
│ index.scala.html
│ main.scala.html
│ user.scala.html
│
├─conf
│ │ application.conf
│ │ logback.xml
│ │ routes
│ │
│ └─evolutions
│ └─default
│ 1.sql
│ 2.sql
│
└─project
│ build.properties
└ plugins.sbt
デバッグ起動
ソースの変更があった箇所を自動コンパイルするモードでサーバを起動しておきます。
コンパイルエラーもリアルタイムで表示してくれるので、この状態で開発を進めていきます。
$ activator "~run"
デフォルトのポート番号は「9000」番ですが、セキュリティソフトなどの関係で使用できない場合は下記のようにポート番号を指定します。
$ activator "~run -Dhttp.port=59000"
アプリケーションの開発(検索機能)
View
画面と、画面まで行き着くためのコントローラーのハリボテを作成します。
画面
main.scala.html
@(title: String)(bodyContent: Html)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0" />
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.7/cerulean/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("stylesheets/main.css")" />
<title>@title</title>
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="navbar-header">
<a class="navbar-brand" href="@routes.Application.index()">サンプルアプリケーション</a>
</div>
<div class="collapse navbar-collapse">
</div>
</nav>
@bodyContent
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script type="text/javascript" src="@routes.Assets.versioned("javascripts/main.js")"></script>
</body>
</html>
index.scala.html
@()
@views.html.main("一覧") {
<div class="container">
<div class="row">
<div class="page-header">
<div class="input-group col-sm-4">
<input type="text" class="form-control" placeholder="名前">
<a class="btn input-group-addon">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
検索
</a>
</div>
</div>
<div>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>id</th>
<th>名前</th>
<th>学年</th>
<th>身長</th>
<th>誕生日</th>
<th>好きな食べ物</th>
<th>作成日時</th>
<th>更新日時</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>高海 千歌</td>
<td>2年生</td>
<td>157cm</td>
<td>8月1日</td>
<td>みかん!</td>
<td>9999/99/99 99:99:99</td>
<td>9999/99/99 99:99:99</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
}
main.less
body {
padding-top: 60px;
}
コントローラー
controllers.Application
※まだ、画面を表示するためのハリボテです。
package controllers;
import javax.inject.Singleton;
import play.mvc.Controller;
import play.mvc.Result;
@Singleton
public class Application extends Controller {
public Result index() {
return ok(views.html.index.render());
}
}
ルーティング
routesへ、「/」へのリクエストのルーティングを追加します ※2回目
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
GET / controllers.Application.index()
この時点での画面
Table
DB関係の共通部品と、テーブルに対応するエンティティの作成を行います。
テーブル検索のための共通部品
tables.BaseTable
package tables;
import java.util.Date;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
import com.avaje.ebean.Model;
import com.avaje.ebean.annotation.CreatedTimestamp;
import com.avaje.ebean.annotation.UpdatedTimestamp;
@MappedSuperclass
public abstract class BaseTable extends Model {
@CreatedTimestamp
public Date createdAt;
@Version
@UpdatedTimestamp
public Date updatedAt;
}
tables.find.Where
package tables.find;
import com.avaje.ebean.ExpressionList;
public interface Where<TABLE> {
ExpressionList<TABLE> where();
}
tables.find.All
package tables.find;
import java.util.List;
public interface All<TABLE> extends Where<TABLE> {
default List<TABLE> all() {
return where().findList();
}
}
tables.find.Unique
package tables.find;
import java.util.List;
import com.avaje.ebean.ExpressionList;
public interface Fuzzy<TABLE> extends Where<TABLE> {
default List<TABLE> fuzzy(String value, String... columnNames) {
ExpressionList<TABLE> ex = where();
for (String c : columnNames) {
ex = ex.like(c, "%" + value + "%");
}
return ex.findList();
}
}
エンティティ
tables.T_User
package tables;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import play.data.validation.Constraints.Required;
import tables.find.All;
import tables.find.Fuzzy;
@Entity
@Table(name = "t_user")
public class T_User extends BaseTable {
public static class Find extends FindDecorator<Long, T_User>
implements All<T_User>, Fuzzy<T_User> {}
public static Find find = new Find();
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Required
public String name;
public Integer schoolYear;
public Date birthDay;
public Integer height;
public String food;
public boolean isEmpty() {
return id == null;
}
}
※クラス変数「find」はテーブル検索用のヘルパーです。FindDecoratorを継承し、tables.findパッケージから使用したい機能をMix-inする、という使い方をします。
Model
画面とやり取りをするモデル(検索フォーム・検索結果)を定義します。
検索結果
models.UserItem
package models;
import java.util.Date;
import lombok.Getter;
import lombok.NoArgsConstructor;
import play.data.format.Formats;
import tables.T_User;
@Getter
@NoArgsConstructor
public class UserItem {
Long id;
String name;
String schoolYear;
@Formats.DateTime(pattern = "MM月dd日")
Date birthDay;
String height;
String food;
@Formats.DateTime(pattern = "yyyy/MM/dd HH:mm:SS")
Date createdAt;
@Formats.DateTime(pattern = "yyyy/MM/dd HH:mm:SS")
Date updatedAt;
public UserItem(T_User user) {
this.id = user.id;
this.name = user.name;
this.schoolYear = user.schoolYear + "年生";
this.birthDay = user.birthDay;
this.height = user.height + "cm";
this.food = user.food;
this.createdAt = user.createdAt;
this.updatedAt = user.updatedAt;
}
}
検索フォーム
models.UserForm
package models;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import tables.T_User;
public class UserForm {
public String searchWord;
public List<UserItem> search() {
List<T_User> list;
if (StringUtils.isBlank(searchWord)) {
list = T_User.find.all();
} else {
list = T_User.find.fuzzy(searchWord, "name");
}
return list.stream()
.map(UserItem::new)
.collect(Collectors.toList());
}
}
ViewとController
モデルをやり取りするように、画面とコントローラーを実装します。
画面
index.scala.html ※2回目
全体の引数・検索フォーム・明細行(TR要素)の編集部分を変更しています。
@(userForm: Form[UserForm], items: List[Form[UserItem]])
@views.html.main("一覧") {
<div class="container">
<div class="row">
<div class="page-header">
@helper.form(action = routes.Application.index()) {
<div class="form-inline">
<div class="input-group col-sm-4">
<input type="text" class="form-control" placeholder="名前" name="searchWord" value="@userForm("searchWord").value">
<span class="input-group-btn">
<button type="submit" class="btn btn-default">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
検索
</button>
</span>
</div>
</div>
}
</div>
<div>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>id</th>
<th>名前</th>
<th>学年</th>
<th>身長</th>
<th>誕生日</th>
<th>好きな食べ物</th>
<th>作成日時</th>
<th>更新日時</th>
</tr>
</thead>
<tbody>
@items.map { field =>
<tr>
<td>@field("id").value</td>
<td>@field("name").value</td>
<td>@field("schoolYear").value</td>
<td>@field("height").value</td>
<td>@field("birthDay").value</td>
<td>@field("food").value</td>
<td>@field("createdAt").value</td>
<td>@field("updatedAt").value</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
コントローラー
controllers.Application ※2回目
モデルを使った検索処理を追加
package controllers;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import models.UserForm;
import models.UserItem;
import play.data.Form;
import play.data.FormFactory;
import play.mvc.Controller;
import play.mvc.Result;
@Singleton
public class Application extends Controller {
private Form<UserForm> forms;
private Form<UserItem> itemForms;
@Inject
public Application(FormFactory formFactory) {
this.forms = formFactory.form(UserForm.class);
this.itemForms = formFactory.form(UserItem.class);
}
public Result index() {
Form<UserForm> requestForm = forms.bindFromRequest();
if (requestForm.hasErrors()) {
return badRequest(views.html.index.render(requestForm, new ArrayList<>()));
}
UserForm form = requestForm.get();
List<UserItem> items = form.search();
return ok(views.html.index.render(forms.fill(form), fill(items)));
}
private List<Form<UserItem>> fill(List<UserItem> items) {
return items.stream()
.map(itemForms::fill)
.collect(Collectors.toList());
}
}
初期データの投入
evolutions
アプリケーション起動時に実行するSQL文をevolutionsへ追加します。
# --- !Ups
INSERT INTO t_user
(
name,
school_year,
birth_day,
height,
food,
created_at,
updated_at
)
VALUES
(
'高海 千歌',
2,
DATE'2003-08-01',
157,
'みかん!',
TIMESTAMP'1900-01-01 00:00:00',
TIMESTAMP'1900-01-01 00:00:00'
),
(
'桜内 梨子',
2,
DATE'2003-09-19',
160,
'ゆでたまご・サンドイッチ',
TIMESTAMP'1900-01-01 00:00:00',
TIMESTAMP'1900-01-01 00:00:00'
),
(
'松浦 果南',
3,
DATE'2003-02-10',
162,
'さざえ・わかめ',
TIMESTAMP'1900-01-01 00:00:00',
TIMESTAMP'1900-01-01 00:00:00'
),
(
'黒澤 ダイヤ',
3,
DATE'2003-01-01',
162,
'抹茶味のお菓子・プリン',
TIMESTAMP'1900-01-01 00:00:00',
TIMESTAMP'1900-01-01 00:00:00'
),
(
'渡辺 曜',
2,
DATE'2003-04-17',
157,
'ハンバーグ・みかん',
TIMESTAMP'1900-01-01 00:00:00',
TIMESTAMP'1900-01-01 00:00:00'
),
(
'津島 善子',
1,
DATE'2004-07-13',
156,
'チョコレート・苺',
TIMESTAMP'1900-01-01 00:00:00',
TIMESTAMP'1900-01-01 00:00:00'
),
(
'国木田 花丸',
1,
DATE'2005-03-04',
152,
'みかん・あんこ',
TIMESTAMP'1900-01-01 00:00:00',
TIMESTAMP'1900-01-01 00:00:00'
),
(
'小原 鞠莉',
3,
DATE'2002-06-13',
163,
'コーヒー・レモン',
TIMESTAMP'1900-01-01 00:00:00',
TIMESTAMP'1900-01-01 00:00:00'
),
(
'黒澤 ルビィ',
1,
DATE'2004-09-21',
154,
'ポテトフライ・スイートポテト',
TIMESTAMP'1900-01-01 00:00:00',
TIMESTAMP'1900-01-01 00:00:00'
);
# --- !Downs
delete from t_user;
この時点での画面
アプリケーションの開発(登録機能)
次に、ユーザーの登録・編集・削除の機能を追加実装します。
新規画面
画面
user.scala.html
登録・編集・削除のための新画面です。
@(userForm: Form[tables.T_User], isEdit: Boolean, message: String)
@id = @{userForm("id").value.toLong}
@action = @{
if (isEdit) {
routes.UserController.update(id)
} else {
routes.UserController.create()
}
}
@views.html.main("一覧") {
<div class="container">
<div class="row">
@if(message != null) {
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="閉じる"><span aria-hidden="true">×</span></button>
@message
</div>
}
<div class="col-lg-12">
@helper.form(action = action) {
<input type="hidden" value="@userForm("id").value" name="id">
@helper.inputText(userForm("name"),
'_label -> "名前")
@helper.inputText(userForm("schoolYear"),
'_label -> "学年")
@helper.inputText(userForm("birthDay"),
'placeholder -> "yyyy-MM-dd",
'_label -> "誕生日")
@helper.inputText(userForm("height"),
'_label -> "身長")
@helper.inputText(userForm("food"),
'_label -> "好きな食べ物")
<input type="submit" class="btn btn-primary" value="登録" />
}
</div>
</div>
@if(isEdit) {
<hr>
<div class="row">
<div class="col-lg-12">
@helper.form(action = routes.UserController.delete(id)) {
<input type="submit" class="btn btn-danger" value="削除" />
}
</div>
</div>
}
</div>
}
コントローラー
ルーティング
routesへ、/user関連のルーティングを追加します。 ※3回目
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
GET / controllers.Application.index()
GET /user controllers.UserController.init()
GET /user/:id controllers.UserController.edit(id:Long)
POST /user controllers.UserController.create()
POST /user/:id controllers.UserController.update(id:Long)
POST /user/delete/:id controllers.UserController.delete(id:Long)
コントローラー
controllers.UserController
package controllers;
import javax.inject.Inject;
import javax.inject.Singleton;
import play.data.Form;
import play.data.FormFactory;
import play.db.ebean.Transactional;
import play.mvc.Controller;
import play.mvc.Result;
import tables.T_User;
@Singleton
public class UserController extends Controller {
private Form<T_User> forms;
@Inject
public UserController(FormFactory formFactory) {
this.forms = formFactory.form(T_User.class);
}
public Result init() {
T_User user = new T_User();
return ok(views.html.user.render(forms.fill(user), false, null));
}
public Result edit(Long id) {
T_User user = T_User.find.byId(id).orElse(new T_User());
if (user.isEmpty()) {
return redirect(routes.UserController.init());
}
return ok(views.html.user.render(forms.fill(user), true, null));
}
@Transactional
public Result create() {
return insertOrUpdate(false);
}
@Transactional
public Result update(Long id) {
return insertOrUpdate(true);
}
private Result insertOrUpdate(boolean isEdit) {
Form<T_User> requestForm = forms.bindFromRequest();
if (requestForm.hasErrors()) {
return badRequest(views.html.user.render(requestForm, isEdit, null));
}
T_User user = requestForm.get();
if (user.isEmpty()) {
user.save();
} else {
user.update();
}
return ok(views.html.user.render(forms.fill(user), true, "保存しました"));
}
@Transactional
public Result delete(Long id) {
System.out.println("DELETE:" + id);
T_User user = T_User.find.byId(id).orElse(new T_User());
if (user.isEmpty()) {
return badRequest("delete error");
}
user.delete();
return redirect(routes.Application.index());
}
}
既存画面の修正
一覧画面に、新規画面へのリンクを追加
index.scala.html ※3回目
@(userForm: Form[UserForm], items: List[Form[UserItem]])
@views.html.main("一覧") {
<div class="container">
<div class="row">
<div class="page-header">
@helper.form(action = routes.Application.index()) {
<div class="form-inline">
<div class="input-group col-sm-4">
<input type="text" class="form-control" placeholder="名前" name="searchWord" value="@userForm("searchWord").value">
<span class="input-group-btn">
<button type="submit" class="btn btn-default">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
検索
</button>
</span>
</div>
<a class="btn btn-primary pull-right" href="@routes.UserController.init">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
新規作成
</a>
</div>
}
</div>
<div>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>id</th>
<th>名前</th>
<th>学年</th>
<th>身長</th>
<th>誕生日</th>
<th>好きな食べ物</th>
<th>作成日時</th>
<th>更新日時</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@items.map { field =>
<tr>
<td>@field("id").value</td>
<td>@field("name").value</td>
<td>@field("schoolYear").value</td>
<td>@field("height").value</td>
<td>@field("birthDay").value</td>
<td>@field("food").value</td>
<td>@field("createdAt").value</td>
<td>@field("updatedAt").value</td>
<td>
<a class="btn btn-success" href="@routes.UserController.edit(field("id").value.toLong)">編集</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
※リンクをちょっと足しただけです。
完成!!!
参考URL
-
Play Framework
- Play 2.5.x documentation(英語)
- [Play 2.4.x ドキュメント(日本語)] (https://www.playframework.com/documentation/ja/2.4.x/Home)
-
使用したライブラリ
-
Bootstrap
-
JavaやPlay関連の他のサンプルアプリケーション
※誤字の指摘などは編集リクエストでお願いします。
-
Lombokの最新バージョンはMaven Repositoryから確認できます。 ↩
-
sbt-eclipseの最新バージョンはGitHunのレポジトリから確認できます。 ↩