[2009 年 4 月号] |
[技術講座]
いよいよデータベース接続編の後半に入ります。 前回紹介したセッションアダプタパターンを用い、Glorp という ORM (Object-Relational Mapping) フレームワークで、PostgreSQL に接続を行います。 Glorp を使うと SQL を極力意識せずに、RDB 内のデータに透過的にアクセスできるようになります。 Seaside との連携もスムーズで、既存の技術が巧みに「オブジェクト指向化」されていることを確認できるでしょう。
まずはデータベース接続のための環境を整えるところから始めていきます。
今回はオープンソース系の RDBMS として定評のある PostgreSQL を使います。 MySQL や SQLite などの選択肢もありますが、Squeak の場合、PostgreSQL のドライバがもっとも実績があり安定しているという事情があります1。
PostgreSQL のインストールは、それほど難しいものではありません。 Windows であれば日本 PostgreSQL ユーザ会のページからインストーラをダウンロード・起動し、指示に従うだけで済みます2。
また主要な Linux ディストリビューションであれば、apt や yum などのパッケージ管理システムから、すぐに入れることができるでしょう。 Mac の場合は MacPorts や Fink を使えば、やはり簡単にインストールできます。
*1 Smalltalk 環境として VisualWorks を選ぶ場合はこの限りではありません。MySQL、Oracle など自由に選択可能です。
*2 例えば All About の記事(PostgreSQL 8.3 インストール)を参考にしても良いでしょう。
前回に引き続いて、All-in-one パッケージである SeasideJOnePlusDB を利用します。 作者がこまめに更新をしているので、最新版をダウンロードして使われることをお勧めします。
下記からダウンロードできます。
SeasideJOnePlusDB
執筆時には 2009/3/6 版が最新バージョンになっています。 DB に接続するサンプルアプリとして、ToDoList が入っており、SStore、OmniBase、Glorp のそれぞれで、どのように DB にアクセスするのかを確認できます。 この記事では、Glorp との連携部分をメインに解説を行います。
既存の環境を変更したくないなど、PostgreSQL を新規に入れたくない時もあるでしょう。 そうした場合は Squeak 入りの Linux である SqLinuxOS が役立ちます。
Seaside 版は以下のリンクになります。
SqLinuxOS Seaside Edition 2009
SqLinuxOS Seaside 版は、ブータブル CD なので、既存の環境を汚しません。 iso イメージを CD-R などに焼いてブートするだけで、SeasideJOnePlusDB と PostgreSQL をすぐに使うことができます。
ディスク容量に余裕があれば、VMWare や VirtualBox など仮想化の仕組みを使って実行する手もあるでしょう。
SqLinuxOS Seaside 版では 'seasider' というユーザが最初から用意されています。 パスワードはユーザ名と同じです。
KDE デスクトップ上の "Seasideスタート" をクリックすると SeasideJOnePlusDB を起動できます。 PostgreSQL は自動的に起動するようになっており、意識する必要はありません。
それでは準備が整いましたので、いよいよプログラミングに入っていきましょう。
まずは Glorp に慣れるべく、小さなサンプルコードを書いてみることにします。
Glorp (Generic Lightweight Object-Relational Persistence) は、Smalltalk の世界では非常によく知られた ORM です。 歴史も古く、ObjectPeople という会社で 1990 年後半に開発されていた TopLink という製品から派生したものです3。
Glorp にはいくつかの特徴がありますが、もっとも際だっているのは「オリジナルのモデルに干渉しない」ということです。
Glorp では、ドメインモデルのクラスと RDB のテーブルをマップするために、「ディスクリプタシステム」という、メタ情報を記述するクラスを作成します。 このディスクリプタシステムクラスは、ドメインモデルのクラス階層とは独立しています。 そのため既存のドメインモデルは、Glorp 導入の前と後で、変更が及ぶことが一切ありません。 特定の永続化用のクラスを継承する必要もなく、永続化用のアノテーションを入れたりする必要もないのです。
また、下回りを極力意識させないように、オブジェクトレベルのトランザクションである UnitOfWork というものも提供しています。 これにより、あたかも OODB を扱っているかのようにプログラミングすることができます。
*3 TopLink は後に Java 版が有名となり、現在は Oracle の製品として組み込まれています。
手始めにドメインモデルとして「メモ」というクラスを考えます。
以下のようにクラス定義をします。 カテゴリは 'GlorpLesson' とでもしておきましょう。
Object subclass: #MyMemo
instanceVariableNames: 'id contents'
classVariableNames: ''
poolDictionaries: ''
category: 'GlorpLesson'
インスタンス変数は 2 つだけです。 一つはメモの id、もう一つは内容 (contents) です。
例によってブラウザメニューの「アクセッサの作成」で、id と contents のアクセッサも作っておきます。
次に、ディスクリプタシステムとなるクラスを書きます。 DescriptorSystem という抽象クラスがあるので、これを継承します。
DescriptorSystem subclass: #MyMemoDescriptorSystem instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'GlorpLesson'
ディスクリプタシステムでは、一つのドメインモデルにつき、3 つのメソッドを用意する必要があります。
一つはドメインモデルのメタ情報用、一つはテーブル定義用、もう一つはモデルとテーブルのマッピング用です。
まずドメインモデルのメタ情報を付加するためのメソッドを追加しましょう。名前は classModelForMyMemo: とします。
MyMemoDescriptorSystem >> classModelForMyMemo: ('class models'カテゴリ)
classModelForMyMemo: aClassModel aClassModel newAttributeNamed: #id. aClassModel newAttributeNamed: #contents
ドメインモデルのうち、永続化させたい属性を newAttributeNamed: で指定します。 今回は使用しませんが、必要に応じて newAttributeNamed:type: で属性の型を指定することもできます4。
Glorp では名前付けルールによって、ドメインモデルとディスクリプタシステムを結びつけるようになっており、メソッド名に気を配る必要があります。 MyMemo のメタ情報メソッドでは classModelForMyMemo: という具合に、最後が必ず 'MyMemo' で終わるようにします。
次にテーブル定義のためのメソッドを定義します。 今度は tableForMYMEMO: という名前にします。
MyMemoDescriptorSystem >> tableForMYMEMO: ('tables'カテゴリ)
tableForMYMEMO: aTable (aTable createFieldNamed: 'id' type: (platform sequence)) bePrimaryKey. aTable createFieldNamed: 'contents' type: (platform varChar: 500)
'MYMEMO' と大文字になっている部分が RDB のテーブル名となります。 通常 RDBMS ではテーブル名の大文字小文字を区別しませんが、Glorp ではテーブルであることを示すため、tableFor 系のメソッドには大文字を使うことになっています。
引数は DatabaseTable のインスタンスです。 フィールド設定用のメッセージを送ると、RDB のフィールドを必要に応じて作ってくれます。 例では 'id' というフィールドを sequence 型で、'contents' というフィールドを 500 文字の varchar 型として定義しています5。 また、'id' はプライマリキーになるので、bePrimaryKey というメッセージも送っています。
最後にドメインモデルとテーブルとのマッピングを行うメソッドを定義します。 descriptorForMyMemo: という名前にします。
MyMemoDescriptorSystem >> descriptorForMyMemo: ('descriptors'カテゴリ)
descriptorForMyMemo: aDescriptor | table | table := self tableNamed: 'MYMEMO'. aDescriptor table: table. aDescriptor directMapping from: #id to: (table fieldNamed: 'id'). aDescriptor directMapping from: #contents to: (table fieldNamed: 'contents')
今回は単純な例なので、テーブルのフィールドとクラスの属性は一対一で対応します。 そのため directMapping というメッセージを、マッピング用クラスであるディスクリプタ (Descriptor) に送り、互いの結びつけを行っています。
一連のメソッドの記述は少しややこしい気もしますが、どのドメインモデルもほぼ同様の書き方になるので、機械的に覚えてしまうのも良いでしょう。
打ち込みが面倒な人は、コードが末尾にあるので活用してください。
*4 単純な型については推論してくれるため、特に指定する必要はありません。
*5 どのような型がサポートされているかは、DatabasePlatform のサブクラスの types カテゴリを見ると良いでしょう。
ではデータベースにメモを格納してみることにします。
まず、下準備としてデータベース内にテーブルを作成する必要があります。 以下の式をワークスペースで実行しましょう。
"ログインオブジェクトの準備" login := Login new database: PostgreSQLPlatform new; username: 'postgres'; password: 'postgres'; connectString: 'localhost_postgres'. "テーブルの作成" MyMemoDescriptorSystem recreateTablesFor: login
データベースにログインするために Glorp の Login クラスを使っています。 今回は PostgreSQL への接続なので、PostgreSQLPlatform を指定します。 ユーザ名や、パスワードのパラメータは、環境に応じて変える必要があるでしょう。 (SqLinuxOSでは、上記コードのままでつながります。)
次にログインオブジェクトを引数として、MyMemoDescriptorSystem に recreateTablesFor: を送り、テーブルを作成しています。
成功すれば RDB 内に MYMEMO テーブルができあがります。 以下の図は PostgreSQL の管理ツールである pgAdmin を使って、テーブルのスキーマを確認してみたものです。
続いてメモを格納しましょう。
login := Login new database: PostgreSQLPlatform new; username: 'postgres'; password: 'postgres'; connectString: 'localhost_postgres'. memo := MyMemo new contents: 'これはメモです。'. session := MyMemoDescriptorSystem sessionForLogin: login. session login. session save: memo. "メモの格納" session logout
ログインを生成する部分は、さきほどと同じですが、今度は MyMemoDescriptorSystem に、sessionForLogin: を送り、データベースへのセッションを得ています。
セッションに login を送ってログインした後、save: でメモを格納しています。 セッションの終了は logout で行います。
これで、MYMEMO テーブルにメモが一つ入りました。 今後は取り出してみましょう。
login := Login new database: PostgreSQLPlatform new;
username: 'postgres';
password: 'postgres';
connectString: 'localhost_postgres'.
session := MyMemoDescriptorSystem sessionForLogin: login.
session login.
(session readManyOf: MyMemo) explore. "メモの取り出し"
session logout
ログインを行った後、今度は readManyOf: をセッションに送っています。 引数として MyMemo クラスを指定したので、対応した MYMEMO テーブルから、MyMemo のすべてのインスタンスが取り出されます。 まだメモを一つしか登録していないため、要素数が 1 の配列が返ってきます。
上記を実行するとオブジェクトエクスプローラが開きます。 配列を展開して、メモの属性の値を確認してみましょう。
id には、明示的に値を入れませんでしたが、自動的に 1 が入っています。 MyMemoDescriptorSystem >> tableForMYMEMO: 内で、id フィールドを sequence 型として指定したので、PostgreSQL が一意の値を振ってくれたのです。
次にデータベースから取り出したメモを更新してみることにします。
login := Login new database: PostgreSQLPlatform new; username: 'postgres'; password: 'postgres'; connectString: 'localhost_postgres'. session := MyMemoDescriptorSystem sessionForLogin: login. session login. "UnitOfWork内での更新" session inUnitOfWorkDo: [ (session readOneOf: MyMemo) contents: 'これはメモです。更新。'. ]. session logout
取り出しに readManyOf: でなく readOneOf: を使ってみました。 この場合はメモのインスタンスが一つだけ取り出されることになります。 次に contents: で、メモの内容を書き換えています。
メモの内容を変える部分を inUnitOfWork: のブロックで囲っているところもポイントです。 これによって明示的に save: を行わなくとも、ブロックを抜けた時に、自動的に更新が行われます。
UnitOfWork とは、マーチン・ファウラーの「エンタープライズ アプリケーションアーキテクチャパターン」(PofEAA本) でも出てくる概念で、オブジェクトに対する一連の「作業単位」を表します。 オブジェクトに変更がある度に SQL を発行していると効率が悪くなるため、UnitOfWork の終了時に、冗長な部分を排除して、本当に必要な SQL のみを DBMS に送るようにするという仕組みです。
UnitOfWork のブロック内でのオブジェクトの変更を、Glorp は自動的に記憶してくれます。 メモのような単純な例では save: を使って明示的に保存してもそれほど負担にはなりませんが、複数のオブジェクトが複雑な関連を持つような場合には、この UnitOfWork は非常に便利です。
実際に Glorp がどのような SQL を発行しているか見てみましょう。
以下をワークスペースで実行します。
PGConnection defaultTraceLevel: 2
これは PostgreSQL コネクタのトレースレベルの設定です。 こうしておくと発行された SQL のログがトランスクリプトウィンドウに表示されるようになります6。
トランスクリプトは、Squeak のデスクトップ右上に配置されたボタンから開くことができます7。
さて、トレースレベルの設定後、下記を実行してみてください。
login := Login new database: PostgreSQLPlatform new; username: 'postgres'; password: 'postgres'; connectString: 'localhost_postgres'. session := MyMemoDescriptorSystem sessionForLogin: login. session login. session inUnitOfWorkDo: [ (session readOneOf: MyMemo) contents: 'これはメモです。更新。'; contents: 'これはメモです。また更新。'. ]. session logout
メモの内容を UnitOfWork 内で 2 回更新していますが、SQL としては一つの UPDATE になっていることが確認できます8。
上記の更新を行うプログラムをもう一度、実行してみると、発行される SQL は以下のようになります。 メモの内容が、「これはメモです。また更新。」で同一なので、Glorp は UPDATE の発行をパスしています。
*6 表示をやめるにはトレースレベルを 0 に設定します。
*7 またはワークスペースから、Transcript open と実行しても良いでしょう。
*8 DB に送る SQL を直接表示しているため文字化けが起こります。
メモを使ったサンプルの仕上げとして、クエリによる検索を試みましょう。
login := Login new database: PostgreSQLPlatform new; username: 'postgres'; password: 'postgres'; connectString: 'localhost_postgres'. session := MyMemoDescriptorSystem sessionForLogin: login. session login. "クエリを指定した検索" (session readOneOf: MyMemo where: [:memo | memo contents like: '%また更新%']) explore. session logout
今後は readOneOf:where: を使って、検索条件を指定しています。 contents の内容を見て、'%また更新%' にマッチするものをデータベースから取り出すというわけですね。
メモの内容がきちんと更新されていることを確認できます。
Glorp の基本機能をざっと見てきました。 複数のドメインモデルがあった場合のマッピング方法については、次の節で扱います。
もっと詳しい情報を知りたい方のために、巻末にも Glorp の参考資料を載せておきましたので、適宜ご参照ください。
以後は SeasideJOnePlusDB に付属している ToDoList アプリを題材にして、Seaside と Glorp の連携方法を見ていきます。
手始めとして ToDoList の動作を確認してみましょう。
Web ブラウザで以下の URL にアクセスします。
https://localhost:9090/seaside/toDoList
アクセスするとまずログイン画面が出てきます。
2 名のユーザがすでに登録済みです。 それぞれ、
ユーザ名 | パスワード |
'user1' | 'pwd1' |
'user2' | 'pwd2' |
となっています。
ログインすると ToDoList の入力画面になります。 下にある"追加"ボタンで新たな ToDo 項目を作成できます。
追加した項目について"編集"のリンクで内容を記入します。
リンクを押すとインラインで内容の編集ができます。
以下、同様に追加、編集などをしていきます。 作業を終えた ToDo 項目については、チェックをつけて"削除"を押せばリストから削除されます。
ToDoList の内容はログインしたユーザごとに保持されます9。
別のユーザでログインすると、今入れたものとは別の ToDoList を入力することができます。
*9 デフォルトでは ToDoMemoryDB というシングルトンがオンメモリで保持しています。
ToDoList アプリは、セッションアダプタパターンを用いて作られているため、セッションクラスを切り替えることで、さまざまなデータベースに接続することが可能です10。
簡略化したクラス図を以下に示します。
ドメインモデル(黄色)は ToDoUser と ToDoItem の 2 つだけで、ユーザ (ToDoUser) が複数の ToDo 項目 (ToDoItem) を集約で持つという関係になっています。
コンポーネント(水色)はログイン用を除くと、リスト表示用 (ToDoListComponent) と項目表示用 (ToDoItemComponent) からなっています。
セッションアダプタ(ピンク)は、実際にはさまざまなデータベース用にいくつもサブクラスがありますが、今回は Glorp 編なので、デフォルトで有効なオンメモリ用 (ToDoSessionMemory) を、Glorp 用 (ToDoSessionGlorp) に切り替えて使うのみにします。
ToDoDescriptorSystem (緑)は、Glorp 用のディスクリプタシステムクラスです。 ToDoSessionGlorp は、これを使って RDB への接続を実現しています。
*10 セッションアダプタパターンについては前回に詳しい解説があります。
ToDoSessionGlorp を有効にする前に、若干の準備を行います。 PostgreSQL に接続して ToDoList 用のテーブルを作成するのです。 ワークスペースで以下を実行してください。
ToDoDescriptorSystem initialize. ToDoDescriptorSystem recreateTables
ローカルの PostgreSQL につなぐためのパスワード入力のダイアログがでてきます。 先ほどのサンプルで Glorp を試した時と同じものを入力すれば良いでしょう。
ToDoList アプリでは、データベース接続の度にログイン情報を入れずに済むように、接続パラメータを ToDoDescriptorSystem の connectionArguments というクラスインスタンス変数に保持しています。 この変数は ToDoDescriptorSystem class >> initialize によって初期化されます。
ToDoDescriptorSystem class >> initialize ('class initialization'カテゴリ)
initialize
"ToDoDescriptorSystem initialize"
super initialize.
connectionArguments := nil
PostgreSQL の設定によってはパスワードの入力だけではうまくつながらないこともあるでしょう。 その場合は connectionArguments のファクトリメソッドである ToDoDescriptorSystem class >> buildConnectionArguments を編集し、再び ToDoDescriptorSystem class >> initialize を実行します。
ToDoDescriptorSystem class >> connectionArguments ('accessingカテゴリ)
connectionArguments
connectionArguments ifNil: [connectionArguments := self buildConnectionArguments].
^connectionArguments
ToDoDescriptorSystem class >> buidConnectionArguments ('postgres-settings'カテゴリ)
buildConnectionArguments ^PGConnectionArgs hostname: 'localhost' portno: 5432 databaseName: 'postgres' userName: 'postgres' password: ((FillInTheBlank requestPassword: 'postgres password:') ifNil: ['postgres'])
ToDoDescriptorSystem では、このキャッシュされた connectionArguments を使い、PostgreSQL 用のログインオブジェクトを作るようになっています。
ToDoDescriptorSystem class >> postgreSQLLogin ('postgres-settings'カテゴリ)
postgreSQLLogin
| loginArgs |
loginArgs := self connectionArguments.
^ Login new database: PostgreSQLPlatform new;
username: loginArgs userName;
password: loginArgs password;
connectString: loginArgs hostname , '_', loginArgs databaseName
データベースへのセッションはこのログインオブジェクトを使って作られます。
ToDoDescriptorSystem class >> newSession ('actions'カテゴリ)
newSession
"ToDoDescriptorSystem newSession"
^self sessionForLogin: self postgreSQLLogin
そしてこのセッションがテーブルの作成で使われているわけです。
ToDoDescriptorSystem class >> recreateTables ('utilities'カテゴリ)
recreateTables "ToDoDescriptorSystem recreateTables" | session | session := self newSession. session login. session recreateTables. session logout
これで
ToDoDescriptorSystem initialize. ToDoDescriptorSystem recreateTables
によってテーブルが作られる流れが理解できたでしょう。
pgAmin で確認してみると、新たに TODOUSER と TODOITEM のテーブルが作成されています。
それでは、ドメインモデルのマッピングがどのように行われているのか、もう少し詳しく見ていきましょう。
単にソースコードを追うだけでは面白くありませんので、既存のドメインモデルにアレンジを加えつつ読み解いていくようにします11。
現状のモデルでは、ToDo 項目に更新日時の情報が入っていませんので、時系列に ToDo 項目を並べたい時などに不便です。
そこで updatedTimeStamp (更新日時)というインスタンス変数を ToDoItem に加えることにします。
まず、モデルである ToDoItem を変更します。 クラス定義は以下のようになります。
Object subclass: #ToDoItem
instanceVariableNames: 'id contents updatedTimeStamp'
classVariableNames: ''
poolDictionaries: ''
category: 'ToDo-Model'
「アクセッサの作成」でアクセッサも作っておきます。 インスタンス生成時に更新日時が初期化されるように、initialize メソッドも書き換えましょう。
ToDoItem >> initialize ('initialization'カテゴリ)
initialize
contents := ''.
updatedTimeStamp := TimeStamp now
ドメインモデルの変更は以上で終わりです。 次に ToDoDescriptorSystem のマッピングを見ていきましょう。
まず、ToDoItem を集約している ToDoUser のマッピングです。 クラスのメタ情報は下記のようになっています。
ToDoDescriptorSystem >> classModelForToDoUser: ('class models'カテゴリ)
classModelForToDoUser: aClassModel aClassModel newAttributeNamed: #name. aClassModel newAttributeNamed: #password. aClassModel newAttributeNamed: #toDoItems collectionOf: ToDoItem
ToDoUser から ToDoItem への一対多の関連を、newAttributeNamed:collectionOf: を使って表現しています。
一方でテーブルのメタ情報はいたって普通です。
ToDoDescriptorSystem >> tableForTODOUSER: ('tables'カテゴリ)
tableForTODOUSER: aTable (aTable createFieldNamed: 'name' type: (platform varChar: 50)) bePrimaryKey. aTable createFieldNamed: 'password' type: (platform varChar: 100)
'name' のフィールドをプライマリキーとして指定しています。 クラスのメタ情報にあった、ToDoItem の指定はここにはありません。 RDB のテーブルでは、ToDoItem への参照は TODOUSER テーブルからではなく、TODOITEM テーブルが外部キーとして保持することになります。 このため TODOUSER の定義としては、上記で十分なわけです。
続いてクラスとテーブルのマッピングです。
ToDoDescriptorSystem >> descriptorForToDoUser: ('descriptors'カテゴリ)
descriptorForToDoUser: aDescriptor | table | table := self tableNamed: 'TODOUSER'. aDescriptor table: table. aDescriptor directMapping from: #name to: (table fieldNamed: 'name'). aDescriptor directMapping from: #password to: (table fieldNamed: 'password'). aDescriptor toManyMapping attributeName: #toDoItems
name と password に関しては、一対一なので通常の directMapping を使っています。 toDoItems に関しては一対多ということで、toManyMapping というメッセージを送っています。
次に ToDoItem のほうに移ります。 こちらは updatedTimeStamp のインスタンス変数を追加しているため、既存のコードを編集することになります。 (以下、書き加えた部分を青色のイタリックで示します。)
クラスのメタ情報の記述である classModelForToDoItem: から見ていきます。
ToDoDescriptorSystem >> classModelForToDoItem: ('class models'カテゴリ)
classModelForToDoItem: aClassModel aClassModel newAttributeNamed: #id. aClassModel newAttributeNamed: #contents. aClassModel newAttributeNamed: #updatedTimeStamp
updatedTimeStamp を永続化するため、最後の行が付け加わっています。
テーブルの定義メソッドもこれに合わせて変更します。
ToDoDescriptorSystem >> tableForTODOITEM: ('tables'カテゴリ)
tableForTODOITEM: aTable | userName | (aTable createFieldNamed: 'id' type: (platform sequence)) bePrimaryKey. aTable createFieldNamed: 'contents' type: (platform varChar: 500). aTable createFieldNamed: 'updatedTimeStamp' type: (platform timestamp). userName := aTable createFieldNamed: 'TODOUSER_name' type: (platform varChar: 50). aTable addForeignKeyFrom: userName to: ((self tableNamed: 'TODOUSER') fieldNamed: 'name')
新たに updatedTimeStamp のフィールドを、timestamp 型として作るようにしました。
その次の部分が外部キーの定義です。 外部キー用のフィールドとして、'TODOUSER_name' というものを作り、最大サイズが 50 までの varchar 型として指定しています。 次に addForengnKeyFrom:to: によって、今作った userName のフィールドを、TODOUSER テーブルの 'name' に結びつけているわけです。
仕上げはクラスとテーブルのマッピングですが、ここは特に難しいところはありません。
ToDoDescriptorSystem >> descriptorForToDoItem: ('descriptors'カテゴリ)
descriptorForToDoItem: aDescriptor | table | table := self tableNamed: 'TODOITEM'. aDescriptor table: table. aDescriptor directMapping from: #id to: (table fieldNamed: 'id'). aDescriptor directMapping from: #contents to: (table fieldNamed: 'contents'). aDescriptor directMapping from: #updatedTimeStamp to: (table fieldNamed: 'updatedTimeStamp')
クラスの属性を素直にテーブルのフィールドにマッピングしています。 外部キー指定のフィールドである 'TODOUSER_name' はここには出てきません。 あくまでも classModelFor... で定義したメタ情報を、テーブルにどう対応させるかという観点で書きます。
それでは RDB のスキーマ定義を、現在のマッピングに合わせて更新しましょう。
ワークスペースで改めて以下を実行します12。
ToDoDescriptorSystem recreateTables
pgAmin で見ると、updatedtimestamp というフィールドが timestamp 型で定義されたことを確認できます。
*12 テーブルを再構成すると、テーブルに格納されていた既存のデータは消えてしまうので、recreateTables は注意して実行するようにしましょう。
今度は RDB にユーザを格納していきましょう。 まず 'user1' と 'user2' の 2 つのユーザを用意します。 以下をワークスペースで実行してください。
user1 := ToDoUser new name: 'user1'; password: 'pwd1'. user2 := ToDoUser new name: 'user2'; password: 'pwd2'. dbSession := ToDoDescriptorSystem newSession. dbSession login. dbSession beginUnitOfWork. "UnitOfWorkを開始" dbSession register: user1. "ユーザ1の登録" dbSession register: user2. "ユーザ2を登録" dbSession commitUnitOfWork. "UnitOfWorkを終了" dbSession logout
先ほどはメモの格納で save: を用いましたが、今回は register: を使っています。 save: では更新状況に関わらずにオブジェクトを強制保存するので、UnitOfWork の利点を活かせません。 register: は UnitOfWork に参加するオブジェクトを新たに登録するという意味を持ちます13。 UnitOfWork のコミット時に、登録したオブジェクトの状態変化が調べられ、データベースにまとめて反映されるのです。
また、inUnitOfWorkDo: ではなく、beginUnitOfWork と commitUnitOfWork とで、明示的に UnitOfWork の開始と終了を行っています。 UnitOfWork 内の処理が長くなる場合は、こちらの書き方が便利です。
register: の動作を確認するため、少し複雑な例を試してみます。 'user3' を用意し、ToDo 項目を 2 つほど追加した後、まとめて保存を行います。
user3 := ToDoUser new name: 'user3'; password: 'pwd3'. toDo1 := ToDoItem new contents: 'これをして'. toDo2 := ToDoItem new contents: 'あれをする'. dbSession := ToDoDescriptorSystem newSession. dbSession login. dbSession beginUnitOfWork. dbSession register: user3. "ユーザ3の登録" user3 toDoItems add: toDo1. user3 toDoItems add: toDo2. toDo2 contents: 'あれもしなきゃ'. dbSession commitUnitOfWork. dbSession logout
上記の場合、register: しているのは user3 だけですが、user3 が保持している ToDo 項目群も自動的に永続化してくれます。
格納した結果を見てみましょう。
dbSession := ToDoDescriptorSystem newSession.
dbSession login.
(dbSession readManyOf: ToDoUser) explore.
以下のようになります。
ここであえてセッションを閉じていないのは、エクスプローラで ToDo 項目を観察できるようにするためです。 user3 のツリーを展開すると、2 つの ToDo 項目をきちんと持っていることがわかります14。 確認後はリソース解放のため、データベースセッションを閉じておきましょう。
dbSession logout
*13 UnitOfWork 内で readManyOf: などで検索されたオブジェクトは自動的に register: されます。
*14 ツリー展開の前に、セッションを閉じてしまうと、ToDo 項目のプロキシが見えることになります。Glorp は一度にすべてのオブジェクトをロードせず、オンデマンドに関連するオブジェクトを取得するのでこのようなことが起こります。
準備が整ったので、実際に ToDoList アプリのセッションを ToDoSessionGlorp に切り替え、動きを追っていくことにします。
セッション部分のクラス図を抜き出すと、以下のようになっています。
デフォルトの設定では、ToDoList アプリはセッションクラスとして ToDoSessionMemory を使用しています。
ToDoList アプリのエントリとなるコンポーネントは ToDoTask なので、そのセッションクラスを ToDoSessionGlorp として指定すれば切り替え完了です15。
app := ToDoTask registerAsApplication: 'toDoList'.
app preferenceAt: #sessionClass put: ToDoSessionGlorp
ToDoSession のサブクラス群は、便利メソッドとして activate というものを提供しています。 そのため以下のようにしてもセッションクラスを切り替えられます。
ToDoSessionGlorp activate
では Web ブラウザからアクセスしてみましょう。
https://localhost:9090/seaside/toDoList
先ほど追加した 'user3' でログインすると、ToDo 項目がリストアップされていることが確認できます。 無事に Glorp 用のセッションに切り替わったということですね。
ToDoSessionGlorp がどのようにしてデータベースにアクセスしているのか、アダプタのコードを少し見てみましょう。
まずデータベースセッションの初期化の部分です。 lazy initialization (遅延初期化) により、dbSession が nil の時に、setUpDatabaseSession を呼び出すようになっています。
ToDoSession >> dbSession ('accessing'カテゴリ)
dbSession
dbSession ifNil: [dbSession := self setUpDatabaseSession].
^ dbSession
setUpDatabaseSession は ToDoSession の各サブクラスでオーバーライドされます。 ToDoSessionGlorp では以下のようになっています。
ToDoSessionGlorp >> setUpDatabaseSession ('private'カテゴリ)
setUpDatabaseSession | newDbSession | newDbSession := ToDoDescriptorSystem newSession. newDbSession login. newDbSession beginUnitOfWork. ^newDbSession
ログイン後、すかさず beginUnitOfWork で UnitOfWork を開始しているところがポイントです。
セッションの解放は ToDoSession >> unregistered で行われています。
ToDoSession >> unregistered ('private'カテゴリ)
unregistered
super unregistered.
self tearDownDatabaseSession
ToDoSessionGlorp では tearDownDatabaseSession を以下のように実装しています。 リソースを解放すべく logout を行った後、dbSession を nil にしています。
ToDoSessionGlorp >> tearDownDatabaseSession ('private'カテゴリ)
tearDownDatabaseSession
self dbSession logout.
dbSession := nil
*15 "Configure" リンクを使って手動で切り替えてもかまいません。詳しくは前回を参照してください。
では、コンポーネント側からはどのようにセッションアダプタを使っているのでしょうか。
まずはユーザ認証の箇所です。 ToDoLoginComponent >> login で、セッションアダプタに認証用のメッセージ (loginUser:password:) を送っています。
ToDoLoginComponent >> login ('private'カテゴリ)
login (self session loginUser: self userName password: self password) notNil ifTrue: [self answer: true] ifFalse: [self answer: false]
受け取った ToDoSessionGlorp では、該当するユーザが存在するかどうか、データベースに問い合わせを行っています (readOneOf:where:)。 存在した場合にはインスタンス変数 currentUser に、データベースから取得したユーザが入ることになります。
ToDoSessionGlorp >> loginUser:password: ('actions-user'カテゴリ)
loginUser: userName password: password ^currentUser := self dbSession readOneOf: ToDoUser where: [:user | user name = userName AND: [user password = password]]
where: ブロック内で大文字の AND: を使っているところが少し不自然に思えるかもしれません。 通常の and: では最適化のためインライン展開されてしまうので、Glorp が SQL を生成する時に支障が生じます。 このため Glorp 用に用意された AND: を使う必要があります (or: 等についても同様です)。
次に ToDo リストの表示部分を見てみましょう。
ToDoListComponent のレンダリングは下記のようになっています。
ToDoListComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html html heading: 'ToDoList: ', self session currentUser name. html form: [ self renderItemsOn: html. html horizontalRule. html submitButton on: #add of: self; with: '追加'. html submitButton on: #remove of: self; with: '削除' ]
ToDo 項目の一覧を表示している部分と、追加、削除のボタンを表示している部分とに分かれています。
一覧表示については、renderItemsOn: という別メソッドで行われます。
ToDoListComponent >> renderItemsOn: ('rendering'カテゴリ)
renderItemsOn: html html table: [ self itemComponents withIndexDo: [:each :idx | html tableRow: [ html tableData: [ html checkbox value: false; callback: [:selected | selected ifTrue: [self selections add: idx]]. html tableData: [html render: each]] ] ] ]
self itemConponents withIndexDo: でイテレートしています。 itemComponents の中には ToDoItemComponent のインスタンスが入っているので、render: で表示できます。 これは初期化用のメソッドである initItemList 内で、セッションアダプタ経由でセットされたものです。
ToDoListComponent >> initialize ('initialization'カテゴリ)
initialize
super initialize.
self initItemList.
ToDoListComponent >> initItemList ('initialization'カテゴリ)
initItemList itemComponents := self session toDoItems collect: [:each | ToDoItemComponent new item: each]. selections := nil
セッションアダプタ側の処理は単純です。 一覧表示の時点では、すでにログインが済んでいるので、currentUser に、toDoItems のメッセージを送っているだけです。
ToDoSessionGlorp >> toDoItems ('accessing'カテゴリ)
toDoItems
^self currentUser toDoItems
では次に更新を行っている箇所を見てみましょう。 ToDoItemComponent のレンダリング部分からたどります。
ToDoItemComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html html span style: 'margin-right: 10px';with: [html text: self item contents]. html span style: 'margin-left: 10px'; with: [ html anchor callback: [self edit]; with: '編集'. ]
ToDo 項目の内容を表示する部分と、'編集'のリンクを表示する部分とに分かれています。 コールバックの edit 内では、セッションアダプタに updateToDoItem: を送り、編集後のToDo項目をデータベースに反映させています。
ToDoItemComponent >> edit ('actions'カテゴリ)
edit
| result |
result := self request: 'ToDoの入力' label: 'OK' default: self item contents.
self item contents: result.
self session updateToDoItem: self item
セッションアダプタ側の updateToDoItem: の処理は、やはり単純です。
ToDoSessionGlorp >> updateToDoItem: ('actions-todo'カテゴリ)
updateToDoItem: aToDoItem
self dbSession commitUnitOfWorkAndContinue
edit で ToDoItem の中身が書き換わるため、ToDoSessionGlorp >> setUpDatabaseSession で開始していた UnitOfWork をコミットするというわけです。
ToDoList のようなインタラクティブなアプリケーションでは、同一セッションで、同じ ToDo 項目が何度も編集されることもあるでしょう。 そのため commitUnitOfWork ではなく、コミット後に直ちに UnitOfWork を再開する commitUnitOfWorkAndContinue を使っています。
追加や削除についても基本的には同じ流れになります。ToDo 項目の削除の処理を見てみましょう。
ToDoListComponent >> remove ('actions'カテゴリ)
remove
self selections
do: [:idx | self session removeToDoItem: (self itemComponents at: idx) item].
self initItemList
ToDoListComponent で、セッションアダプタに removeToDoItem: を送っています。
これを受けた ToDoSessionGlorp では、ドメインモデルの更新の後、やはりデータベースセッションに commitUnitOfWorkAndContinue を送ります。
ToDoSessionGlorp >> removeToDoItem: ('actions-todo'カテゴリ)
removeToDoItem: aToDoItem self currentUser toDoItems remove: aToDoItem ifAbsent: [^nil]. self dbSession delete: aToDoItem. self dbSession commitUnitOfWorkAndContinue
currentUser が集約している toDoItem から、ToDo 項目を削除しています。 その後、delete: によってデータベースからも削除を行い、最後にデータベースセッションをコミット、再開しているというわけですね。
以上で、Seaside と Glorp による CRUD(Create, Read, Update, Delete) の書き方が概観できました。
最後にまとめとして、複数の ToDo 項目を、その内容によって検索するアプリを作ってみることにしましょう。
クラス定義は以下のようにします。
WAComponent subclass: #ToDoSearchComponent
instanceVariableNames: 'matchString'
classVariableNames: ''
poolDictionaries: ''
category: 'SeasideGo-ToDo'
matchString というインスタンス変数を用意しました。 これは検索条件となる文字列を保持するためのものです。 今回は SQL の LIKE で指定するパターンをそのまま使うことにします。
例によって「アクセッサの作成」も実行しておきましょう。
初期化用のメソッドも作ります。
ToDoSearchComponent >> initialize ('initialization'カテゴリ)
initialize
super initialize.
matchString := '%'
次に、ToDoSessionGlorp 側に、matchString のパターンにマッチした ToDo 項目を検索させるメソッドを定義します。 名前は toDoItemMatches: とでもしておきましょう。
ToDoSessionGlorp >> toDoItemMatches: ('actions'カテゴリ)
toDoItemsMatches: matchString | query result | "クエリの準備" query := Query readManyOf: ToDoItem where: [:item | item contents like: matchString]. query orderBy: [:item | item updatedTimeStamp descending]. "検索の実行" result := self dbSession execute: query. self tearDownDatabaseSession. ^result
今回は Query というクラスを使い、少し複雑な問い合わせを表現しています。
1 行目で ToDoItem の内容が matchString のパターンに合うものという指定をし (readManyOf:where:)、その上で更新日時が新しいもの順に結果が並ぶようにしています (orderBy:)。 作成した Query インスタンスは、execute: を使って実行することができます。
execute: の後には self tearDownDatabaseSession で Glorp のセッションを閉じていますが、これは次回の検索に備えるためのものです。 セッションが保持したキャッシュを破棄し、常に最新の結果をデータベースから取得できるようにしています16。
さて、残るは ToDoSearchComponent のレンダリングです。 上部に検索結果がテーブルで表示され、下部には条件となる文字列を入れられるようにしました。
ToDoSearchComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html
" ToDoItemのテーブル表示"
html table with: [
(self session toDoItemsMatches: self matchString) do: [:each |
html tableRow with: [
html tableData: [html text: each updatedTimeStamp].
html tableData: [html text: each contents]
]
]
].
html horizontalRule.
"検索条件を入れるフォームの表示"
html form: [
html textInput callback: [:v | matchString := v ]; with: self matchString.
html submitButton.
]
これでできあがりです。ワークスペースでアプリケーションの登録を行いましょう。
app := ToDoSearchComponent registerAsApplication: 'toDoSearch'.
app preferenceAt: #sessionClass put: ToDoSessionGlorp
Web ブラウザから動作確認をします。
https://localhost:9090/seaside/toDoSearch
以下のような実行画面となります。 ToDoList アプリのほうも別途立ち上げ、いろいろなユーザでログインし、内容を追加、編集して、検索結果の変化を見てみると楽しいでしょう。
余力のある人は、並び順を変えられるようするなどの拡張を行ってみてください。
*16 ToDoList アプリケーションのほうでは終了時まで破棄を行っていません。データベースセッションのライフサイクルは状況に応じて変わってきます。
ソースコードをまとめて取り込めるように、.st ファイルとして置いておきます。 ダウンロードしてお使いください。
「Glorp を試す」
SeasideGo-Lesson-Chap5-Glorp-Lesson.st
「ToDoList アプリの導入」
SeasideGo-Lesson-Chap5-ToDo-ModelExtension.st
「検索用アプリの作成」
SeasideGo-Lesson-Chap5-ToDo-Search.st
今回は Seaside と Glorp との連携を扱いました。 マッピングが多少面倒に思えるかもしれませんが、慣れてしまえば既存のドメインモデルに変更が一切及ばない行儀の良さが気に入ってくると思います。 UnitOfWork のサポートにより、RDB を利用しながらもオブジェクトレベルでの操作ができるため、見通しも良く、パフォーマンス的にも有利になります。
さて、この連載も次回が最終回となります。 仕上げとして、今まで取り扱ってこなかった、コンポーネントの見栄えの部分にスポットを当てていきます。 CSS や Ajax を Seaside から利用する方法を扱います。 また Seaside の今後の動向にも触れる予定です。 ご期待ください。
© 2009 Masashi Umezawa |
|