Modelを定義する
では、Modelを定義してみましょう。第2回のView層の解説で利用したTwitterクローンアプリケーションでは、投稿されたメッセージはメモリ上に保持しており、サーバを再起動すると消えてしまいました。
Modelを利用して、投稿されたメッセージをデータベースに保存するようにしてみます。
データベースの設定
Modelを利用してデータベースへアクセスするためには、接続先のデータベースの設定を行わなければなりませんが、mavenを利用してLiftのプロジェクトを作成した時点で、既にBoot.scalaに記述されているため、開発者が改めて書く必要は無いのです。
デフォルトの設定では、Liftに同梱されているH2 Database Engineを利用するようになっています。今回のサンプルでは、H2 Database Engineをそのまま利用するので、特に設定の必要はありません。もし、接続先の情報を変更したい場合は、JDBCドライバのクラスやURLをPropertyファイルに定義すれば、このコードをそのまま利用できます。
プロパティファイルでデータベースへの接続情報を定義するには、「src/main/webapp/WEB-INF/classes/props/default.props」に、リスト1の形式で記述します。また、使用するデータベースにあったJDBCドライバのjarファイルをpom.xmlに定義しておく必要があります。詳細は、Lift公式のWikiページを参照ください。
db.driver = JDBCドライバのクラス名 db.url = 接続先URL db.user = ユーザー名 db.password = パスワード
Message Modelの定義
では、投稿されたメッセージを表すMessage Modelを定義します。
プロジェクトディレクトリ以下の「src/main/scala/demo/twitterclone/model/」ディレクトリに、「Message.scala」というファイル名で新しいファイルを作成します。
// パッケージ宣言
package demo.twitterclone.model
// import宣言
import java.util.Date
import _root_.net.liftweb.mapper._
import _root_.net.liftweb.util.Helpers._
/**
* 投稿されたメッセージに対応するMapperクラスです。
*/
class Message extends LongKeyedMapper[Message] with IdPK { *1
def getSingleton = Message // MetaMapperを返す *2
*3
object status extends MappedString(this, 140) // 本文
object dateOf extends MappedDateTime(this) // 投稿日時
object user extends MappedLongForeignKey(this,User) // 投稿したユーザー
dateOf( new Date ) // 投稿日時のデフォルトは現在時刻
}
/**
* 投稿されたメッセージに対応するMetaMapperオブジェクトです。
*/
object Message extends Message with LongKeyedMetaMapper[Message]{ *4
// ソート順の指定
override def fieldOrder = List( dateOf ,id , status )
}
Mapperクラス
MapperクラスであるMessageクラスは、LongKeyedMapper[Message]とIdPKを継承しています。
*1はMessageクラスの宣言です。LongKeyedMapperはtraitであり、Long型のkeyを持つMapperであることを意味します。IdPKは、主キーのidを自動採番するためのtraitです。
Messageクラスの内部を見てみましょう。*2でdefgetSingletonと定義されている関数は、このMessageクラスに対応するMetaMapperオブジェクトを返すように定義します。
*3は、Messageクラスのプロパティの定義です。Messageクラスには、status(本文)とdateOf(投稿日時)とuser(投稿したユーザー)の3つのプロパティが定義されています。
各プロパティは対応するデータベースのカラムのデータ型に対応するObjectを継承して定義します。status(本文)は文字列なので、MappedStringを継承しています。例にあるMappedString(this,140)では、140字までのデータ長であることを宣言しています。
dateOf(投稿日時)は、日付型なので、MappedDatetimeを継承して定義します。user(投稿したユーザー)は、投稿したユーザーのIDを保持するカラムです。このユーザーIDは、Liftプロジェクトを作成した時点で既に定義されているユーザーテーブルへの外部キーとなっています。よって、MappedLongForeignKey(this,User)のように、Long型外部キーでUserテーブルへの参照を指定しています。
MetaMapperオブジェクト
MetaMapperオブジェクトであるMessageオブジェクトは、Messageクラスを継承しており、さらにLongKeyedMetaMapperも継承します。
MessageクラスとMessageオブジェクトは、同一パッケージかつ同一ファイル内で同じクラス名とオブジェクト名で定義されています。scalaでは、このような関係のクラスとオブジェクトをコンパニオンクラス/コンパニオンオブジェクトと呼び、それぞれに特権的なアクセス権(privateなメンバーへのアクセスなど)が付与されます。
MetaMapperオブジェクトの内部では、fieldOrder関数をオーバーライドして、Mapperに用意されているユーティリティ関数でフォーム用HTMLを生成したときのフィールドの出力順を指定しています。
スキーマの移行(Schemifier)
LiftのModelには、Modelクラスで定義した内容を元に、自動的にデータベース上のテーブル定義を更新するSchemifierという機能があります。Schemifierを利用すると、Modelの定義とは別にCREATE TABLE文などのDDLを用意する必要がなくなります。
Schemifierは対話型コンソールからも実行できますが、開発中は設定ファイルであるBoot.scalaにSchemifierの実行を定義しておくとよいでしょう。このようにしておくことで、開発サーバを起動するたびに、Modelの定義とデータベースのスキーマの内容を比較して、更新されている場合はModelの定義に合わせてデータベースのスキーマを自動的に同期してくれます。
class Boot {
def boot {
// ...省略
// Schemifierの呼び出し。第3引数以降に
// 同期させたいモデルを指定する
Schemifier.schemify(true, Log.infoF _, User,Message)
// ...省略
}
}
対話型コンソールでの動作確認
以上でModelの定義ができました。早速動作確認をしてみましょう。Liftは、対話型コンソールからModelの操作を行うことができます。
まずは、mavenコマンドで対話型コンソールを起動しましょう。
$ mvn scala:console Welcome to Scala version 2.7.5.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_15). Type in expressions to have them evaluated. Type :help for more information. scala>
このように、Mavenからscalaの対話型コンソールを起動することで、あらかじめ必要なライブラリがクラスパスに通った状態で起動できます。
次に、Liftの設定を読み込みます。コンソールに次のように入力しましょう。
scala> (new bootstrap.liftweb.Boot).boot INFO - CREATE TABLE users (id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY , firstname VARCHAR(32) , lastname VARCHAR(32) , email VARCHAR(48) , locale VARCHAR(16) , timezone VARCHAR(32) , password_pw VARCHAR(48) , password_slt VARCHAR(20) , textarea VARCHAR(2048) , superuser SMALLINT , validated SMALLINT , uniqueid VARCHAR(32)) INFO - ALTER TABLE users ADD CONSTRAINT users_PK PRIMARY KEY(id) INFO - CREATE TABLE message (id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY , user_c BIGINT , status VARCHAR(140) , dateof TIMESTAMP) INFO - ALTER TABLE message ADD CONSTRAINT message_PK PRIMARY KEY(id) INFO - CREATE INDEX users_email ON users ( email ) INFO - CREATE INDEX users_uniqueid ON users ( uniqueid ) INFO - CREATE INDEX message_user_c ON message ( user_c ) scala>
Liftの設定を読み込むと、ログにCREATE TABLEなどのSQL文が表示されました。これは、Boot.scalaにSchemifierの呼び出しを定義しているため、設定読み込みの際にスキーマとの同期が実行されているためです。初回の起動ではテーブルが存在しないため、UserモデルとMessageモデルに対応するテーブルがCREATE TABLE文で作成されています。
では、実際にMessageモデルを操作してみましょう。リスト6のように入力してみてください。
scala> import demo.twitterclone.model._ *1
import demo.twitterclone.model._
scala> val msg = Message.create.status("hello") *2
msg: demo.twitterclone.model.Message = demo.twitterclone.model.Message={id=-1,user=NULL,status=hello,dateof=Mon Oct 19 15:03:48 JST 2009}
scala> msg.save *3
res3: Boolean = true
scala> Message.findAll *4
res4: List[demo.twitterclone.model.Message] = List(demo.twitterclone.model.Message={id=1,user=NULL,status=hello,dateof=2009-10-19 00:00:00.0})
*1では、Modelが定義してあるパッケージをimportしています。
*2では、Messageモデルをnewします。status("hello")は、status(本文)プロパティに"hello"という文字列を設定しています。モデルの各プロパティへの値の設定は、メソッドチェーンで続けて設定できるようになっています。
*3は、モデルのsave関数で、*2で作成したMessageモデルをデータベースにinsertしています。
*4は、MetaMapperオブジェクトであるMessageオブジェクトのfindAll関数を呼び出して、Messageテーブルから全件検索を行っています。*3で保存されたデータが検索されていることが分かると思います。
