Liftを用いたWebアプリケーションの作り方 その2

昨日の

blog1.mammb.com

に続き、本日は、Lift の公式ページにある Getting Started を参考に・・

プロジェクトの作成

今回作成するプロジェクトはtodo管理アプリケーションとなります。前回と同様に Maven によりプロジェクトを作成します。

mvn archetype:generate -U \ 
 -DarchetypeGroupId=net.liftweb \ 
 -DarchetypeArtifactId=lift-archetype-basic \ 
 -DarchetypeVersion=1.1-M5 \ 
 -DremoteRepositories=http://scala-tools.org/repo-releases \ 
 -DgroupId=com.liftworkshop \ 
 -DartifactId=todo \ 
 -Dversion=0.1-SNAPSHOT


確認では Y を入力します。

Confirm properties configuration:
groupId: com.liftworkshop
artifactId: todo
version: 0.1-SNAPSHOT
package: com.liftworkshop
 Y: : Y


この時点で動作確認をしてみます。

$ cd todo
$ mvn jetty:run

ブラウザにてhttp://localhost:8080/にアクセスして todo のページが表示されれば成功です。ctrl-cにてサーバを止めておきます。
これからソースを編集していきますが、以下のコマンドをコマンドプロンプトで実行しておくことで、ソースファイルの変更時に自動的にコンパイルが行われるようになります。

$ mvn scala:cc

modelの追加

ToDoモデルをsrc/main/scala/com/liftworkshop/model/ToDo.scalaとして追加します。コードは以下のようになります。

package net.liftweb.hello.model 
 
import net.liftweb._ 
import mapper._ 
import http._ 
import SHtml._ 
import util._ 
 
class ToDo extends LongKeyedMapper[ToDo] with IdPK { 
 def getSingleton = ToDo 
 
 object done extends MappedBoolean(this) 
 object owner extends MappedLongForeignKey(this, User) 
 object priority extends MappedInt(this) { 
   override def defaultValue = 5 
 } 
 object desc extends MappedPoliteString(this, 128) 
} 
 
object ToDo extends ToDo with LongKeyedMetaMapper[ToDo]

ポイントは以下の通りです。

  • IdPKトレイトによりPKとなるIDをミックスイン
  • doneというBoolean型のフィールドを定義
  • ownerというLong型のフィールドは、テンプレートで作成されるUserの外部キーとなる
  • priorityというInt型のフィールドをデフォルト値を5として定義
  • descというVARCHARにマッピングされるフィールドを定義
  • LongKeyedMetaMapper[ToDo]トレイトにより、List[ToDo]を返却するfindAll() メソッドがミックスインされる

ToDoエンティティへの反映

今作成した ToDo モデルをエンティティとして反映させるよう、src/main/scala/bootstrap/liftweb/Boot.scala へ設定を追加します。Boot.scala の以下の定義を

    // where to search snippet
    LiftRules.addToPackages("com.liftworkshop")
    Schemifier.schemify(true, Log.infoF _, User)


以下のように変更し、SchemifierへToDoモデルを追加します。

    // where to search snippet
    LiftRules.addToPackages("com.liftworkshop")
    Schemifier.schemify(true, Log.infoF _, User, ToDo)


この時点で再度Jettyを起動してみましょう。コマンドプロンプトに以下のようなDDLのログが出力され、テーブルが自動作成されていることが確認できます。

INFO - CREATE TABLE todo (priority INTEGER , id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY , done SMALLINT , desc_c VARCHAR(128) , owner BIGINT)
INFO - ALTER TABLE todo ADD CONSTRAINT todo_PK PRIMARY KEY(id)
INFO - CREATE INDEX todo_owner ON todo ( owner )

snippetとビューの作成

ビューから呼び出される Util.scala という snippet を作成します。作成するファイルはsrc/main/scala/com/liftworkshop/snippet/Util.scalaとなります。

package com.liftworkshop.snippet 
 
import scala.xml.{NodeSeq} 
import com.liftworkshop._ 
import model._ 
 
class Util { 
 def in(html: NodeSeq) = 
   if (User.loggedIn_?) html else NodeSeq.Empty 
 
 def out(html: NodeSeq) = 
   if (!User.loggedIn_?) html else NodeSeq.Empty 
}

ここでは、ユーザがログインしているかを判断して、inメソッドに渡されたHTMLを表示するかどうかを制御しています。


次に、ビューから snippet を呼び出してみましょう。 src/main/webapp/index.html を以下のように変更します。

<lift:surround with="default" at="content"> 
 <lift:Util.out>Please 
   <lift:menu.item name="Login">Log In</lift:menu.item> 
 </lift:Util.out> 
</lift:surround>

ビューに記述した、のタグから snippet が呼び出されます。この時、Utilクラスのoutメソッドに、タグで囲まれたHTMLが渡されることになります。


ビューと snippet の連携を確認してみます。Jettyを起動し、http://localhost:8080/にアクセスします。ログインしていない状態だと以下のように表示されます。


ログイン後は以下となります。


ビューとsnippetのクラス・メソッド呼び出しの対応関係は以下のようになっています。

View code Class Method
Foo render
FooBar render
FooBar render
FooBar baz

ToDo アイテムを操作する snippet の作成

ToDoアイテムを操作するための snippet を src/main/scala/com/liftworkshop/snippet/TD.scala として作成します。

package com.liftworkshop.snippet 
 
import com.liftworkshop._ 
import model._ 
 
import net.liftweb._ 
import http._ 
import SHtml._ 
import S._ 
 
import js._ 
import JsCmds._ 
 
import mapper._ 
 
import util._ 
import Helpers._ 
 
import scala.xml.{NodeSeq, Text} 
 
class TD { 
 def add(form: NodeSeq) = { 
   val todo = ToDo.create.owner(User.currentUser) 
 
   def checkAndSave(): Unit = 
   todo.validate match { 
    case Nil => todo.save ; S.notice("Added "+todo.desc) 
    case xs => S.error(xs) ; S.mapSnippet("TD.add", doBind) 
   } 
 
   def doBind(form: NodeSeq) = 
   bind("todo", form, 
      "priority" -> todo.priority.toForm, 
      "desc" -> todo.desc.toForm, 
      "submit" -> submit("New", checkAndSave)) 
 
   doBind(form) 
 } 
}

このクラスでは add というトップレベルメソッドを定義しています。このメソッドのポイントは以下となります。

  • 1行目で、新しい ToDo アイテムのインスタンスを作成
  • 最下部で、doBind というメソッドを呼び出し
  • doBind メソッドは、作成した新しい ToDo アイテムにフォームからの値を設定している
  • doBind メソッドは、form の submit に応じて、checkAndSava メソッドを呼び出す
  • checkAndSava ではバリデーションを行い、エラーが無ければ ToDo アイテムを保存する


src/main/scala/com/liftworkshop/model/ToDo.scala にある以下のメソッドを

 object priority extends MappedInt(this) { 
   override def defaultValue = 5 
 } 

以下のように修正します。

 object priority extends MappedInt(this) { 
   override def defaultValue = 5 
 
   override def validations = validPriority _ :: super.validations 
 
   def validPriority(in: Int): List[FieldError] = 
   if (in > 0 && in <= 10) Nil 
   else List(FieldError(this, <b>Priority must be 1-10</b>)) 
 
   override def _toForm = Full(select(ToDo.priorityList, 
                            Full(is.toString), 
                            f => set(f.toInt))) 
 }

ここではpriorityフィールドのバリデーション処理を追加しています。


続いて、以下のdesc フィールドを

 object desc extends MappedPoliteString(this, 128) 


以下のように修正し、descフィールドのバリデーション処理を追加します。

 object desc extends MappedPoliteString(this, 128) { 
   override def validations = 
    valMinLen(3, "Description must be 3 characters") _ :: 
      super.validations 
 }


さらに以下のオブジェクトを

 object ToDo extends ToDo with Lon

以下のように修正し、priorityフィールドのプルダウン内容をpriorityListとして定義しています。

object ToDo extends ToDo with LongKeyedMetaMapper[ToDo] { 
 lazy val priorityList = (1 to 10). 
      map(v => (v.toString, v.toString)) 
}


以上で ToDo.scala の修正は完了です。続いて index.html を編集し、ToDo の入力画面とします。
src/main/webapp/index.html を以下のように変更します。

<lift:surround with="default" at="content"> 
<lift:Util.in> 
   <lift:TD.add form="post"> 
    <table> 
      <tr> 
       <td>Description:</td> 
       <td><todo:desc>To Do</todo:desc></td> 
      </tr> 
      <tr> 
       <td> 
         Priority 
       </td> 
       <td> 
         <todo:priority> 
          <select><option>1</option></select> 
         </todo:priority> 
      </td> 
      </tr> 
      <tr> 
       <td>&nbsp;</td> 
       <td> 
         <todo:submit> 
          <button>New</button> 
         </todo:submit> 
       </td> 
      </tr> 
    </table> 
   </lift:TD.add> 
 </lift:Util.in>
</lift:surround>


jetty:runで起動し、http://localhost:8080 でアクセスしてみましょう。ログインを行うと以下の画面が表示されます。


入力エラー時には、左下にメッセージが表示され、バリデーション処理が行われていることが確認できます。


この時点で、入力したToDo内容の保存まで行うことができます。
次回は、ここでの内容を変更し、登録したToDoを一覧表示する所まで作成します。