TECH EXPERT 7日目

TECH::EXPERT

基本機能の実装と応用機能の実装がそれぞれ4時間くらいなんだけど、4時間で終わるわけない。
この時間で終わる人は理解してないが、もともと知ってた人だ。
基本機能の実装で丸1日かかったよ。それでも理解には程遠い。
進捗のアドバンテージもなくなった。
(中間テスト後に丸1週間復習期間があるから問題ないけど)

面倒な質問だと思うが、メンターさんに聞くと真摯に答えてくれる。
ありがたい。けどRailsの根本的な問題だったりして、結局解決には至らなかったり。

やったことメモ

あいまい検索

文字列意味
%任意の文字列(空白文字列含む)
_任意の1文字
実行例詳細
where(‘title LIKE(?)’, “a%”)aから始まるタイトル
where(‘title LIKE(?)’, “%b”)bで終わるタイトル
where(‘title LIKE(?)’, “%c%”)cが含まれるタイトル
where(‘title LIKE(?)’, “d_”)dで始まる2文字のタイトル
where(‘title LIKE(?)’, “_e”)eで終わる2文字のタイトル

routes.rbの書き方例

Rails.application.routes.draw do
  devise_for :users
  root  'tweets#index'
  resources :tweets do
    resources :comments, only:[:create]
  end
  resources :users, only:[:show]

  # get   'tweets'     => 'tweets#index'  
  # get   'tweets/new' => 'tweets#new'
  # post  'tweets'     => 'tweets#create'
  # delete 'tweets/:id' => 'tweets#destroy'
  # get    'tweets/:id' =>  'tweets#show'
  # get   'tweets/:id/edit' => 'tweets#edit'
  # patch 'tweets/:id'  =>  'tweets#update'
  # get   'users/:id'  => 'users#show'
end

Rubyのバージョン管理とGemのバージョン管理

rubyはrubyで、gemはgemで独立して管理されている。

間違い
-ruby 1 - gem1,gem2,gem3
-ruby 2 - gem1,gem2,gem3

正解
-Ruby- ruby1, ruby2
-gem - gem1, gem2, gem3

同一gemの別バージョンも同じように管理されているため、porjectが増えるとバージョンが混在して依存関係で問題がでる。このような場合にはbundle execをつけて実行することでgemfile内の情報を辿って指定のバージョンのgemを利用できる。
ざっくり言えばエラーが出ない限りはbundle execを付ける意味はない。エラーが出たらbundle execをつけましょう。

rails g modelやrails g controllerをする際にはルーティングファイルも参照されるのでエラーに注意

resoucesメソッド

resources:reviews, only:[:new, :create]
上は下と同義
get 'products/:product_id/reviews/new' => 'reviews#new'
post 'products/:product_id/reviews' => 'reviews#create'

resourcesメソッドのネスト

resources :books do
  resources :reviews
end
          Prefix Verb   URI Pattern                                Controller#Action
    book_reviews GET    /books/:book_id/reviews(.:format)          reviews#index
                 POST   /books/:book_id/reviews(.:format)          reviews#create
 new_book_review GET    /books/:book_id/reviews/new(.:format)      reviews#new
edit_book_review GET    /books/:book_id/reviews/:id/edit(.:format) reviews#edit
     book_review GET    /books/:book_id/reviews/:id(.:format)      reviews#show
                 PATCH  /books/:book_id/reviews/:id(.:format)      reviews#update
                 PUT    /books/:book_id/reviews/:id(.:format)      reviews#update
                 DELETE /books/:book_id/reviews/:id(.:format)      reviews#destroy
           books GET    /books(.:format)                           books#index
                 POST   /books(.:format)                           books#create
        new_book GET    /books/new(.:format)                       books#new
       edit_book GET    /books/:id/edit(.:format)                  books#edit
            book GET    /books/:id(.:format)                       books#show
                 PATCH  /books/:id(.:format)                       books#update
                 PUT    /books/:id(.:format)                       books#update
                 DELETE /books/:id(.:format)  

form_for

ヘルパーメソッド。特定のモデルを編集・追加するためのフォームを生成する。
特定のテーブルにレコードだけを新規作成・更新するときに使う。

<%= form_for(モデルクラスのインスタンス) do |f| %>
…
<% end %>

form_forは引数のインスタンスが何も情報を持っていなければ自動的にcreateアクションへ、
すでに情報を持っている場合はupdateアクションへ自動的に振り分ける。

<%= f.text_field :name %>
これは下のように変換される

<input id="モデル名_name" name="モデル名[name]" type="text" size="モデルで設定したsize">
f.labellabelのlabelタグを表示 
f.text_fieldtextのinputタグを表示
f.date_selectモデルで設定したフィールドをselectタグで選べるようにして表示
f.check_boxcheckboxのinputタグを表示
f.number_fieldnumberのinputタグを表示
f.submitsubmitのinputタグを表示

form_forとform_tag

form_for と form_tag どちらを使うべきかは、基本的にモデルの有無で判断します。入力フォームで入力するデータのモデルがあれば form_for を使い、入力するデータが特にモデルを持っていなければ form_tag を使います。

resourceをネストさせた時の処理

routes.rb

resources :users do
  resources :products do
    resources :reviews
  end
end
reviews_controller.rb

def new
  @user = User.find(4)
  @product = Product.find(2)
  @review = Review.new
end
new.html.erb
<%= form_for ( [@user, @product, @review ] ) do |f| %>
  (中略)
<% end %>
送られるリクエスト
# HTTPメソッド
POST
# パス
/users/4/product/2/reviews

requireメソッド

form_forに入力されたデータはparamsの中に以下のような形でコントローラに送られる。

<%= form_for(@book) do |f| %>
  <%= f.text_field :name %>
  <%= f.text_area :detail %>
<% end %>
params
# { book: { name: "入力された名前", detail: "入力された詳細" } }

キーはform_forの引数にあるインスタンスの モデル名(book) になり、バリューはカラム名と入力された値のハッシュです。

わざわざrequireメソッドを使っているが、ハッシュなので普通に取り出しても動く。
params.require(:review)
params[:review]
なのでparamsに対して.require().permit()とする理由は可読性だけ?

form_tagとform_forで返されるparamsの違い

form_tag ただのハッシュ

{
  "utf8"=>"✓",
  "authenticity_token"=>"/3QchQigdEcpc2VaOn+6IGIV14x1iJ5bDsM7GI5NA6k=",
  "image"=>"http://sample/hoge.jpg",
  "text"=>"hello!",
  "controller"=>"tweets",
  "action"=>"create"
}

form_for ハッシュのネスト

{
  "utf8"=>"✓",
  "authenticity_token"=>"/3QchQigdEcpc2VaOn+6IGIV14x1iJ5bDsM7GI5NA6k=",
  "review"=> {
    "rate"=>"5", 
    "review"=>"It's reasonable"
  },
  "action"=>"create",
  "controller"=>"reviews",
  "product_id"=>"4"
}

redirect_to

redirect_to controller: :コントローラー名, action: :アクション名

コントローラーとビューの変数の受け渡しと、form_forの返り値

コントローラで@変数を定義するとビューへ渡せる。
これをform_for [@aaa, @bbb]としてparamsに値を返した際、
paramsでは以下のようにハッシュのネストになる。ネストされたハッシュのキーは@bbbではなく、bbbとなる。

{
  "utf8"=>"✓",
  "authenticity_token"=>"/3QchQigdEcpc2VaOn+6IGIV14x1iJ5bDsM7GI5NA6k=",
  "bbb"=> {
    "rate"=>"5", 
    "review"=>"It's reasonable"
  },
  "action"=>"create",
  "controller"=>"reviews",
  "product_id"=>"4"
}

whereメソッド

Product.where('title LIKE(?)', "test%")
Review.where(name: "test")

Renderメソッド

<% @product.reviews.each do |review|%>
  <%= render partial: "review", locals: { review: review } %>
<% end %>

render partial: “_なしのファイル名”, locals: { 部分テンプレート内で使う変数名: 引き渡したい変数}

ActiveRecord Relationクラス

whereメソッドやアソシエーションを利用してDBから複数のレコードをインスタンスとして取得した場合、取得した配列はActiveRecord Relationクラスに属す。
ActiveRecord Relationクラスには、whereを始めとして複数のメソッドが準備されている。

products = Product.all
products.class #=> ActiveRecord::Relation::ActiveRecord_Relation_Product

whereメソッドやアソシエーションで、DBに当てはまるレコードが存在しない場合、返り値は空の配列になる。

modelからインスタンスを取得すると配列になる。配列内の個々のモデルにはハッシュ?

クラスや変数の型の確認

is_a?(Hash)
is_a?(Array)
kind_of?(Hash)
kind_of?(Array)
kind_of?(Object)
kind_of?(Enumerable)

is_a?(Hash)が機能していない場合、respond_to?(:key)でtrueならハッシュのはず。

whereやfind等で取得したインスタンスは配列で格納される。
では個々のインスタンスの中身はハッシュなのか?配列なのか?

product = Products.find(1)
product.reviews
=> [#<Review:0x00007fac8d3e4c70
  id: 1,
  created_at: Fri, 19 Jul 2019 05:02:46 UTC +00:00,
  updated_at: Fri, 19 Jul 2019 05:02:46 UTC +00:00>,
 #<Review:0x00007fac900197f0
  id: 2,
  created_at: Fri, 19 Jul 2019 05:03:15 UTC +00:00,
  updated_at: Fri, 19 Jul 2019 05:03:15 UTC +00:00>]

配列で値が返る。[]で括られているので明示的に配列だと思う。
product.reviews[1]
=> #<Review:0x00007fac900197f0
 id: 2,
 nickname: "あああ",
 rate: 9,
 review: "skakakいいいううう\r\nえええ\r\nおおお",
 product_id: 10,
 created_at: Fri, 19 Jul 2019 05:03:15 UTC +00:00,
 updated_at: Fri, 19 Jul 2019 05:03:15 UTC +00:00>

配列?ハッシュ?
<>の括りはハッシュの意味だろうか?
[33] pry(main)> product.reviews[1].is_a?(Hash)
=> false
[34] pry(main)> product.reviews[1].is_a?(Array)
=> false
[35] pry(main)> product.reviews[1].respond_to?(:rate)
=> true
[36] pry(main)> product.reviews[1][1]
=> nil
[37] pry(main)> product.reviews[1][:rate]
=> 9

ハッシュかどうか尋ねてもfalseが返る。
しかしシンボルでキーを指定すると値を返すのでハッシュでいいと思う。
参考にした質問にrespond_to(:key)を使ったらとあるので試すとTrue。
正確かは保証しないが、ハッシュという認識でいいと思う。

Why is_a? returns false for Hash class? - Stack Overflow

averageメソッド

取得したモデルインスタンスの平均を出力できる。
以下の書き方をするが、カラム名をシンボルで指定する。
例)reviews.average(:rate)

モデル.average(カラム名 [, オプション])

配列に対して1つずつ中身(ハッシュ)を取り出して、シンボル指定で値を抜いている印象。
コードの実態が何をしているのかはわからないが、eachを当てているように見える。

limit / orderメソッド

limitメソッドもorderメソッドと同じように、allメソッドを省略できる。
@ranking = Product.limit(5)

layout ‘レイアウトファイル名’

コントローラ内でlayout ‘レイアウトファイル名’と書くと、そのコントローラでのアクションが呼ばれたあとに表示するビューのレイアウトファイルを指定できる。
仮にlayout ‘movie’に指定すると、MovieControllerのindexアクションが呼ばれたときに表示されるレイアウトはmovie.html.erbとなる。なにも指定しないとレイアウトファイルはapplication.html.erbとなる。

each.with_index

eachメソッドと with_indexメソッドを併用すると、要素の数だけブロックを繰り返し実行し、繰り返しごとに | で囲われている部分の i に番号が入る。デフォルトでは、iには0から入ります。

<% @ranking.each.with_index(1) do |product, i| %>
<% end %>

groupメソッド

テーブルのレコードを指定したカラムでまとめる。
.group(:key)のように使う。

Review.group(:product_id)
  Review Load (10.5ms)  SELECT `reviews`.* FROM `reviews` GROUP BY product_id
=> [#<Review id: 1, nickname: "まいき", rate: 1, review: "おもしろい", product_id: 21, created_at: "2014-11-05 16:03:39", updated_at: "2014-11-05 16:03:39", user_id: 4>,
  #<Review id: 3, nickname: "えいちゃん", rate: 10, review: "感動した!また見たい", product_id: 22, created_at: "2014-11-05 16:03:39", updated_at: "2014-11-05 16:03:39", user_id: 4>,
  #<Review id: 13, nickname: "ごとう", rate: 10, review: "思っていたより良かった", product_id: 23, created_at: "2014-11-05 16:03:39", updated_at: "2014-11-05 16:03:39", user_id: 4>

返る値はテーブル情報のハッシュ(?)が入った配列?

countメソッド

array.count(hogehoge)

count (Array)
配列の中で引数hogehogeと同じ要素の数を返す。
引数を省略すると、要素の数を返す。

order(‘count_カラム名’).count(カラム名)

countメソッドの引数にカラム名を指定できる。
こうするとorderメソッドでcount_カラム名でのソートが可能になり、そのカラムを持つレコードの数でソートする。

例として、reviewをproduct_idでまとめたレコードをレコード数でソートしてカウントする。
返り値として、カラム名とレコード数のハッシュで返す、という処理がされる。

limit()を併用したい場合にはcount()の前につける。
limitメソッドは複数のレコードの配列のような形であるActiveRecord::Relationに対するメソッド。
count(:product_id)の後に行うと返り値がハッシュになるため使えない。

自己矛盾?.countを実行する前にcount_カラム名が使えるのはおかしい気がする。
SQL上では先にcount関数が実行されてcount_カラム名が作られて一度中断、一連の処理が終わった後にcountの処理が再開されてハッシュを返している?
SQLのcountには集計する以上の機能がないのでハッシュにして返しているのはRails独自の機能かもしれない。なので先にcountが起動できて、別処理であるハッシュにする作業を後に行えるの感も?

.group.countと併用した場合

Review.group(:product_id).count
=> {21=>2, 22=>1, 23=>4}

arrayに対して使う.countメソッドを用いた場合ハッシュが返ることから、.groupで作られた新たな配列は、ハッシュを要素に持つ配列。そしてハッシュのキーはgroup()で指定したキー、valueはハッシュの形でテーブル情報を持つ配列

.group(:id)
[
  {id=1:  [ classinstance1{}, classinstance2{} ] },
  {id=2:  [ classinstance1{}, classinstance2{} ] },
]

.groupした際には内部的には配列がネストされている???
仮にそうだとしても上記に.countを当ててもハッシュの配列にしかならず、返り値がハッシュになる道理がない。

こちらを参考にさせていただいた。
どうも集計関数は特殊な存在で、使うとハッシュで値を返すようだ。そのためorderメソッドよりも前に.countを書くとエラーになる感じ。その割には.countが先に動作しないと並べ替えできないはずなので不思議。先に展開するけど、最終的にreturnするのは最後という指定なんだろうか?
ActiveRecordパターンというキーワードもよく調べる必要がありそう。
思考停止して書いちゃえば動くし見た目も分かりやすいんだけど、実態がよく読めない。

keysメソッド

ハッシュはkeysというメソッドを持っています。これはハッシュのキーだけを取り出して配列として返すメソッドです。

mapメソッド

mapメソッドは配列オブジェクトのインスタンスメソッドです。mapメソッドは配列の中身を1つずつ取り出してブロックという構文を繰り返し実行します。そして、ブロックの返り値を集めた新しい配列を作成します。

配列オブジェクト.map {|ele| ブロックの処理}
# eleには配列の要素が1つずつ代入される
# ブロックの処理は配列の要素の数だけ繰り返し実行される
ランキング機能の実例
def ranking
    product_ids = Review.group(:product_id).order('count_product_id DESC').limit(5).count(:product_id).keys  
    @ranking = product_ids.map{|id| Product.find(id) }
end

to_sql

sql文を返す。
使ったことがないのでなんとも。
order.countやorder.limitなどで謎な挙動をした際に調べるのに使えるかも。
今の私にはSQL文を読むのは荷が重い。

Deviseのインストール

Gemfile

 gem 'devise'
bundle install
rails g devise:install

ビューファイルの作成

rails g devise:views

ユーザーモデルの作成(migrationファイルも含まれる)

rails g devise user
$ rails g devise user
invoke  active_record
create    db/migrate/20190719114302_devise_create_users.rb
create    app/models/user.rb
invoke    test_unit
create      test/models/user_test.rb
create      test/fixtures/users.yml
insert    app/models/user.rb
 route  devise_for :users

テーブルの作成

bundle exec rake db:migrate

サーバーの再起動

$ rails s

after_sign_out_path_forメソッド

deviseでサインアウトしたあとのリダイレクト先を指定するメソッドとしてafter_sign_out_path_forがあります。このメソッドでは返り値にサインアウト後のリダイレクト先URLを指定します。deiviseのメソッドを上書きしている関係上resourceを引数に渡さなけらばならないので、resourceを引数に渡します。

def after_sign_out_path_for(resource)
  '/users/sign_in' # サインアウト後のリダイレクト先URL
end

コメント