Help us understand the problem. What is going on with this article?

Play Framework 2.5 (Java)で一覧検索・登録のサンプルアプリケーション

More than 3 years have passed since last update.

概要

PlayframeworkとJavaで一覧検索&CRUDするサンプルアプリケーションをさっくり作成します。

完成予定図

一覧画面
1.png

登録画面
2.png

環境

  • 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でいきます。

ソースコード

https://github.com/kunst1080/play-java8-sample

※説明では同じソースを複数回参照します。全体像が知りたいという人は直接上記レポジトリを参照して下さい。

開発

事前準備

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ファイルの内容を減らします。

conf/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 の末尾に、下記の内容を追記します。
(コメントに書かれている内容を転記)

project/plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "3.0.2")

build.sbtのプロジェクト設定でPlayEbeanのプラグインを有効化します。

build.sbt
lazy val root = (project in file(".")).enablePlugins(PlayJava)
                .enablePlugins(PlayEbean)     // この行を追加

application.confにEbeanの設定を追加します。

conf/application.conf
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

build.sbt
libraryDependencies += "org.projectlombok" % "lombok" % "1.16.10"

LESSの設定

build.sbtへ、LESSの設定を追記します。

build.sbt
// LESS compile setting
includeFilter in (Assets, LessKeys.less) := "*.less"
excludeFilter in (Assets, LessKeys.less) := "_*.less"

Eclipseプロジェクト作成

project/plugins.sbt の末尾に、下記の内容を追記します。2

project/plugins.sbt
// 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

views/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/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

assets/stylesheets/main.less
body {
    padding-top: 60px;
}

コントローラー

controllers.Application

※まだ、画面を表示するためのハリボテです。

controllers/Application.java
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回目

conf/routes
# 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()

この時点での画面

3.png

Table

DB関係の共通部品と、テーブルに対応するエンティティの作成を行います。

テーブル検索のための共通部品

tables.BaseTable

tables/BaseTable.java
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

tables/find/Where.java
package tables.find;

import com.avaje.ebean.ExpressionList;

public interface Where<TABLE> {

    ExpressionList<TABLE> where();

}

tables.find.All

tables/find/All.java
package tables.find;

import java.util.List;

public interface All<TABLE> extends Where<TABLE> {

    default List<TABLE> all() {
        return where().findList();
    }
}

tables.find.Unique

tables/find/Fuzzy.java
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

tables/T_User.java
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

models/UserItem.java
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

models/UserForm.java
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要素)の編集部分を変更しています。

views/index.scala.html
@(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回目

モデルを使った検索処理を追加

controllers/Application.java
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へ追加します。

conf/evolutions/default/2.sql
# --- !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;

この時点での画面

4.png

アプリケーションの開発(登録機能)

次に、ユーザーの登録・編集・削除の機能を追加実装します。

新規画面

画面

user.scala.html

登録・編集・削除のための新画面です。

views/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回目

conf/routes
# 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

controllers/UserController.java
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回目

views/index.scala.html
@(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

※誤字の指摘などは編集リクエストでお願いします。


  1. Lombokの最新バージョンはMaven Repositoryから確認できます。 

  2. sbt-eclipseの最新バージョンはGitHunのレポジトリから確認できます。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away