『Ruby on Rails 5 アプリケーションプログラミング』を学習する18

Ruby on Rails 5アプリケーションプログラミング

Ruby on Rails 5アプリケーションプログラミング

序文

Ruby on Rails 5 アプリケーションプログラミング」学習18日目。
ポモドーロテクニックとともに。

GitHub

github.com

進捗

  • 第5章 モデル開発
    • 5.4 レコードの登録/更新/削除
      (学習時間:3.5時間)

感想

レコードの登録、更新、削除などを行うクエリメソッドについて学習。
ポモドーロテクニック発動。
調子いいときには面倒くさく感じるけど、あんまりやる気ないときには最低限の成果を担保してくれるような気がしなくもない。
そもそも勉強する気力すらわかない時はどうしようもないけれど。

カロリーメイトください。

BGM

ダイエッター / 小林未奈 www.youtube.com

コード実装部分

↓config/routes.rb

# 省略

  get  'record/update_all'
  get  'record/update_all2'

  get  'record/destroy2/:id', to: 'record#destroy2'
  get  'record/delete/:id', to: 'record#delete'

  get  'record/destroy_all'
  get  'record/delete_all'

  get  'record/transact'

  get  'record/enum_rec'

↓/app/controllers/record_controller.rb

class RecordController < ApplicationController

  # 省略

  # レコードの更新例
  def update_all
    cnt = Cd.where(label: 'omake records')
            .update_all(label: 'サザナミレーベル')
    render plain: "#{cnt}件のデータを更新しました。"    
    # UPDATE "cds" 
    # SET "label" = 'omake records' 
    # WHERE "cds"."label" = ?  
    # [["label", "サザナミレーベル"]]
  end

  # レコードの更新例2
  def update_all2
    cnt = Cd.order(:label)
            .limit(5)
            .update_all('price = price * 0.8')
    render plain: "#{cnt}件のデータを更新しました。"    
    # UPDATE "cds" 
    # SET price = price * 0.8 
    # WHERE "cds"."id" 
    # IN (
    #   SELECT "cds"."id" 
    #   FROM "cds" 
    #   ORDER BY "cds"."label" ASC 
    #   LIMIT ?
    # )
    # [["LIMIT", 5]]
  end

  def destroy2
    # 削除件数は帰らなかった
    Cd.destroy(params[:id])
    # SELECT  "cds".* 
    # FROM "cds" 
    # WHERE "cds"."id" = ? 
    # LIMIT ?  
    # [["id", 1], 
    # ["LIMIT", 1]]

    # DELETE FROM "artists_cds" 
    # WHERE "artists_cds"."cd_id" = ?  
    # [["cd_id", 1]]

    # DELETE FROM "cds" 
    # WHERE "cds"."id" = ?  [["id", 1]]
  end

  def delete
    # 関連モデルは削除せずに単純にdeleteのみ行う
    # 削除件数も返るっぽい
    cnt = Cd.delete(params[:id])
    render plain: "#{cnt}件のデータを削除しました。"    
    # DELETE FROM "cds" 
    # WHERE "cds"."id" = ?  
    # [["id", 1]]
  end

  def destroy_all
    # 返り値は削除したモデルの配列
    Cd.where.not(label: 'サザナミレーベル').destroy_all
    render plain: "データをすべて削除しました。"  
    # SELECT "cds".* 
    # FROM "cds" 
    # WHERE ("cds"."label" != ?)  
    # [["label", "サザナミレーベル"]]

    # DELETE FROM "artists_cds" 
    # WHERE "artists_cds"."cd_id" = ?
    # [["cd_id", 2]]
  
    # DELETE FROM "cds" 
    # WHERE "cds"."id" = ?  
    # [["id", 2]]  
  end

  def delete_all
    # 返り値は削除した件数
    cnt = Cd.where.not(label: 'サザナミレーベル').delete_all
    render plain: "#{cnt}件のデータを削除しました。"  
    # DELETE FROM "cds" 
    # WHERE ("cds"."label" != ?)  
    # [["label", "サザナミレーベル"]]
  end

  # トランザクション処理の例
  def transact
    # モデル、もしくはインスタンス経由でtransactionを呼び出す
    Cd.transaction do
      c1 = Cd.new(
        jan: '978-4-7741-5067-3',
        title: 'ダイエッター',
        price: 2500,
        label: 'FLAVOR RECORDS',
        released: '2018-03-21'
      )
      # save()はtrue/falseを返し、
      # save!()は失敗した場合に例外を返す
      # 例外が発生するとrescueブロックが実行される
      c1.save!
      # throwだ!
      # raise '例外発生:処理はキャンセルされました'
      c2 = Cd.new(
        jan: '978-4-7741-5067-5',
        title: 'dinosaur',
        price: 2500,
        label: 'nom records',
        released: '2018-02-13'
      )
      c2.save!
    end
    # begin transaction
    # INSERT INTO "cds" ("jan", "title", "price", "label", "released", "created_at", "updated_at") 
    # VALUES (?, ?, ?, ?, ?, ?, ?) 
    # [["jan", "978-4-7741-5067-3"], 
    #  ["title", "ダイエッター"], 
    # ["price", 2500], 
    # ["label", "FLAVOR RECORDS"], 
    # ["released", "2018-03-21"], 
    # ["created_at", "2018-04-10 04:53:19.064469"], 
    # ["updated_at", "2018-04-10 04:53:19.064469"]]
    # commit transaction
    render plain: 'トランザクションは成功しました'
  # catchだ!
  rescue => e
    render plain: e.message
    # begin transaction
    # INSERT INTO "cds" ("jan", "title", "price", "label", "released", "created_at", "updated_at") 
    # VALUES (?, ?, ?, ?, ?, ?, ?)  
    # [["jan", "978-4-7741-5067-3"], 
    #  ["title", "ダイエッター"], 
    #  ["price", 2500], 
    #  ["label", "FLAVOR RECORDS"], 
    #  ["released", "2018-03-21"], 
    #  ["created_at", "2018-04-10 04:55:11.265965"], 
    #  ["updated_at", "2018-04-10 04:55:11.265965"]]
    # rollback transaction

  # 補足 : トランザクション分離レベルの指定例
  # 必要になったら調べよう。とりあえずSQLiteは対応していない
  # だいたいREAD COMMITTEDかREPEATABLE READにしとけばいいんじゃね?
  # Cd.transaction(isolation: :repeatable_read) do
  #   @cd = Cd.find(1)
  #   @cd.update(price: 3000)
  # end
  end

  # 列挙型の利用例
  def enum_rec
    @review = Review.find(1)
    # 列挙体statusがpublishedに設定されているレコードのみを取得
    # @review = Review.published.where(.....)
    # SELECT  "reviews".* 
    # FROM "reviews" 
    # WHERE "reviews"."status" = ? 
    # ORDER BY "reviews"."updated_at" DESC 
    # LIMIT ?  
    # [["status", 1], 
    #  ["LIMIT", 11]]

    # 列挙型の設定(必要ない場合はSQLは発行されない)
    @review.published!
    # 他の書き方の例
    # @review.status = 1
    # @review.status = :published

    # UPDATE "reviews" 
    # SET 
    #   "status" = ?, 
    #   "updated_at" = ? 
    # WHERE "reviews"."id" = ?  
    # [["status", 1], 
    #  ["updated_at", "2018-04-10 06:55:00.583813"], 
    #  ["id", 1]]

    render plain: 'ステータス:' + @review.status
    # 列挙型自体をモデルとして作成すると
    # 複数のモデルから参照できるかもしれない

    # render plain: @review.inspect
  end
end

↓/db/migrate/20180410052604_create_members.rb

class CreateMembers < ActiveRecord::Migration[5.1]
  def change
    create_table :members do |t|
      t.string :name
      t.string :email
      # 同時実行制御のためのフィールド
      t.integer :lock_version, default: 0

      t.timestamps
    end
  end
end

↓app/views/members/_form.html.erb

<%# 省略 %>

  <%# 隠しフィールドとしてlock_versionを仕込んで %>
  <%# オプティミスティック同時実行制御を行う %>
  <%= form.hidden_field :lock_version %>

<%# 省略 %>

↓/app/controllers/members_controller.rb

  # 省略

  # PATCH/PUT /members/1
  # PATCH/PUT /members/1.json
  def update
    respond_to do |format|
      if @member.update(member_params)
        format.html { redirect_to @member, notice: 'Member was successfully updated.' }
        format.json { render :show, status: :ok, location: @member }
      else
        format.html { render :edit }
        format.json { render json: @member.errors, status: :unprocessable_entity }
      end
    end
  rescue ActiveRecord::StaleObjectError
    render plain: '競合エラーが発生しました。'
    # 競合エラー発生時のSQL
    # begin transaction
    # UPDATE "members" 
    # SET 
    #   "name" = '小林未奈', 
    #   "updated_at" = '2018-04-10 06:05:25.497660', 
    #   "lock_version" = 2 
    # WHERE 
    #   "members"."id" = ? 
    #   AND "members"."lock_version" = ?  
    # [["id", 1], 
    # ["lock_version", 1]]
    # commit transaction

    # begin transaction
    # UPDATE "members" 
    # SET 
    #   "name" = 'こばやし未奈', 
    #   "lock_version" = 2, 
    #   "updated_at" = '2018-04-10 06:05:35.310184' 
    # WHERE 
    #   "members"."id" = ? 
    #   AND "members"."lock_version" = ?  
    # [["id", 1], 
    #  ["lock_version", 1]]
    # rollback transaction
  end

  # 省略

↓db/migrate/20180329053536_create_reviews.rb

class CreateReviews < ActiveRecord::Migration[5.1]
  def change
    create_table :reviews do |t|
      t.references :cd, foreign_key: true
      t.references :listener, foreign_key: true
      t.integer :status, default: 0, null: false
      t.text :body

      t.timestamps
    end
  end
end

↓/app/models/review.rb

class Review < ApplicationRecord
  # モデル依存の列挙型はモデルファイル内に定義する
  # これにより@review.statusみたいな感じで列挙体名が呼び出せる
  enum status: { draft:0, published:1, deleted:2 }
  # これも同じ
  # enum status: {:draft, :published, :deleted}

  belongs_to :cd
  belongs_to :listener

  default_scope{ order(updated_at: :desc) }
end

↓/test/fixtures/members.yml

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

one:
  id: 1
  name: 小林未奈
  email: kobayashi@calorie.mate
  lock_version: 0

two:
  id: 2
  name: せりかな
  email: serikana@calorie.mate
  lock_version: 0


three:
  id: 3
  name: 泉川そら
  email: sora@calorie.mate
  lock_version: 0

実行結果

f:id:yjkym:20180410164330p:plain