【PlayFramework 2.5】ログイン処理(1/2)ユーザー登録画面の実装

 今回は、PlayFramework2.5で、簡易的なユーザー登録画面を実装します。

 前回はこちら

 Play Frameworkでは、EbeanというORMライブラリが利用できます。SQL文をハードコーディングしなくても、簡単なことはメソ...

Modelを作成する

 ログイン処理を行うためには、それに対応するユーザー名・パスワードの組み合わせが必要ですから、対応するエンティティを作成します。

package models;

import java.util.*;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

import com.avaje.ebean.Model;
import play.data.format.*;
import play.data.validation.*;

@Entity
public class User extends Model {

    @Id
    public Long id;

    @NotNull
    @Column(unique=true)
    public String userName;

    @NotNull
    public String password;

    @NotNull
    public String fullName;

    public static Finder<Long, User> find = new Finder<Long,User>(User.class);
}

 ユーザー名にUnique制約を付与するアノテーション(@Column(unique=true))を定義していることに注意してください。

 Unique(一意性)制約とは、ユーザー名が重複しないように、あらかじめデータベースに対して制約を持たせたもので、この制約が付いているカラムに他のレコードと重複した値を設定できなくするものです。

Controllerを作成する

 /app/controllersに、AccountController.javaファイルを新規作成します。

package controllers;

import play.mvc.*;
import play.data.*;
import javax.inject.*;

import views.html.*;

import models.*;
import services.*;

public class AccountController extends Controller {
}

ユーザー登録画面を作成する

 ログイン処理を行う前に、ユーザー登録が必要です。登録のための画面を作ってしまいましょう。

/app/controllers/AccountController.java

// メソッドを追加

    public Result register() {
        return ok(register.render(Form.form(User.class)));
    }

 200 OKを返し、viewsのregister.scala.htmlの描画処理を行います。描画処理メソッドにはフォームで使用する定義を渡します。

 さて、該当するビューがまだありませんので、こちらも作成します。

/conf/routes

 routesファイルに以下を追記します。

GET     /register                   controllers.AccountController.register

 routesファイルとは、URLのルーティング設定ファイルで、リクエストされたURLはここの設定によってコントローラの処理に振り分けられます。
 左から順に、「リクエストメソッド(GET|POST|PUT|DELETE)」「URL(パス)」「対応するメソッド」を記述します。

views/register.scala.html

@(registerForm: Form[User])

@import helper._

<!DOCTYPE html>
  <title>Register form</title>
  <h1>register form</h1>
  @form(routes.AccountController.doRegister) {
    @inputText(registerForm("userName"))
    @inputPassword(registerForm("password"))
    @inputText(registerForm("fullName"))
    <input type="submit" value="submit" >
  }

  @if(flash.get("exception") != null){
    <p>@flash.get("exception")</p>
  }

 見慣れない構文がいくつかあります。順に解説します。

@(registerForm: Form[User])

 引数の宣言です。先ほどcontrollerのrender()メソッドで引き渡したForm.form(User.class))を受け取り、名前を定義しています。

 Play Frameworkのviewでは、テンプレートエンジンにscalaを採用しています。Javaの引数宣言とは少し記法が違いますので、覚えておいてください。

Form<User> registerForm    // java
registerForm: Form[User]   // scala

 続いて、インポート文です。

@import helper._

 javaでは、ワイルドカードに*(アスタリスク)を使いますが、scalaでは_(アンダースコア)を使います。少し独特な記法ですが、頻繁に使うので、こちらも併せて覚えておきましょう。

<!DOCTYPE html>
  <title>Register form</title>
  <h1>register form</h1>
  @form(routes.AccountController.doRegister) {
    @inputText(registerForm("userName"))
    @inputPassword(registerForm("password"))
    @inputText(registerForm("fullName"))
    <input type="submit" value="submit" >
  }

  @if(flash.get("errormsg") != null){
    <p>@flash.get("errormsg")</p>
  }

 Play Frameworkのテンプレートエンジンは、基本的にhtmlをそのまま記述することができますので、フロントサイドの開発もスムーズに出来ます。

 ここでは、解説のため少し特殊なテンプレートヘルパーを採用していますが、HTMLのIDやクラスの定義がかえって煩雑になります。特に使わなくても問題ありませんので、使わない方法は次回解説します。

  @form(routes.AccountController.doRegister) {

 フォームヘルパーを使用し、フォームを定義します。引数にPOST先のURLをリバースルーティングで渡します。こうすることにより、routesファイルの定義を参照して自動的にパスを設定してくれます。

 パスの変更が必要になった場合に影響箇所が少なくなるので、出来るだけリバースルーティングで定義していきましょう。

 続いて、各種フォーム部品を定義していきます。

    @inputText(registerForm("userName"))
    @inputPassword(registerForm("password"))
    @inputText(registerForm("fullName"))
    <input type="submit" value="submit" >
  }

 @inputTextでは、<input type='text'>を生成します。
 @inputPasswordでは、<input type='password'>を生成します。

 フォームテンプレートヘルパーには、submitボタン用のメソッドは存在しないようですので、微妙に気持ち悪いですが、HTML構文で書きます。

 最後に、エラーメッセージ表示の処理を書きます。

  @if(flash.get("errormsg") != null){
    <p>@flash.get("errormsg")</p>
  }

 フラッシュスコープのerrormsgに値が入っていたら、

タグを追加してエラーメッセージを表示するようにします。

 これでフォームの定義は完了しました。

ユーザー登録処理を作成する

 ユーザー登録画面は完成しましたが、登録処理が未完成です。先ほどビューでリバースルーティングとして定義したdoRegister()メソッドを実装していきます。

/app/services/UserManager.java

 ここは好みによります。現在のModelにそのままcreateやaddメソッドなどを追加していっても一切問題ありませんし、よりオブジェクト指向的で簡潔かもしれません。

 しかし、ページが増えてきた場合に、ページ毎のfindメソッドなどを追記していくと、瞬く間にModelの巨大化を招きます。後にページ毎にコンポーネント化しようとしても、影響範囲との戦いになるでしょう。

 今回は、一連のモデル操作(登録・更新・削除)をサービスクラスとして定義することにしました。

 createUserメソッドは、エラーメッセージを返すようにします。nullを返した場合は正常終了とします。エラーメッセージが必要ない場合はboolean値などでも構いません。

package services;

import models.*;
import javax.inject.*;

@Singleton
public class UserManager {
    public String createUser(User user){
        if(User.find.where().eq("userName",user.userName).findRowCount() > 0){
            return "ユーザー名が重複しています。他のユーザー名を入力してください。";
        }

        try{
            user.save();
        }catch(Exception e){
            return "想定しないエラーが発生しました。時間をおいて再度お試しください。";
        }

        return null;
    }
}

 @Singletonアノテーションにより、クラスをSingletonにします。

Singleton
public class UserManager {

 これだけでSingletonパターンが適用出来るのですから、便利な世の中ですね。

        if(User.find.where().eq("userName",user.userName).findRowCount() > 0){
            return "ユーザー名が重複しています。他のユーザー名を入力してください。";
        }

 userNameの値で検索し、レコードの件数が1件以上ならユーザー名重複とみなし、エラーメッセージを返します。

 今回はソースコードにエラーメッセージを直接ハードコーディングしていますが、実際の開発ではコンフィグファイルなどを用いてメッセージ管理を行いましょう。

 これは以下のようなSQL文と同等の問い合わせを行います。

SELECT Count(*)
    FROM User 
    WHERE userName LIKE ?

 各メソッドについては、FinderのAPIを参照してください。

 次に、save()メソッドを呼び出します。データベースに変更を加えるような箇所は、必ず例外のハンドリングを行ってください。コードに瑕疵がなくても、偶発的なDBの故障やネットワーク障害が起こりえます。これは開発環境ではあまり遭遇しませんが、本番環境では特に注意が必要です。

        try{
            user.save();
        }catch(Exception e){
            return "想定しないエラーが発生しました。時間をおいて再度お試しください。";
        }

 今回は、想定外エラーのメッセージを返すようにしました。

    return null;

 特に問題がなければ、nullを返します。これにより、エラーメッセージが空=正常である、ということが判断できます。

 これで、登録処理が完成しました。続いて、これに紐づくControllerとroutesを定義していきます。

/app/controllers/AccountController.java


// Injectアノテーションを追加 @Inject UserManager userManager; // メソッドを追加 public Result doRegister() { User user = Form.form(User.class).bindFromRequest().get(); String errorMessage = userManager.createUser(user); if(errorMessage != null){ flash("errormsg",errorMessage); return redirect(routes.AccountController.register()); } return redirect(routes.HomeController.index()); }

 順番に見ていきましょう。

    @Inject UserManager userManager;

 先ほど作成したUserManagerをInjectします。

    UserManager userManager = UserManager.getInstance();

 と読み替えて問題ないと思います。

 続いて、フォームの値を取得します。

        User user = Form.form(User.class).bindFromRequest().get();

 FormクラスのbindFromRequestメソッドを使い、フォームの内容をUserインスタンスにバインドします。

        String errorMessage = userManager.createUser(user);

 UserManagerのcreateUserメソッドを実行します。戻り値にエラーメッセージが返りますので、String型を受け皿にしておきます。

 エラーメッセージがnullでなければ、エラー処理を行います。

        if(errorMessage != null){
            flash("errormsg",errorMessage);
            return redirect(routes.AccountController.register());
        }

 flashというのは、PlayFrameworkにおける「フラッシュスコープ」と呼ばれるものです。有効期限はこのリクエストのみで、次のリクエスト時には破棄されてしまうことから、主にエラーメッセージなどを格納するのに使われます。今回はここにエラーメッセージを格納しました。

 エラーなので、再度登録画面を表示します。ここで、先ほどviewで定義したエラーメッセージ表示用の処理を思い出してください。

  @if(flash.get("errormsg") != null){
    <p>@flash.get("errormsg")</p>
  }

 flashにより格納した値を、flash.getメソッドによって取得しています。引数で受け渡しを行う他にも、セッションなどの値は直接受け取れるという点を覚えておいてください。

        return redirect(routes.HomeController.index());

 問題なく登録が完了したら、indexページへ遷移(リダイレクト)します。

/conf/routes

 routesファイルに以下を追記します。

POST    /api/register               controllers.AccountController.doRegister

動作確認

 ウェブブラウザなどで実際に、/registerにアクセスしてみましょう。
 3つのインプットボックスと1つの送信ボタンが現れました。これらのフィールドを埋めて、submitを押すとindexページへ遷移することを確認してください。

 また、再度フォームにアクセスし、先ほどと同じユーザー名を入れてsubmitを押すと、登録画面のまま、「ユーザー名が重複しています。他のユーザー名を入力してください。」と表示されることを確認してください。

まとめ

 ここまでで、ユーザー登録に関する処理を実装しました。

 次回は、いよいよこれらの情報を使ってログイン処理を実装していきます。

 前回は、ログイン処理の前提となるユーザー登録画面を実装しました。  今回は、いよいよログイン処理を実装していきます。 ログイン処理...