Rails: ボタンを押したら複数のモデルにデータを登録
夏休みを開けてから、何回かアプリを更新しようと努力してきましたが…大きな機能を実装しようとすると、どうしても時間切れになってブログが書けなくなってしまうので、今回からは、こまかく砕いて実装して行きたいと思います。
…そのほうがgithubのContributions(更新頻度に応じて緑が濃くなるやつ)も鮮やかになって良いしね。
■ briefing
今回はナビゲーションバーに追加した「新規サイト登録」から、新規ページを登録すると、その下にある「登録サイト一覧」で確認できるようにしていきます。
技術的障害になるかどうかは分かりませんが、下記ページはMainsコントローラー下にあり、新規サイト登録はsitesコントローラー、そして、登録サイト一覧はsbsc (subscribe)コントローラー配下ということで、別のモデル、コントローラー配下にあります。
私のRailsに対する理解度ではこの辺が今回の開発の障害になるのではないか…と踏んでいます。
■ ブランチの作成
今回はサイト登録機能を実装するので、"creSite"というブランチを作成します。
~/workspace/xapp8(master)$ git checkout -b "creSite"
~/workspace/xapp8(creSite)$
■ 現状
上記画面を構成する為に書かれているコードは以下の通りになっています。
ログイン後の画面を定義しているmains/indexから、パーシャルのaddsiteを読んでいます。この辺の設計は"Ruby on Railsチュートリアル"を参考に設計しています。
app/views/mains/index.html.erb
<div class="row">
<aside class="col-md-2">
<div class="nav-top"><%= render 'shared/navmenu' %></div>
<div class="nav-middle"><%= render 'shared/addsite' %></div>
<div class="nav-bottom"><%= render 'shared/sitelist' %></div>
</aside>...
</div>
次にmainsコントローラーのindexは以下のようになっています。インスタント変数"@sReg"にsiteモデルのオブジェクトを作成しておきます。
app/controllers/main_controller.rb
class MainsController < ApplicationController
...
def index
@main = Main.where(:user_id => current_user.id) # フィード取得用
@sList = Sbsc.where(:user_id => current_user.id) # Site List 購読中のサイト一覧
@regSite = Site.new # RegistrySite サイト登録用
end
end
そして、indexからrenderコマンドで呼ばれているパーシャル"addsite"は以下の通り。form_forが上記でnewしたモデルオブジェクトを呼んで、登録画面を出力しています。
app/views/shared/_addsite.html.erb
<h5>新規サイト登録</h5>
<%= form_for(@regSite) do |f|%>
<%= f.url_field :url, placeholder: "URLを入力してください" %>
<%= f.submit "登録", class: "btn btn-xs btn-primary" %>
<% end %>
■ 今回の実装方針
データーベースの関係図は以下の通りとなっています(ORマッパーの概念にナカナカなじめず、E-R図になっているのは申し訳ない…)。
「現状」の項目で説明した"addsite.html.erb"からURLをクリックすると、以下のような動きをすることを想定します。
① siteモデルに同様のURLが存在しないことを確認(存在する場合は④へ)
② feedjiraを使ってurlサイトのページタイトルを取得
③ siteモデルに情報を登録
④ sbscモデルに購読情報を登録
■ サイトリストに登録(③)
mains_controllerでモデルオブジェクトを作成(Site.new)して、インスタンス変数の"@sReg"に格納しました。これをフォームとして画面に生成して、submitすると、siteコントローラーにcreateアクションでパラメーターが渡されます。
ですので、siteコントローラーを作成して、createアクションにデータベースに書き込めるように定義します。ここら辺はscaffold生成時のテンプレートからパクっていますが、"addsite_params"メソッドで、フォームに入力されたURLを取得して、"create"メソッドでデータベースに書き込んだ後、元のページに戻るようにしてあります。
app/controllers/sites_controller.rb
class SitesController < ApplicationController
#ログインしているかどうか確認
before_filter :authenticate_user!def create
@regSite = Site.new(addsite_params)
@regSite.save
redirect_to mains_path
end
private
# Never trust parameters from the scary internet, only allow the white list through.
def addsite_params
params.require(:site).permit(:url)
end
end
試しにURLを"http://www.google.co.jp"と入力してからSubmitして、"Sites"テーブルにレコードが追加されているかどうか確認してみます。SELECTの結果が表示されれば登録は成功です。
~/workspace/xapp8(creSite*)$ rails s
(アプリ画面から新規サイトを登録)
~/workspace/xapp8(creSite*)$ rails dbconsole
sqlite> select * from sites where URL="http://www.google.co.jp";
54||http://www.google.co.jp||2014-08-30 02:18:01.513422|2014-08-30 02:18:01.513422
■ 重複の排除(①)
ちょっとタイトルが変ですが、いわゆるインサート時に重複を排除する仕組みを実装します。
まずは、Railsの機能で、モデルにバリデイトを追加します。今回はお試してURLの正規表現によるチェックも付け加えてみました。
app/models/site.rb
class Site < ActiveRecord::Base
VALID_URL_REGEX = /^https?\:\/\/[^ ]*$/i
validates :url, presence: true, format: { with: VALID_URL_REGEX }, uniqueness: { case_sensitive: false }
end
また、データベース側でも一意性を担保するように書き換えて行きます。ウェブ上では新しいマイグレーションファイルを作って、インデックスを追加するやり方が多く見られますが、私はどうしてもテーブル単位での管理を意識してしまいますので、既存のsiteファイルを修正しました。
db/migrate/[timestamp]_create_site.rb
class CreateSites < ActiveRecord::Migration
def change
create_table :sites do |t|
t.string :title
t.string :url
t.datetime :date
t.timestamps
end
# インデックスと一意制約の追加
add_index :sites, :url, unique: true
end
end
今回は既にSitesテーブルにレコードが存在する場合は、Sitesテーブルに登録しません。
app/controllers/sites_controller.rb
class SitesController < ApplicationController
...def create
@regSite = Site.new(addsite_params)
if @dup = Site.find_by(:url => "#{@regSite.url}") # duplicate 重複している場合レコードを取得
puts "--------------------" #デバッグ用
puts "重複あり id=#{@dup.id}"
puts "--------------------" #デバッグ用
redirect_to mains_path
else
puts "--------------------" #デバッグ用
puts "重複なし"
puts "--------------------" #デバッグ用
@regSite.save!
redirect_to mains_path
end
end...
end
ここまで出来たら実際に一意性が適用されているかどうか確かめてみます。
~/workspace/xapp8(creSite*)$ rake db:migrate:reset
~/workspace/xapp8(creSite*)$ rake db:sample
rake aborted!
ArgumentError: The provided regular expression is using multiline anchors (^ or $), which may present a security risk. Did you mean to use \A and \z, or forgot to
add the :multiline => true option?
データベースを削除して、"rake db:sample"でテストデータを生成(Ruby on Railsのサンプルデータ生成手順みたいなかたちで、"task sample"を作成してあります。)してみたら、エラーが発生しました。どうやらURL Validateの正規表現に問題があるようです。正規表現の先頭と末尾にある ^ と $ を外しました。
app/models/site.rb
class Site < ActiveRecord::Base
VALID_URL_REGEX = /https?\:\/\//[^ ]*/i
validates :url, presence: true, format: { with: VALID_URL_REGEX }, uniqueness: { case_sensitive: false }
end
再度、データ投入を実行してみます。コマンドライン上にputsのコメントが以下のように表示されていると思います。
~/workspace/xapp8(creSite*)$ rake db:migrate:reset
~/workspace/xapp8(creSite*)$ rake db:sample
~/workspace/xapp8(creSite*)$ rake test:prepare
~/workspace/xapp8(creSite*)$ rails s
(アプリ画面から"http://www.google.co.jp"を登録)
...
--------------------
重複あり id=55
--------------------(アプリ画面から"https://www.google.co.jp"を登録)
--------------------
重複なし
--------------------
■ 購読リストに登録(④)
ここは既知の技能で登録できる筈なので簡単です。"重複の排除(①)"で登録したURLにはタイトルが含まれていないので、一覧を表示した際にタイトルが表示されません。まずは、この問題を解消する為にデータベースの書き換えを行います。
~/workspace/xapp8(creSite*)$ rails dbconsole
sqlite > update sites set title = 'グーグル・ジャパン' where url like '%google%';
sqlite> select * from sites where like '%google%';
57|グーグル・ジャパン|http://www.google.co.jp||2014-08-30 06:41:50.917404|2014-08-30 06:41:50.917404
58|グーグル・ジャパン|https://www.google.co.jp||2014-08-30 06:42:09.308552|2014-08-30 06:42:09.308552
sqlite > .quit
Siteコントローラーで、先ほどレコードの有無をチェックしましたので、その後のロジックでsbscモデルに対する書き込みのロジックを追記します。
app/controllers/sites_controller.rb
...
def create
@regSite = Site.new(addsite_params)
if @dup = Site.find_by(:url => "#{@regSite.url}") # duplicate 重複している場合レコードを取得
# Sbscsテーブルに購読情報を登録
@regSub = Sbsc.new
@regSub.user_id = current_user.id
@regSub.site_id = @dup.id
@regSub.save!
# 元のページにリダイレクト
redirect_to mains_path
else
@regSite.save! # Sitesテーブルにレコードを登録
@dup = Site.find_by(:url => "#{@regSite.url}") #Sitesテーブルからレコードを取得
# Sbscsテーブルに購読情報を登録
@regSub = Sbsc.new
@regSub.user_id = current_user.id
@regSub.site_id = @dup.id
@regSub.save!
# 元のページにリダイレクト
redirect_to mains_path
end
end
サーバーを起動して、試しに"http://www.google.co.jp"を登録します。すると、サイト一覧の一番下に"グーグル・ジャパン"が登録されたかと思います。
■ Feedjiraでサイト情報を取得 (②)
ここにきて、RSSフィードのURLとサイトのURLが異なっているのに、同アプリ上では特に管理してこなかったことに気づきました。sitesテーブルのurlカラムを削除して、siteurlカラムとrssurlカラムに変更しました。また、この変更に伴って、該当カラムを参照している_sitelist.html.erb、_addsite.html.erb、sites_controller.rb(addsite_paramsメソッド)も修正してあります。
Feedjiraでサイト情報を取得するには、フォームから受け取ったURLを引数にFeedjiraをコールします。正しく結果を受け取れているようなら、Sitesテーブルに値をInsertして処理を続行します。失敗した場合はflashでエラー出力するようにしました。
Sitesテーブルに値が登録されている、もしくは、登録した場合には、Sbscsテーブルに購読情報を登録するように定義しました。ここの部分については、MVC的にはSbscs_Controller.rbで定義するようにしたほうがいいかもしれませんが、とりあえず。
app/controllers/sites_controller.rb
def create
@regSite = Site.new(addsite_params)
# duplicate URLが重複している場合はレコードを取得。できない場合はthen以下。
unless @dup = Site.find_by(:rssurl => "#{@regSite.rssurl}") then
# Feedjiraによるフィード取得処理
feed = Feedjira::Feed.fetch_and_parse "#{@regSite.rssurl}"
if feed == 404 then
flash[:alert] = "エラー: #{@regSite.rssurl}のフィードが見つかりませんでした"
redirect_to mains_path
return
else
@regSite.title = feed.title
@regSite.siteurl = feed.url
@regSite.save! # Sitesテーブルにレコードを登録
@dup = Site.find_by(:rssurl => "#{@regSite.rssurl}") #Sitesテーブルからレコードを取得
end
end
# Sbscsテーブルに購読情報を登録
@regSub = Sbsc.new
@regSub.user_id = current_user.id
@regSub.site_id = @dup.id
unless @regSub.save then
flash[:alert] = "エラー: '#{@regSub.site.title}'は既に登録されています"
end
# 元のページにリダイレクト
redirect_to mains_path
end
Sbscsテーブル書き込み時に、user_idとsite_idの組み合わせが一意になるようにモデルを定義して、インデックスに複合キーを定義します。
app/models/sbsc.rb
class Sbsc < ActiveRecord::Base
...
validates_uniqueness_of :site_id, :scope => :user_id
end
db/migrate/[timestamp]_create_sbsc.rb
class CreateSbscs < ActiveRecord::Migration
def change
...add_index :sbscs, [:user_id, :site_id], unique: true
end
end
■ 最後に
いつも通り、各種サービスにアップロードして終了です。
~/workspace/xapp8(creSite*)$ git add .
~/workspace/xapp8(creSite)$ git commit -m"ページ登録機能"
~/workspace/xapp8(creSite)$ git checkout master
~/workspace/xapp8(master)$ git merge creSite
~/workspace/xapp8(master)$ git push
~/workspace/xapp8(master)$ git push heroku
~/workspace/xapp8(master)$ heroku rake db:migrate:reset