ユーザーインタフェースをもつウェブアプリケーションは、MVC (Model View Controller) 構造をデザインパターンとして採用することが多いです。Aloneでは、MVCを強制はしませんが、積極的にサポートはするというスタンスです。
また、モデルは内部状態を更新したり削除したりといった典型的な操作があり、CRUD として知られています。セッションの項で説明したとおり、この場合もリクエストをまたいでモデルが存在しなければなりませんが、モデルの内部状態の保存にはセッションが使われるケースは少なくRDBMS等に保存するケースが多くなります。
ここでは、簡単な住所録アプリを作る事を例にして、MVCとCRUD、双方の実装の仕方を説明します。
最初は簡単のため、入力できる項目は「名前」と「住所」の2つにします。 画面遷移は、以下の通りに設計しました。
今回も既存の既にあるアプリケーションベースに手順をおって開発する方が楽なので、またhello worldをベースにしてみます。リスト表示の時ややこしいので、READMEファイルは削除しておいてください。
cd controllers cp -r 00_hello address_book cd address_book rm README
コントローラ名を、変更します。併せてrequireを以下の通り修正してください。
require "alone" class AddressBookController < AlController
モデルは、名前をAddressBook 、操作としてCRUDの機能を持つよう設計します。ゼロから実装してもいいのですが、Aloneには AlPersist データ永続化モジュール というデータの永続化機構があり、CRUDを備えているのでそれを継承することにします。
require "al_persist_file" class AddressBook < AlPersistFile DB_FILE = "#{AL_TEMPDIR}/address_book.dat" def initialize() super( DB_FILE ) end end
簡単のため、RDBMSではなく、単純なファイルを保存先にするAlPersistFileを使います。
コンストラクタでファイル名を指定し、データの保存先を明らかにします。モデルにCRUDの機能が必要なだけなら、コードはこれだけです。 残りの create(), read(), update(), delete() ほかは、AlPersistクラスから継承されます。
たとえば、createを試してみましょう。
require "alone" require_relative "./address_book" class AddressBookController < AlController def action_test_create() address_book = AddressBook.new p address_book.create({:name=>"木田太良", :address=>"兵庫県宝塚市"}) end
このアクションを実行し、画面にtrueと表示されれば成功です。
http://localhost:10080/index.rb?ctrl=address_book&action=test_create
ファイル、/tmp/address_book.dat ができ、データをエンコードした行が保存されています。
入力フォームを表示、及び入力された値を受け取るために、フォームオブジェクトを作ります。
今回は、名前と住所の2つのテキスト入力が必要です。名前は必須入力としておきます。 加えて以下の2アイテムを追加します。
require "alone" require_relative "./address_book" class AddressBookController < AlController # コンストラクタ def initialize() @form = AlForm.new( AlText.new("name", :label=>"名前", :required=>true), AlText.new("address", :label=>"住所"), AlSubmit.new("決定"), AlHidden.new("id"), ) end
<%= header_section %> <%= body_section %> <%= @form.make_tiny_form() %> <%= footer_section %>
これで入力フォームが表示されます。
このアドレス帳の最初の表示はリスト(一覧)表示ですので、それを実装しましょう。
コントローラでは、モデルに全件を得るよう指示します。得たデータは@alldataへ入れます。
# デフォルトアクション(リスト表示) def action_index() address_book = AddressBook.new @alldata = address_book.all() AlTemplate.run("index.rhtml") end
テンプレートでは、@alldataに入っている全件データをeachで1件ずつ取り出し、テーブルを使って表示します。
この例では、モデルのcreateテストで使ったデータが1件のみ表示されています。
新規(create)、編集(update)、削除(delete) の選択肢を追加します。
URLへは、データを特定するためのIDを追加するとともに、各操作とも以下の通り2段階の動作となるので、それぞれアクション名をわかるようにつけます。
新規(create)
新規フォーム表示 action_create_form ⇒ 確定 action_create_exec
更新(update)
更新フォーム表示 action_update_form ⇒ 確定 action_update_exec
削除(delete)
確認画面表示 action_delete_confirm ⇒ 確定 action_delete_exec
<%= header_section %> <%= body_section %> <table class="al-list-table"> <tr> <th>名前 <th>住所 <th>(操作) </tr> <% @alldata.each do |item| %> <tr> <td><%=h item[:name] %> <td><%=h item[:address] %> <td> <a href="<%=h make_uri(action:"update_form", id:item[:id]) %>">更新</a> <a href="<%=h make_uri(action:"delete_confirm", id:item[:id]) %>">削除</a> </tr> <% end %> </table> <br> <a href="<%=h make_uri(action:"create_form") %>">新規</a><br> <%= footer_section %>
新規登録アクションを定義します。
新規フォーム表示 action_create_form ⇒ 確定 action_create_exec の2段階になります。
空のフォームを表示すれば良いので、簡単です。
def action_create_form() @form.action = make_uri(:action=>"create_exec") AlTemplate.run("form.rhtml") end
<%= header_section %> <%= body_section %> <%= @form.get_messages_by_html() %> <%= @form.make_tiny_form() %> <%= footer_section %>
def action_create_exec() # 新規登録時は、IDは不要(自動採番)なので取り除く @form.delete_widget( :id ) # 入力フォームにエラーがあれば、フォーム再表示 if !@form.validate() AlTemplate.run("form.rhtml") return end # モデルオブジェクトを生成し、createする。 address_book = AddressBook.new address_book.create( @form.values ) # リダイレクトを使って、デフォルト(リスト表示)へ戻る。 Alone.redirect_to( make_uri() ) end
バリデーション、モデルによる登録実行、結果表示という流れです。
今回 AlPersistFile を使うので、IDは自動採番となり、ブラウザから送られてきたデータは不要です。あらかじめフォームオブジェクトから取り除いておきます。
モデルの create メソッドにバリデーション後のデータ一式を渡して、登録を行います。エラーチェックはしていませんが、より厳密にするためには create メソッドの戻り値を確認すると良いでしょう。
結果表示は、独立して行わず、リスト表示に戻しています。実用的なアプリケーションでも、このようにリダイレクトによって2重POSTによる誤動作を防ぎます。
更新アクションを定義します。
更新フォーム表示 action_update_form ⇒ 確定 action_update_exec の2段階になります。
def action_update_form() # データ特定用のIDを得る id = AlForm.get_parameter( AlInteger.new("id") ) if !id return end # モデルオブジェクトを生成し、readする。 address_book = AddressBook.new if !address_book.read({:id=>id}) return end # フォームに初期値として設定し表示する @form.values = address_book.values @form.action = make_uri(:action=>"update_exec") AlTemplate.run("form.rhtml") end
更新のフォーム表示は、新規登録の時と違い、既存データが入力された状態で表示しなければなりません。そのため、IDをキーにモデルを使ってデータをreadし、その内容をフォームにセットする動作が必要です。
IDはフォームを使って取り出すこともできますが、プライマリキー等の単純なデータ1つを取得したい場合には、この例の通りget_parameterメソッドを使うとより簡単になります。
テンプレートは、新規登録と同じです。
def action_update_exec() # 入力フォームにエラーがあれば、フォーム再表示 if !@form.validate() AlTemplate.run("form.rhtml") return end # モデルオブジェクトを生成し、updateする。 address_book = AddressBook.new address_book.update( @form.values ) # リダイレクトを使って、デフォルト(リスト表示)へ戻る。 Alone.redirect_to( make_uri() ) end
新規登録とほとんど同じで、バリデーション、モデルによる更新実行、結果表示という流れです。新規登録との違いは、IDを使うためにフォームオブジェクトから取り除く必要が無いことと、モデルのメソッドが createではなく、update であること位です。
削除アクションを定義します。
確認画面表示 action_delete_confirm ⇒ 確定 action_delete_exec の2段階になります。
def action_delete_confirm() # データ特定用のIDを得る id = AlForm.get_parameter( AlInteger.new("id") ) if !id return end # モデルオブジェクトを生成し、readする。 address_book = AddressBook.new if !address_book.read({:id=>id}) return end # 簡易画面の生成のため、フォームに入れてテンプレートに渡す @form.values = address_book.values AlTemplate.run("delete_confirm.rhtml") end
削除確認画面の表示も一度データを表示したいので、更新と同じようにIDの値を使ってreadを行い、その内容をフォームの機能を使って表示します。
<%= header_section %> <%= body_section %> <%= @form.make_tiny_sheet() %> 削除してもよろしいですか?<br> <a href="<%=h make_uri() %>">いいえ</a> <a href="<%=h make_uri(:action=>"delete_exec", :id=>@form[:id]) %>">はい</a> <%= footer_section %>
def action_delete_exec() # データ特定用のIDを得る id = AlForm.get_parameter( AlInteger.new("id") ) if !id return end # モデルオブジェクトを生成し、deleteする address_book = AddressBook.new if !address_book.delete({:id=>id}) return end # リダイレクトを使って、デフォルト(リスト表示)へ戻る。 Alone.redirect_to( make_uri() ) end
削除の実行は、データを特定するIDのみが必要となるので、ここでもget_parameterメソッドでIDのみを取得します。IDを使って、モデルのdeleteメソッドによりデータを削除します。その後、リダイレクトによりリスト表示に戻ります。
名前と住所だけでは実用的ではないので、登録項目を増やしてみます。
例として、電話番号入力欄と、登録日付を追加します。
コントローラでは、フォームオブジェクトに項目を追加します。
def initialize() @form = AlForm.new( AlText.new("name", :label=>"名前", :required=>true), AlText.new("address", :label=>"住所"), AlText.new("tel", :label=>"電話"), # 追加 AlSubmit.new("決定"), AlHidden.new("id"), ) end
テンプレートでは、リスト表示テンプレートに項目を追加します。
<%= header_section %> <%= body_section %> <table class="al-list-table"> <tr> <th>名前 <th>住所 <th>電話 <!-- 追加 --> <th>(操作) </tr> <% @alldata.each do |item| %> <tr> <td><%=h item[:name] %> <td><%=h item[:address] %> <td><%=h item[:tel] %> <!-- 追加 --> <td> <a href="<%=h make_uri(action:"update_form", id:item[:id]) %>">更新</a> <a href="<%=h make_uri(action:"delete_confirm", id:item[:id]) %>">削除</a> </tr> <% end %> </table> <br> <a href="<%=h make_uri(action:"create_form") %>">新規</a><br> <%= footer_section %>
AlPersistFileを使う場合は、これだけで項目の増減が可能です。RDBを使う場合は、データベース側のカラム増減も併せて行う必要があります。
登録日は、手入力するのは無駄なので自動入力としたいでしょう。 ですから、モデルに手を加えて新規登録(create)時に現在時刻を追加するようにし、表示は一覧表示のみに出るようにします。
def create( values = nil ) values[:created_at] = Time.now super( values ) end
<%= header_section %> <%= body_section %> <table class="al-list-table"> <tr> <th>名前 <th>住所 <th>電話 <th>登録日 <!-- 追加 --> <th>(操作) </tr> <% @alldata.each do |item| %> <tr> <td><%=h item[:name] %> <td><%=h item[:address] %> <td><%=h item[:tel] %> <td><%=h item[:created_at] %> <!-- 追加 --> <td> <a href="<%=h make_uri(action:"update_form", id:item[:id]) %>">更新</a> <a href="<%=h make_uri(action:"delete_confirm", id:item[:id]) %>">削除</a> </tr> <% end %> </table> <br> <a href="<%=h make_uri(action:"create_form") %>">新規</a><br> <%= footer_section %>
新規登録を行なった時の日時が追加され、更新では変わらないという動作が確認できると思います。
プログラム全体を再掲します。
require "al_persist_file" class AddressBook < AlPersistFile DB_FILE = "#{AL_TEMPDIR}/address_book.dat" def initialize() super( DB_FILE ) end def create( values = nil ) values[:created_at] = Time.now super( values ) end end
# coding: utf-8 require "alone" require_relative "./address_book" class AddressBookController < AlController # # コンストラクタ # def initialize() @form = AlForm.new( AlText.new("name", :label=>"名前", :required=>true), AlText.new("address", :label=>"住所"), AlText.new("tel", :label=>"電話"), AlSubmit.new("決定"), AlHidden.new("id"), ) end # # デフォルトアクション(リスト表示) # def action_index() address_book = AddressBook.new @alldata = address_book.all() AlTemplate.run("index.rhtml") end # # 新規登録 フォーム表示 # def action_create_form() @form.action = make_uri(:action=>"create_exec") AlTemplate.run("form.rhtml") end # # 新規登録 実行 # def action_create_exec() # 新規登録時は、IDは不要(自動採番)なので取り除く @form.delete_widget( :id ) # 入力フォームにエラーがあれば、フォーム再表示 if !@form.validate() AlTemplate.run("form.rhtml") return end # モデルオブジェクトを生成し、createする。 address_book = AddressBook.new address_book.create( @form.values ) # リダイレクトを使って、デフォルト(リスト表示)へ戻る。 Alone.redirect_to( make_uri() ) end # # 更新 フォーム表示 # def action_update_form() # データ特定用のIDを得る id = AlForm.get_parameter( AlInteger.new("id") ) if !id return end # モデルオブジェクトを生成し、readする。 address_book = AddressBook.new if !address_book.read({:id=>id}) return end # フォームに初期値として設定し表示する @form.values = address_book.values @form.action = make_uri(:action=>"update_exec") AlTemplate.run("form.rhtml") end # # 更新 実行 # def action_update_exec() # 入力フォームにエラーがあれば、フォーム再表示 if !@form.validate() AlTemplate.run("form.rhtml") return end # モデルオブジェクトを生成し、updateする。 address_book = AddressBook.new address_book.update( @form.values ) # リダイレクトを使って、デフォルト(リスト表示)へ戻る。 Alone.redirect_to( make_uri() ) end # # 削除 確認画面 # def action_delete_confirm() # データ特定用のIDを得る id = AlForm.get_parameter( AlInteger.new("id") ) if !id return end # モデルオブジェクトを生成し、readする。 address_book = AddressBook.new if !address_book.read({:id=>id}) return end # 簡易画面の生成のため、フォームに入れてテンプレートに渡す @form.values = address_book.values AlTemplate.run("delete_confirm.rhtml") end # # 削除 実行 # def action_delete_exec() # データ特定用のIDを得る id = AlForm.get_parameter( AlInteger.new("id") ) if !id return end # モデルオブジェクトを生成し、deleteする address_book = AddressBook.new if !address_book.delete({:id=>id}) return end # リダイレクトを使って、デフォルト(リスト表示)へ戻る。 Alone.redirect_to( make_uri() ) end end
<%= header_section %> <%= body_section %> <table class="al-list-table"> <tr> <th>名前 <th>住所 <th>電話 <th>登録日 <th>(操作) </tr> <% @alldata.each do |item| %> <tr> <td><%=h item[:name] %> <td><%=h item[:address] %> <td><%=h item[:tel] %> <td><%=h item[:created_at] %> <td> <a href="<%=h make_uri(action:"update_form", id:item[:id]) %>">更新</a> <a href="<%=h make_uri(action:"delete_confirm", id:item[:id]) %>">削除</a> </tr> <% end %> </table> <br> <a href="<%=h make_uri(action:"create_form") %>">新規</a><br> <%= footer_section %>
<%= header_section %> <%= body_section %> <%= @form.get_messages_by_html() %> <%= @form.make_tiny_form() %> <%= footer_section %>