学生向けプログラミング入門

学生向けにプログラミングを解説。Java、C++、Ruby、PHP、データベース

Ruby on RailsでWebアプリケーション開発その19 買い物カートのテーブル作成

<<前  [TOP]  次>>


商品をセッションカートという一時的な保存場所に格納出来るようなシステムを作っていきます。
Railsでは、アクセスしている人が実行するすべての操作を追跡できるようにするために、sessionと呼ばれるものを使用しています。
sessionはコントローラ内にハッシュに似たような形で保持しています。
この機能を使って、商品を格納するためのカートを作成してみます。


rails generate scaffold cart」とコマンドプロンプトで入力します。
f:id:MrRadiology:20180219110603p:plain


途中「CSSを上書きしますか?」と聞かれるので、今回は「n」と入力して上書きしないようにします。
f:id:MrRadiology:20180219124322j:plain


rake db:migrate」コマンドでcartsテーブルをデータベースに作成します。
f:id:MrRadiology:20180219112013p:plain


テーブルの内容を見てみましょう。
f:id:MrRadiology:20180221090214p:plain


「id」「created_at」「updated_at」の3つのフィールドがあります。
Rails4.2で自動生成される「id」フィールドは、「int」型になっています。


「app/controllers」フォルダにある「application_controller.rb」ファイルを編集します。


【app/controllers/application_controller.rb】

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

	private
	def current_cart

		Cart.find(session[:cart_id])

		rescue ActiveRecord::RecordNotFound
			cart = Cart.create
			session[:cart_id] = cart.id
			cart
	end
end

current_cart()は「private」にします。これはこのメソッドをコントローラのアクションとして公開しないという指定をしています。
最初にsessionオブジェクトから「:cart_id」というキーを取得してfind()メソッドで探します。
このfind()メソッドはRails4.2までしか使用できません。
この「:cart_id」はcartsテーブルのidフィールドを指しています。
カートの商品が見つからない時は新しいカートを作成し、idをセッションに格納してそのカートを返します。


次に「rails generate scaffold line_item good_id:integer cart_id:integer」とコマンドプロンプトで入力します。
「CSSを上書きしますか?」と聞かれるので、今回も「n」と入力して上書きしないようにします。
f:id:MrRadiology:20180221090648p:plain


rake db:migrate」コマンドでline_itemsテーブルをデータベースに作成します。
f:id:MrRadiology:20180219130742p:plain


line_itemsテーブルとgoodsテーブルを外部キーの参照によってリンクさせています
line_itemsテーブルの「good_id」フィールドとgoodsテーブルの「id」フィールドがリンクしています。
goodsテーブルの「id」フィールドは自動で出来たもので「int型」になっていたので、line_itemsテーブルの「good_id」フィールドの型も「int型」にしました。
リンクさせる場合、型を合わせないとエラーが起きます。


「line_items」テーブルを確認してみます。
f:id:MrRadiology:20180221091046p:plain


では「app/models」フォルダに移動して「cart.rb」ファイルを編集します。


【app/models/cart.rb】

class Cart < ActiveRecord::Base

	has_many :line_items, dependent: :destroy

end



has_many()の呼び出しを追加しました。
「has_many :line_items」の部分は、1つのカートに多数の商品が関連付けされるということを示しています。
「dependent: :destroy」の部分は、商品が存在するかどうかはカートが存在するかどうかに依存するという意味になります。
カートを空にしてデータベースから削除した時は、そのカートに関連付けられている商品もすべて削除されるようにするということです。


続いてline_itemsからcarts、goodsテーブルへのリンクを指定します。
「app/models」フォルダにある「line_item.rb」ファイルを以下のように編集します。


【app/models/line_item.rb】

class LineItem < ActiveRecord::Base

	belongs_to :good
	belongs_to :cart

end



「belongs_to」は line_itemsテーブルはcartsテーブルとgoodsテーブルが親子関係にあるということを示しています。
対応するカートと商品がなければline_itemは存在できません。


さらにgoodモデルにもhas_many()の呼び出しを追加しておきます。
「app/models」フォルダにある「line_item.rb」ファイルを以下のように編集します。


【app/models/good.rb】

class Good < ActiveRecord::Base

	has_many :line_items
	before_destroy :referenced_by_line_item

	validate :price_validate

	#Railsで標準で用意されている検証メソッド
	#指定されたフィールドが存在し、その内容が空でないことを確認。
	validates_presence_of :title, :image_url, :maker, :category,
	:message => "が空の状態で保存することは出来ません。"

	#priceフィールドに数字か入力されているか検証。
	validates_numericality_of :price,
	:message => "が有効な数値ではありません。"

	#titleフィールドに保存しようとする名称が存在していないかどうかを確認。
	validates_uniqueness_of :title,
	:message => "はすでに存在しています。"

	#フィールドの値が正規表現に一致するかどうかをチェック。
	#.gif,.jpg,.pngのどれかで終わっていることを確認。
	validates_format_of :image_url,
	:with => /\a|\.jpg$|\.png$|\.gif$\z/,
	:message => "はGIF,JPG,PNG画像でなければなりません。"

	#商品の価格が正の数であることを確認する。
	#価格フィールドが空でないときだけチェックをする。
	protected
	def price_validate
		errors.add(:price, "は0より大きくなければなりません。") unless price.nil? || price > 0.0
	end

	def self.select_shop
		where("date <= ?","now()").order(title: "ASC")
	end

	private
	def referenced_by_line_item

		if line_items.empty?
			return true
		else
			errors.add(:base, '品目が存在します。')
			retuen false
		end
	end

end



追加したのは以下の記述です。

has_many :line_items
before_destroy :referenced_by_line_item

private
def referenced_by_line_item

	if line_items.empty?
		return true
	else
		errors.add(:base, '品目が存在します。')
		retuen false
	end
end

追加した「referenced_by_line_item()」メソッドは商品を参照している品目(line_item)がないことを確認しています。
商品が多数の品目(line_item)を持つことをhas_many()で宣言してから「referenced_by_line_item」メソッドを定義しています。
「before_destroy」で宣言されるものをフックメソッドといい、Railsに自動で呼び出されるメソッドになります。
今回の場合はRailsがデータベースの行を削除しようとした時に呼び出されます。
このフックメソッドで「false」を返すと行は削除されません。
「errors.add(:base, '品目が存在します。')」はerrorsオブジェクトに直接アクセスし、ベースオブジェクト自体と関連付けているという意味になります。


次に「カートに入れる」ボタンを追加します。
「app/views」フォルダの「market」フォルダにある「index.html.erb」を以下のように編集します。


【app/views/market/index.html.erb】

<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
<br>
<h1>Railsはじめてマート</h1>
<h2>      Railsで楽しくお買い物♪</h2>
<br>
<%= link_to 'カートを表示', market_path, class: 'btn' %>
<br>
<br>
<table>
    <tr>
      <th></th>
      <th></th>
      <th></th>

    </tr>
	<% @goods.each do |good| %>
	<tr>
		<td text-align:center;>
			<img height="80" src="<%=h good.image_url %>"/>
		</td>
		<td>
			<p><font size="5">商品名:<%= good.title %></font></p>
			商品説明:<%= good.description %><br></font></p>
			分類:<%= good.category %><br>
			メーカー:<%= good.maker %><br>
			<p><font size="4">価格:<%= good.price %></font></p>

		</td>
		<td>
			<%= button_to 'カートに入れる', line_items_path(good_id: good), class: 'fbtn' %>
		</td>
	</tr>

	<% end %>
</table>



変更したのは以下の部分です。

<%= button_to 'カートに入れる', line_items_path(good_id: good), class: 'fbtn' %>

POSTで渡したいので「button_to()」メソッドを使います。
コントローラの名前に「_path」と付け加えるとデータの受け渡しをRailsにすべて任せることが出来ます。
「good_id:」オプションを使用すればRailsがidを抽出してくれます。


「app/controllers」フォルダにある「line_items_controller.rb」を編集します。
編集するのは「create()」メソッドです。


【app/controllers/line_items_controller.rb】

class LineItemsController < ApplicationController
  before_action :set_line_item, only: [:show, :edit, :update, :destroy]

  # GET /line_items
  # GET /line_items.json
  def index
    @line_items = LineItem.all
  end

  # GET /line_items/1
  # GET /line_items/1.json
  def show
  end

  # GET /line_items/new
  def new
    @line_item = LineItem.new
  end

  # GET /line_items/1/edit
  def edit
  end

  # POST /line_items
  # POST /line_items.json
  def create

	@cart = current_cart

	good = Good.find(params[:good_id])

	@line_item = @cart.line_items.build(good: good)

    respond_to do |format|
      if @line_item.save
        format.html { redirect_to @line_item.cart, notice: 'カートに商品が追加されました。' }
        format.json { render :show, status: :created, location: @line_item }
      else
        format.html { render :new }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /line_items/1
  # PATCH/PUT /line_items/1.json
  def update
    respond_to do |format|
      if @line_item.update(line_item_params)
        format.html { redirect_to @line_item, notice: 'Line item was successfully updated.' }
        format.json { render :show, status: :ok, location: @line_item }
      else
        format.html { render :edit }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /line_items/1
  # DELETE /line_items/1.json
  def destroy
    @line_item.destroy
    respond_to do |format|
      format.html { redirect_to line_items_url, notice: 'Line item was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_line_item
      @line_item = LineItem.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def line_item_params
      params.require(:line_item).permit(:good_id, :cart_id)
    end
end



編集したのは以下の部分です。

def create

	@cart = current_cart

	good = Good.find(params[:good_id])

	@line_item = @cart.line_items.build(good: good)

    respond_to do |format|
      if @line_item.save
        format.html { redirect_to @line_item.cart, notice: 'カートに商品が追加されました。' }
        format.json { render :show, status: :created, location: @line_item }
      else
        format.html { render :new }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
end

create()メソッドに変更を加えて、商品idをフォームパラメータとして受け取れるようにしました。
Railsはidフィールドによってオブジェクトやそれに対応するデータベースの行を識別します。
create()メソッドにidを渡すことで、カートに追加する商品を指定しています。
まず、「application_controller.rb」に実装した「current_cart()」メソッドを使ってセッション内のカートを検索、作成しています。
次にparamsを使ってgood_idパラメータを取得しています。今回はビューで使用しないのでローカル変数「good」に格納しています。
その後取得したidの商品を「@cart.line_items.build」に渡しています。これで@cartオブジェクトとgoodの間に新しいline_itemのつながりが出来ます。
このline_itemを「@line_item」に格納します。
残りの部分で、エラー処理とJSONリクエストの処理を行っています。
line_itemオブジェクトからcartオブジェクトを見つけられるように「redirect_to」の記述で「@line_item.cart」として「.cart」を付け加えています。


最後に「app/views/carts」フォルダにある「show.html.erb」を以下のように編集します。


【app/views/carts/show.html.erb】

<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
<br>
<h1>Railsはじめてマート</h1>
<h2>   カートに追加された商品</h2>
<br>
<span>  </span><%= link_to '買い物を続ける', market_path, :class => 'btn' %><span>
<br>
<br>
<table>
	<tr>
		<th></th>
		<th>商品名</th>
		<th>価格</th>
	</tr>
		<% @cart.line_items.each do |item| %>
		<tr>
			<td text-align:center;><img height="80" src="<%=h item.good.image_url %>"/></td>
			<td text-align:center;><font size="4"><%= item.good.title %></font></td>
			<td text-align:center;><font size="4"><%= (item.good.price).to_i %></font></td>
		</tr>
		<% end %>
</table>



http://localhost:3000/」を開いて商品をカートに追加してみましょう。
f:id:MrRadiology:20180221121908j:plain


f:id:MrRadiology:20180222121506p:plain


<<前  [TOP]  次>>