niki12260714の日記

フリーランスのITエンジニアの呟き。

Ruby学習20日目:トランザクション、フラッシュ、merge

イベントに向けて原稿書いたり、アニメ化追っかけていたりで進捗悪いんですけど、予約を取るところまでできたので。

【やりたいこと】
・アイテム製作者のページにアクセスすると、本日以降にイベントがあるアイテムの中で、取り置き可能なアイテムが表示される
→取り置き可能とは、取り置き上限数に対し、取り置き予約の数が下回っている状態を指す
・予約を確定するときには、取り置きテーブルに対してトランザクションを張り、「依頼者のメールアドレスがブラックリストテーブルに登録されていないか」「取り置き上限数に既に達していないか」をチェックし、問題なければ登録する
・登録完了すると、取り置き依頼をしたアイテムをリスト表示する画面に遷移する

前回までで製作者の画面を作ったので、実際に取り置き依頼をするためのアイテム画面を作りました。

f:id:niki12260714:20171001182007j:plain

こんな感じで、取り置き依頼をするアイテムの情報が表示され、取り置き依頼情報を入力します。
取り置き数入力欄は「最小値は1、最大値は上限数-現時点の予約数」となります。
なので、Viewのコードに以下を記載。

<%
reserve_limit = @item[0].max_reserve_num.to_i - @item[0].reserve_lists_count.to_i
%>
<label for="reserve_num">取り置き数:</label>
<%= number_field_tag :reserve_num, '1', min: 1, max: reserve_limit %>

 ここで上限数に達しないように制御かけてますけど、実際にはこの画面を見て考えている最中に他の人が予約をかけて上限数に達してしまうかもしれない。
なので、取り置き依頼ボタンを押し、取り置き登録処理をするときにトランザクションを張ることにします。

f:id:niki12260714:20171001182736j:plain

※requestコントローラー

29行目からトランザクションを張っています。
この中で32行目、39行目でそれぞれ、「ブラックリスト入りしているユーザーではないか?」「取り置き上限数に達していないか?」をチェックし、引っ掛かったら発火させて54行目で拾い上げてます。
newでinsertする処理は必ずmodel経由で行うこと。
でないと、折角カウンターキャッシュを効かせて、商品テーブルの中に作っている「現在の予約数」を入れているカラムが更新されないから。

さて、失敗にしろ成功にしろ、処理が終わったらredirect_toで取り置き依頼一覧画面に飛びます。
この時に「処理が成功しましたよ」なのか「処理が失敗しましたよ」なのか、画面にメッセージを表示させてあげたい。
最初の表示だけで消えて欲しい値は「フラッシュ」と呼ばれる機能を使って渡してあげます。
redirect_toのURLの次の引数がそれにあたります。
こうやっておくと、次のページで「<%= notice %>」で表示されます。

さて、次は自分が取り置き依頼したアイテム一覧画面。

f:id:niki12260714:20171001183837j:plain

画面表示するのは「商品テーブル」「取り置きテーブル」両方になります。
加えて、「Cookieに書き込んだメールアドレス」が「取り置きテーブルのメールアドレス」と一致し、かつ「商品テーブルの頒布日が今日以降」という条件が付きます。

これまでは、Selectするカラムが一つのモデルで済んでいたのですが、ここでは二つのモデルに重なることになります。
一つ一つ、課題を紐解いていきます。

【商品テーブルと取り置きテーブルは1:nの関係である】
→has_manyで宣言しておく
→Select文を発行するとき、自動的にidを結び付けてくれる

Cookieに書き込んだメールアドレスが、取り置きテーブルのメールアドレスと一致する】
【商品テーブルの頒布日が今日以降】
→両方ともScopeで定義しておく

ここまで出来たので、あとは二つのテーブルを結び付けて実行することにします。
両方のテーブルのScopeを利用するためには、mergeを利用します。

Item.request_sale_date.merge(ReserveList.reserve_by_mail(cookies[:reserve_mail])).select(*)

 これで両方のテーブルのカラムを取ってこられるのですが、実はこれをやると、同名のカラムがどちらかに上書きされてしまうという……
そして今回はたまたま大丈夫でしたが、同名のカラムをScopeで使っていると「列名が曖昧です!」って怒られてSelectが失敗してしまう。
こちらを一つずつ修正。

Scopeで曖昧さを回避するためには、「参照するカラムは自分のテーブルですよ」っていうのを宣言する必要があります。
これが「arel_table」。
書き方としては、

# 取り置き時に登録したメールアドレスと一致するスコープ
scope :reserve_by_mail, -> (address){
where(arel_table[:mail].eq address)
}

「.eq」は等しいということ。「.gt」ならより大きいを示します。

Scopeで定義できるのはSelect句もなので、mergeとselectもスコープで定義してしまいます。
この時に、mergeする側のテーブルのカラムを別名に変えていくわけですが、これはネットで公開されていたコードを拝借させていただきました。

# Selectするカラムで同名のものは別名に変更
scope :joins_get_all_columns, ->(*tables) {
select_stmt = [%{"#{self.table_name}".*}] # joinするテーブルは通常通りのカラム名
select_stmt << tables.map{|t|
ar = ActiveRecord::Base.const_get(t.to_s.classify.singularize)
table_name = ar.table_name
table_name_single = table_name.singularize
ar.attribute_names.map{|col|
%{"#{table_name}".#{col} as #{table_name_single}_#{col}} # "table名_column名"
}
}
joins(*tables).select(select_stmt.flatten.join(","))
}

 これを使って、さっきのを書き直し。

Item.joins_get_all_columns(:reserve_lists).request_sale_date.merge(ReserveList.reserve_by_mail(cookies[:reserve_mail]))

商品テーブルのカラムはそのまま、取り置き予約テーブルのカラムを取るときは、例えば「reserve_num」は「reserve_list_reserve_num」で取得可能になります。

さて次は、この画面にQRコードを表示します。
このQRコードに書かれたURLにアクセスすると「引き渡した」ことになり、引き渡したかチェックが可能になります。
というところで、力尽きたので、また後日。