====== MVCとCRUD ====== ユーザーインタフェースをもつウェブアプリケーションは、[[https://ja.wikipedia.org/wiki/Model_View_Controller|MVC (Model View Controller)]] 構造をデザインパターンとして採用することが多いです。Aloneでは、MVCを強制はしませんが、積極的にサポートはするというスタンスです。 {{:prog_cgi:mvc-process.png?direct&200|}} MVCパターン(Wikipediaより) また、モデルは内部状態を更新したり削除したりといった典型的な操作があり、[[https://ja.wikipedia.org/wiki/CRUD|CRUD]] として知られています。セッションの項で説明したとおり、この場合もリクエストをまたいでモデルが存在しなければなりませんが、モデルの内部状態の保存にはセッションが使われるケースは少なくRDBMS等に保存するケースが多くなります。 ここでは、簡単な住所録アプリを作る事を例にして、MVCとCRUD、双方の実装の仕方を説明します。 ====== 全体設計 ====== 最初は簡単のため、入力できる項目は「名前」と「住所」の2つにします。 画面遷移は、以下の通りに設計しました。 {{:prog_cgi:mvc_page_transition.png?nolink|}} ====== 実装開始 ====== 今回も既存の既にあるアプリケーションベースに手順をおって開発する方が楽なので、また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:start|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アイテムを追加します。 * フォームの自動生成を使いたいので、決定ボタンを追加。 * データを特定するためのIDが必要となるので、それを持ち回るためのIDを非表示で追加。 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 %> これで入力フォームが表示されます。 {{:prog_cgi:mvc_form.png?nolink|}} ===== リスト(一覧)表示 ===== このアドレス帳の最初の表示はリスト(一覧)表示ですので、それを実装しましょう。 コントローラでは、モデルに全件を得るよう指示します。得たデータは@alldataへ入れます。 # デフォルトアクション(リスト表示) def action_index() address_book = AddressBook.new @alldata = address_book.all() AlTemplate.run("index.rhtml") end テンプレートでは、@alldataに入っている全件データをeachで1件ずつ取り出し、テーブルを使って表示します。 <%= header_section %> <%= body_section %> <% @alldata.each do |item| %> <% end %>
名前 住所
<%=h item[:name] %> <%=h item[:address] %>
<%= footer_section %>
==== 実行結果 ==== この例では、モデルのcreateテストで使ったデータが1件のみ表示されています。 {{:prog_cgi:mvc_list1.png?nolink|}} ==== 他のアクションへのリンクを追加 ==== 新規(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 %> <% @alldata.each do |item| %> <% end %>
名前 住所 (操作)
<%=h item[:name] %> <%=h item[:address] %> ">更新 ">削除

">新規
<%= footer_section %>
{{:prog_cgi:mvc_list2.png?nolink|}} ===== 新規登録 ===== 新規登録アクションを定義します。\\ 新規フォーム表示 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 %> {{:prog_cgi:mvc_form.png?nolink|}} ==== 登録実行 ==== 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メソッドを使うとより簡単になります。 テンプレートは、新規登録と同じです。 {{:prog_cgi:mvc_update.png?nolink|}} ==== 更新実行 ==== 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() %> 削除してもよろしいですか?
いいえ @form[:id]) %>">はい <%= footer_section %>
{{:prog_cgi:mvc_delete.png?nolink|}} ==== 削除実行 ==== 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 %> <% @alldata.each do |item| %> <% end %>
名前 住所 電話 (操作)
<%=h item[:name] %> <%=h item[:address] %> <%=h item[:tel] %> ">更新 ">削除

">新規
<%= footer_section %>
==== 実行結果 ==== {{:prog_cgi:mvc_list3.png?nolink|}} AlPersistFileを使う場合は、これだけで項目の増減が可能です。RDBを使う場合は、データベース側のカラム増減も併せて行う必要があります。 ===== 登録日の追加 ===== 登録日は、手入力するのは無駄なので自動入力としたいでしょう。 ですから、モデルに手を加えて新規登録(create)時に現在時刻を追加するようにし、表示は一覧表示のみに出るようにします。 def create( values = nil ) values[:created_at] = Time.now super( values ) end <%= header_section %> <%= body_section %> <% @alldata.each do |item| %> <% end %>
名前 住所 電話 登録日 (操作)
<%=h item[:name] %> <%=h item[:address] %> <%=h item[:tel] %> <%=h item[:created_at] %> ">更新 ">削除

">新規
<%= footer_section %>
==== 実行結果 ==== {{:prog_cgi:mvc_list4.png?nolink|}} 新規登録を行なった時の日時が追加され、更新では変わらないという動作が確認できると思います。 ====== プログラム全体 ====== プログラム全体を再掲します。 * モデル address_book.rb * コントローラ main.rb * テンプレート(ビュー) index.rhtml, form.rhtml, delete_confirm.rhtml 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 %> <% @alldata.each do |item| %> <% end %>
名前 住所 電話 登録日 (操作)
<%=h item[:name] %> <%=h item[:address] %> <%=h item[:tel] %> <%=h item[:created_at] %> ">更新 ">削除

">新規
<%= footer_section %>
<%= header_section %> <%= body_section %> <%= @form.get_messages_by_html() %> <%= @form.make_tiny_form() %> <%= footer_section %> <%= header_section %> <%= body_section %> <%= @form.make_tiny_sheet() %> 削除してもよろしいですか?
いいえ @form[:id]) %>">はい <%= footer_section %>