【PostgreSQL】テーブルをコピーしたらindexや制約がコピーされないぞ?
過去データの取り込み作業でテーブルにデータを流し込む作業をしているのだけど、念のためのバックアップを作って何かあれば戻せるように準備していたのだが、index
や制約がコピーされてないことに気づき調査開始。
単純にコピー作って戻す方法(ダメな方法)
hoge=# CREATE TABLE shop_sales_back AS SELECT * FROM shop_sales; hoge=# DROP TABLE shop_sales hoge=# ALTER TABLE shop_sales_back RENAME TO shop_sales;
indexや制約を意識した方法(良い方法)
hoge=# CREATE TABLE shop_sales_back AS SELECT * FROM shop_sales; #テーブルは消さずに、データだけ空っぽにする hoge=# TRUNCATE TABLE shop_sales; hoge=# INSERT INTO shop_sales (SELECT * from shop_sales_back);
テーブルは消さずに中身だけ削除! コピーしたテーブルから再度データを流し込むって感じですね。
また、別の方法としてテーブル定義の情報含めてpg_dump
してリストアする方法がありそうなのだけど何がどう違うのか分かってないので、先輩に聞いてみよう。
pg_dumpしてリストアする方法
# ダンプ $ pg_dump —username=user --table shop_sales hoge > hoge.sql # リストア $ psql —username=user hoge < hoge.sql
indexや制約を確認する方法
$ psql hoge hoge=# \d table_name
【Ruby on Rails】ActiveRecordを使ってランダムなデータを指定した数だけ取得する方法
データの更新作業などで更新結果を確認したい場合に、対象データが多い場合は、全部のデータ見るのは大変なので何件かデータを取得してチェックするなんて場面があるかなと思います。
そんな時にランダムなデータを取得して結果の確認が出来れば便利ですよね。
> Hoge.order("RANDOM()").limit(10)
order("RANDOM()")
でランダムなデータを取得して、limit
で10件取得してます。
条件絞りたい時はwhere
を追加すれば良いですし、必要なカラムだけ表示させたい時はmap
使えば良いですし後は自由ですね。
> Hoge.where(code: 4).order("RANDOM()").limit(10).map{|v| [v.code, v.name]}
という話なのですが、これだとmysql
場合はRAND
にしないといけないなどDBに依存するので宜しくないとアドバイス頂きました!
2行になるけど、改善する方法はpluck
使ってid
だけ取ってきて、それを条件にする方法
> target_ids = Hoge.pluck(:id).sample(5) => [6, 10, 8, 7, 1] > target_ids.class => Array > Hoge.find(target_ids) => [#<Hoge id: 6,.......
こんな感じ
【Heroku】DBを1から作り直したい時にやること(rails db:reset)
ローカルだと一発で出来るdb:reset
がheroku
だとできないだと
$ rails db:reset $ rails db:seed
$ heroku run rails db:reset rails aborted! ActiveRecord::ProtectedEnvironmentError: You are attempting to run a destructive action against your 'production' database. If you are sure you want to continue, run the same command with the environment variable: DISABLE_DATABASE_ENVIRONMENT_CHECK=1
heroku
じゃなくて、rails5から本番環境では削除できないようになってるっぽいですね。
Rails5から、productionモードでは、db:dropなどのDBを破壊する系のコマンド実行を防止する機能が追加された。
なんかオプション付けろって言われたので、再実行
$ heroku rake db:reset DISABLE_DATABASE_ENVIRONMENT_CHECK=1 FATAL: permission denied for database "postgres" DETAIL: User does not have CONNECT privilege. Couldn't drop database 'hogehoge' rails aborted! PG::ConnectionBad: FATAL: permission denied for database "postgres" DETAIL: User does not have CONNECT privilege.
権限が無いだと...
色々調べてみるとDATABASE_URL
とYOUR_APP_NAME
を指定したheroku pg:reset
ってコマンドがあるみたい
$ heroku config DATABASE_URL: postgres://hogehoge $ heroku apps lit-ravine-31204 mapachannel $ heroku pg:reset postgres://hogehoge -a mapachannel ▸ Unknown database: postgres://hogehoge. Valid options are: ▸ DATABASE_URL
くそーやっぱりできねーじゃねーか。 なので、別手段。
やりたいことはデータを消したいことなので、直接DBに接続してデータ消す作戦へ
$ heroku pg:psql mapachannel::DATABASE=> drop schema public cascade; $ heroku rake db:migrate rails aborted! ActiveRecord::StatementInvalid: PG::InvalidSchemaName: ERROR: no schema has been selected to create in LINE 1: CREATE TABLE "schema_migrations" ("version" character varyin...
あーこれ完全にぶっ壊したなー
完全に詰んだ!と思っていたのだけど、もっかい調べてみてるとheroku pg:reset DATABASE_URL
としか書いてない。
これ、本当にこのまま打つんじゃないか?と思い実行。
$ heroku pg:reset DATABASE_URL ▸ WARNING: Destructive action ▸ postgresql-reticulated-99811 will lose all of its data ▸ ▸ To proceed, type mapachannel or re-run this command with --confirm mapachannel > mapachannel Resetting postgresql-reticulated-99811... done
すでにデータが無いよってエラーだと思うけど、アプリの名前mapachannel
を打ったら成功した。
で、再実行!
$ heroku run rails db:migrate $ heroku run rails db:seed
でけた。めでたしめでたし。
【Ruby】文字列をDate(日付)に変換する時は桁数を意識しないと違う日付になる
文字列が意図しない日付になる?
売上などのデータでcsvファイルを連携して取り込むってのはよくある話だと思うのですが、単純に文字列結合して変換すると違った日付になったりエラーになったりするので気をつける必要がある。
> require 'date' => true >Date.parse("2017121") => #<Date: 2017-05-01 ((2457875j,0s,0n),+0s,2299161j)> # 期待するのは「2017/12/1」だけど7桁だと「2017/05/01」になる!!! > Date.parse("201711") ArgumentError: invalid date from (irb):25:in `parse' from (irb):25 from /home/bondgate/.rbenv/versions/2.3.3/bin/irb:11:in `<main>' # 6桁だとエラーになる > Date.parse("2017011") => #<Date: 2017-01-11 ((2457765j,0s,0n),+0s,2299161j)> # 後ろ2桁を見て判断してるっぽい > Date.parse("2017041") => #<Date: 2017-02-10 ((2457795j,0s,0n),+0s,2299161j)> # 41日と判断してっぽいので「2/10」になる。
文字列を正しい日付に変換する方法
きちんと8桁になるように、new
or 0埋めでparse
すること。
> Date.parse("20171201") => #<Date: 2017-12-01 ((2458089j,0s,0n),+0s,2299161j)> > year = "2017" > month = "12" > day = sprintf("%02d",1) => "01" > Date.parse(year + month + day) => #<Date: 2017-12-01 ((2458089j,0s,0n),+0s,2299161j)> > Date.new(2017, 1,1) => #<Date: 2017-01-01 ((2457755j,0s,0n),+0s,2299161j)>
まとめ
7桁の場合はエラーにならないので、知らぬ知らぬで間違った日付で更新するなんて全然ありえるから怖いね。 日付を扱う時は、しっかりとあらゆる想定をするのが大事そうだ!
そんな感じ。
【Ruby on Rails】`rails db:reset`だと変更/追加したmigrationファイルが反映されない!
rails db:reset
テーブル削除 → schema.rb
の情報を元に作り直す
rails db:migrate:reset
テーブル削除 → 作成 → db:migrate
が実行される
つまり、migration
ファイルを作成した後にそのファイルを修正した場合はdb:reset
しても反映されない!
調べてみたら、まさにまさにの情報が。 ありがとうございます! http://easyramble.com/difference-bettween-rake-db-migrate-reset.html
なんだけど、気になって色々やってると新しくrails g migration AddColumnToHoge
みたいに新しく作った場合でもdb:reset
じゃ反映されないぞ。
って思ったのだけどschema.rb
はrails db:migrate
した時に更新されるっぽいので新しくファイルを作った場合はdb:migrate
しないとダメみたい。
すっきりした。
エンジニア=プログラマーは夢のある職業であることを痛感した1年だった
こんちにちは。opiyoです。
今日から新年初出社ですが、世の中的には今週まで休みの人も多いですかね。
僕も30歳目前。外の世界の変化が激しい中、大切な20代も残りわずか。
普通に過ごしているだけでは当たり前のように生きていくことすら厳しい、そんな時代。
もー若くないと感じた一年でもあったので振り返りと今年の目標を。
2017年は色々あったような無かったような、そんな1年でした。
非エンジニアからエンジニアを目指している人
にとっては参考になることも多いと思うので、是非ご覧下さい。
2017年振り返り
人生に詰む
働き方改革、副業容認というニュースが今年は多く見られたと思いますが、これって逆に
「会社に頼った生活は今後は出来ないよ」
って言われているのとイコールだと思います。
年末になると大企業の平均ボーナスは幾らでした。みたいなニュースが流れますが平均ボーナスは確か90万くらいだって報道されていました。 ですが、これで世の中って好景気じゃん!と思うのは間違いだと僕は思います。 そんな額を貰えるのは大企業問わず一部の人間だろうし、この参考値に入っているであろう東芝があんな事件があり職を失う人がいるってのが今の時代だと思います。
このニュース見たとき時代が違えば僕の実家もと思うと怖いです。
こんな時代だからこそ、企業側も副業を容認して万が一に備えてねってことだと思うのです。
誰もが会社に依存しない自分で稼ぐ力ってのが今後はますます必要な時代になってきていると思います。
だけどどうすりゃいいんだ。自分で稼ぐって言っても右も左も分からない。って状況な時に「りゅうけんさん」を知りました。
りゅうけんさんを知る
人生に絶望していた時に知ったのがりゅうけんさんでした。
りゅうけんさんはフリーランスのエンジニアをやっていて、サロンをやっていたりブログもやられていたり色々な方法で稼いでおられる、とんでもない方です。 エンジニアを目指している方は是非是非ご覧下さい。モヤモヤしていたものが一気に晴れますよ。マジで。
このブログに出会い、サロンに入会したことでエンジニア=プログラマーの道を進むのは間違ってないことだったのだと強く思うようになりました。
先日もこんなつぶやきをしていましたが、エンジニアは間違いの無い選択肢なのです。
エンジニアになれば向こう10年はだいぶ儲かります。 pic.twitter.com/ebssJUonVP
— やまもとりゅうけん/人生逃げ切り😇 (@ryukke) December 26, 2017
なので先ずは自分の現在地を確かめてみようと思いました。
第一転職活動
私の現在のスキルはざっくりこんな感じです。
- IT業界7年目
- 保守6年、開発1年
- Java、c#で半年開発経験あり(5年前なので忘れてる)
- Ruby、Rails独学(Ruby Silver取得)
- 業務で携わっている製品がRails製
- 現在はプログラム以外の部分を担当(要件定義、テスト、保守)
利用したのはこのサイト
- Green
- Wantedly
最初は今世の中にどんな会社があるのか、どんなスキルが求められているのかを知るぐらいのつもりで始めたのですが会社側から「話を聞いてみたい」ってのが結構届くのです。
この辺りは別の機会でも話出来ればと思いますが、理由は簡単で「Ruby/Rails」です。
サイトを見てもらえれば分かりますが、多くの会社でRuby/Railsを使われていることが分かります。
なので、私も調子乗っていくつか話を聞かせていただいたのですが結果は惨敗でした。
理由は簡単で「実力不足」です。
パッと見は約4年Railsに携わっているように見えるので、声は凄くかかるのですが僕は本当に基礎的なことしかコードが書けません。
これは僕の肌感覚ですがポテンシャルだけで通用するのは25,6歳までです。 20代後半になったら焦った方が良いと思います。何も実績無くヤル気だけで通用する世界では無くなります。
独学
実力不足を痛感した僕は何か見せれるもの、自分に自信が少しでもつくものと思い「Railsチュートリアル」を勉強材料として学びました。
どんなことを勉強し何を学んだのか見えるように学んだことをアウトプットしようと思いブログにまとめることにしました。
その時のブログが以下です。
全部で14章あるのですが、1章1章をまとめるような形でまとめていきました。これらはその中の2つです。
正直結構難しいですが、何とかやり切りました。
20代後半はのんびりしてられないことを痛感しましたので、もっかいチャレンジしてみることにしました。
第二転職活動
今度は転職のプロ!
エージェントをこれでもかというくらい使って活動してやろうと思い、色々な所に応募してみました。
- アイムファクトリー株式会社
顧客常駐はもう嫌だ!社内SEへ転職するなら【社内SE転職ナビ】
- 株式会社Branding_Engineer
- ギークス株式会社
- 株式会社WORKPORT
IT・インターネット・ゲーム業界専門の転職コンシェルジュ【WORKPORT】
- 株式会社PE─BANK
- レバレジーズ株式会社
この中で私が特にオススメなので、「アイムファクトリー株式会社」さんと「 株式会社Branding_Engineer」さんです。
案件の数は勿論ですが、人生の悩み相談までも聞いてもらえるので自分の立ち位置や進む先がどうなのかという将来設計までもしてもらえます。 なので、転職やフリーランスになりたいけど自信が無いって人も是非一度相談してみるのをオススメします。
今後どうやって生きていけば良いのかの道筋をしっかり示してもらえますよ。
で、自分の活動はどうだったのかですが結論としては「実力不足」でした。
何がどうってのは改めてさせてもらえればですが、やはり「エンジニアとしての業務経験」ってのが凄く凄く大切です。
これは独学で幾ら勉強してもよっぽどの人じゃ無い限り難しいと思います。
会社の中に入ってコードを書くということは見た目通り作れるだけでは当然ダメでチームとしてコードを書くというスキルも求められます。 こういった背景もあるからこそ、最近話題のエンジニアスクールが重宝されているのかもしれません。
- インターノウス株式会社
- コードキャンプ株式会社
- 株式会社Dive into Code
こういった場所で学ぶことで一緒に学ぶ仲間も出来ますし、いつだって相談できる講師もいる。そして何より就職支援までしてくれるのですから人生やり直すには本当に最高な環境ですよね。
特にインターノウスさんは「完全無料」ですよ? 関東圏に住んでいるならば、学ばない理由が無いです。
私も聞いてみたいことがあり問い合わせをしてみたことがありますが、すごく親切に対応してもらえますし返信もめちゃくちゃ早いので一度相談してみると良いと思います。
開発部への移動
長くなりましたが、現在の僕は結果的に会社にだだをこねて「開発部」へ移動させてもらいました。
この1年で学んだことはただ一つ
- 業務を通して開発の経験を積むこと
これに尽きます。
なので結果的に開発部に戻ることが出来たので、良かった。というのが僕の2017年でした。
2018年の目標
エンジニア=プログラマーになる
言葉の通りですが、先ずは「仕様書通りにコードを書く」ということを目標にやっていきたいと思います。 汚くても良いから、先ずは要求された通りに実装することがファーストステップだと思います。
前部署から持ち帰っている仕事もあり1日全てをコード書く時間に使えないので、いかに効率良く仕事を回しコードを書く時間を増やすかがポイントになりそうです。
副業で稼ぐ
やはり今の時代一つの柱で生きていくことは不可能だと思いますので、自分でサービスを作りたいなと思っています。 アイデアは幾つかあるので、途中で諦めずに公開するところまでやり切る。ことを目標に2018年はやっていけたらなと思っています。
これが出来れば、これに関することを何処かの勉強会で話出来ればと思いますので何かあれば宜しくお願い致します。
ということで来年はより一層技術的な部分で多くのアウトプットを残していき一回り成長できればと思います。
また、この発信が誰かの少しでも力になれればと思いますので今度とも宜しくお願い致します。
CentOS6で新しいRubyのバージョンが無い時どうすればいいの?
git
を使ってrbenv
をインストールしている。
Rubyのバージョンを見てみると2.3.3までしかインストールされてない状況。
$ cd ~/.rbenv $ git pull origin master $ rbenv install --list 2.3.3
git pull
すればいけるんじゃ無いかと思ったけど、ダメぽ。
で色々調べてみたらrbenv update
するプラグインがあるみたい。
$ mkdir -p "$(rbenv root)/plugins" # rbenv rootは僕の場合は「~/.rbenv」です。 $ git clone https://github.com/rkh/rbenv-update.git "$(rbenv root)/plugins/rbenv-update"
これでrbenv update
すればok
$ rbenv update $ rbenv install --list $ rbenv install 2.3.6
とここまで来て疑問。
ruby-build
がplugins
以下にあるのだけど、普通にgit pull
すりゃー同じことだったような気がしてる。
ソース読んでみたけど、plugins/
以下をグルグル回してgit pull
してるっぽいからやってることは同じっぽい。
https://github.com/rkh/rbenv-update/
最後に
$ rbenv global 2.3.6 $ gem install bundler
を忘れずにと。
Ruby on Rails チュートリアルで30歳までに人生を変える(番外編:シェア=リツイート機能の拡張)
こんにちは。opiyoです。
今回は、番外編:シェア=リツイート機能の拡張をやっていきます。
マイクロポストにリツイートアイコンを表示して、「シェア=リツイート」できるようにします。
ではでは、早速行ってみましょう。
railsチュートリアルシェア=リツイート機能の拡張でやること
仕様
【できたこと】
【できてないこと】
対象画面とイメージ
railsチュートリアルシェア=リツイート機能の拡張の完成版
【view】
# app/views/microposts/_micropost.html.erb <li id="micropost-<%= micropost.id %>"> <% if micropost.retweet_user(current_user) %> <p><%= fa_icon("retweet green", text: "自分がリツイート") %></p> <% end %> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"> <%= micropost.content %> <%= image_tag micropost.picture.url if micropost.picture? %> </span> <span class="timestamp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. </span> <span class="action"> <span id="like"> <%= render "microposts/hart", micropost: micropost %> </span> <% if micropost.retweet_user(current_user) %> <%= link_to fa_icon("retweet green", text: micropost.retweets.count), retwwets_destroy_path(micropost_id: micropost.id) %> <% else %> <%= link_to fa_icon("retweet", text: micropost.retweets.count), retwwets_create_path(micropost_id: micropost.id) %> <% end %> <% if current_user?(micropost.user) %> <%= link_to fa_icon("trash"), micropost, method: :delete, data: {confirm: "You sure?"}, micropost_id: micropost.id %> <% end %> </span> </li>
【controller】
# app/controllers/retwwets_controller.rb class RetwwetsController < ApplicationController before_action :correct_user, only: [:edit, :update] def create micropost = Micropost.find(params[:micropost_id]) Retweet.create(user_id: current_user.id, micropost_id: micropost.id) # 対象のマイクロポストのupdate_atを現在時刻にする Micropost.find(micropost.id).update_attribute(:updated_at, Time.now) redirect_to root_path end def destroy micropost = Micropost.find(params[:micropost_id]) Retweet.find_by(user_id: current_user.id, micropost_id: micropost.id).destroy # 対象のマイクロポストのupdate_atを現在時刻にする Micropost.find(micropost.id).update_attribute(:updated_at, Time.now) redirect_to root_path end end
# app/controllers/users_controller.rb def show @user = User.find(params[:id]) @microposts = @user.myfeed.paginate(page: params[:page]) .where('content LIKE ?', "%#{params[:search]}%") .reorder(updated_at: :DESC) end
【model】
# app/models/micropost.rb has_many :retweets, dependent: :destroy # シェア=リツイートしたかしてないか確認する def retweet_user(user_id) self.retweets.find_by(user_id: user_id) end
# app/models/retweet.rb class Retweet < ApplicationRecord belongs_to :micropost, counter_cache: :retweets_count belongs_to :user end
# app/models/user.rb # マイクロポストを取得する # 自分と自分がフォローしているユーザー # 自分がリツイート # フォローしているユーザーがリツイート(未実装) def feed following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id" retweet_ids = "SELECT micropost_id FROM retweets WHERE user_id = :user_id" Micropost.where("id IN (#{retweet_ids}) OR user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id) end # 自分が投稿、リツイートしたマイクロポストを取得する def myfeed retweet_ids = "SELECT micropost_id FROM retweets WHERE user_id = :user_id" Micropost.where("id IN (#{retweet_ids}) OR user_id = :user_id", user_id: self.id).order(:updated_at) end
【その他】
# config/routes.rb get 'retwwets/create' get 'retwwets/destroy'
# db/migrate/20170912225854_create_retweets.rb class CreateRetweets < ActiveRecord::Migration[5.0] def change create_table :retweets do |t| t.integer :user_id t.integer :micropost_id t.timestamps end end end
# db/migrate/20170919223052_add_retweets_count_to_microposts.rb class AddRetweetsCountToMicroposts < ActiveRecord::Migration[5.0] def change add_column :microposts, :retweets_count, :integer end end
railsチュートリアルシェア=リツイート機能の拡張で学んだこと
orderを指定してもソート順が変化しない?
自分のタイムラインに表示するポストを更新日順の降順=新しい投稿を上にするをしたくて、最初はこう書いていた。
def show @user = User.find(params[:id]) @microposts = @user.myfeed.paginate(page: params[:page]) .where('content LIKE ?', "%#{params[:search]}%") .order(updated_at: :DESC) end
だけど、なんだか上手くいかないなーと思っていたら犯人を見つけた!
# app/models/micropost.rb default_scope -> { order(created_at: :desc) }
これが定義されていることで、micropost
へのデータ取得はこれが絶対に設定されてしまう。
だから違う設定を反映したい場合は再定義してやる必要があるのだが、解決方法はすごーくシンプル。order
→ reorder
にすればOK
def show @user = User.find(params[:id]) @microposts = @user.myfeed.paginate(page: params[:page]) .where('content LIKE ?', "%#{params[:search]}%") .reorder(updated_at: :DESC) end
リツイートしたポストを表示する
やり方あってるか分からないけど、リツイートしたポストはリツイートテーブルに登録されるのでログインユーザーで引っ掛けて取得。
それをin句を使って取得って感じでやった。
def myfeed retweet_ids = "SELECT micropost_id FROM retweets WHERE user_id = :user_id" Micropost.where("id IN (#{retweet_ids}) OR user_id = :user_id", user_id: self.id).order(:updated_at) end
最初where句の書き方を反対に書いていたのだけど、それだと上手くいかなかったんだが良く分かってない。
これは引き続き調べよう。
トップページのタイムラインにフォローしているユーザーがリツイートしたポストを表示する
やり方としてはこんな感じなのかなぁ
- ログインユーザーがフォローしているユーザーを取得する
- ↑を使ってリツイートテーブルからマイクロポストのIDを取得する
- ↑を使ってマイクロポストテーブルからデータを取得する
自分とフォローしているユーザー、自分とフォローしているユーザーがリツイートしているデータをin句で書けば良いのかなと思ったのだが上手くいかないぁ
- 一応解決したので追記 -
それぞれSQL走ってしまうから、良くないことは分かっているのだが一様解決したのでメモです。
def feed Micropost.where(id: (self.microposts.pluck(:id) + Retweet.where(user_id: self.id).pluck(:micropost_id) + Micropost.where(user_id: self.followers.pluck(:id)).pluck(:id) + Retweet.where(user_id: self.followers.pluck(:id)).pluck(:id) ) ) end
やりたいことを一個づつSQLで取得して、最後にmidropost
のid
に配列で渡して取得する。
railsチュートリアルシェア=リツイート機能の拡張のまとめ
リツイートアイコンの表示やリツイートした数の表示は、いいね機能とほとんど同じなので比較的素直に実装できた。
が、やはりSQL力が全然無い。
複数テーブルを参照しながらデータを取得しないといけないとなると途端に分からなくなる。
以前お世話になった現場の先輩が「プログラムの仕事は7割くらいSQL力」のような事を言っていた事を思い出した。
少し大げさな気がするが、SQL力が大事なことは改めて実感した。
プログラマーは本当に次から次へと覚えることが多いから大変だけど、一つ一つしっかり学んで「こういう時はこーする」みたいな感覚がしっかり掴むことが大事なのかなとも思う。
だからとにかく手を止めずに頑張ろう。
Ruby on Rails チュートリアルで30歳までに人生を変える(番外編:いいね機能の拡張)
こんにちは。opiyoです。
今回は、番外編:いいね機能の拡張をやっていきます。
マイクロポストにハートアイコンを表示して、「いいね」できるようにします。
ではでは、早速行ってみましょう。
railsチュートリアルのいいね機能の拡張でやること
仕様
「いいね」機能を実装するに当たって、ざっくりだけど仕様を明確にしてみようと思う。
- 一つ一つのマイクロポストに「いいね」することができる
- 「いいね」は一つのマイクロポストに対して一人一回まで
- 「いいね」表示場所は、投稿日下にアイコンを使って表示する(いいね:ハート、削除:ゴミ箱)
- 「いいね」されたら赤いハート、取り消されたら白いハートにする
- 「いいね」された数を表示する(実装中)
- 「いいね」ボタンはajaxで処理する(困ってる)
こんな感じだろうか。一人一回までって制御が出来れば色々サンプルはありそうだし出来そうかな?
対象画面とイメージ
対象になる画面は2つ
【トップページ:app/views/static_pages/home.html.erb】
【ユーザー詳細ページ:app/views/users/show.html.erb】
railsチュートリアルのいいね機能の拡張の完成版
全部乗っけると数が多くなるので、主要な部分を載せます。
【view】
# app/views/likes/create.js.erb $(".micropost<%= @micropost.id %> i").addClass("fa-heart"); $(".micropost<%= @micropost.id %> i").removeClass("fa-heart-o"); // $(".micropost<%= @micropost.id %>").text("<%= @micropost.likes_count %>"); # これやるとアイコンが消えちゃう
# app/views/likes/destroy.js.erb $(".micropost<%= @micropost.id %> i").addClass("fa-heart-o"); $(".micropost<%= @micropost.id %> i").removeClass("fa-heart"); // $(".micropost<%= @micropost.id %>").text("<%= @micropost.likes_count %>"); # これやるとアイコンが消えちゃう
# app/views/microposts/_micropost.html.erb <li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"> <%= micropost.content %> <%= image_tag micropost.picture.url if micropost.picture? %> </span> <span class="timestamp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. </span> <span class="action"> <% if micropost.like_user(current_user.id) %> # ----- ここから ----- <%= link_to fa_icon("heart", text: micropost.likes_count), likes_destroy_path(micropost_id: micropost.id), remote: true, class: "micropost#{micropost.id}" %> <% else %> <%= link_to fa_icon("heart-o", text: micropost.likes_count), likes_create_path(micropost_id: micropost.id), remote: true, class: "micropost#{micropost.id}" %> <% end %> <% if current_user?(micropost.user) %> <%= link_to fa_icon("trash"), micropost, method: :delete, data: {confirm: "You sure?"}, micropost_id: micropost.id %> <% end %> # ----- ここまで ----- </span> </li>
【controller】
# app/controllers/likes_controller.rb class LikesController < ApplicationController def create micropost = Micropost.find(params[:micropost_id]) Like.create(user_id: current_user.id, micropost_id: micropost.id) @micropost = Micropost.find(params[:micropost_id]) respond_to do |format| format.html { redirect_to root_path} format.js end end def destroy micropost = Micropost.find(params[:micropost_id]) Like.find_by(user_id: current_user.id, micropost_id: micropost.id).destroy @micropost = Micropost.find(params[:micropost_id]) respond_to do |format| format.html { redirect_to root_path} format.js end end end
【model】
# app/models/like.rb class Like < ApplicationRecord belongs_to :micropost, counter_cache: :likes_count belongs_to :user end
# app/models/micropost.rb class Micropost < ApplicationRecord has_many :likes, dependent: :destroy end
【routes】
# config/routes.rb get 'likes/create' get 'likes/destroy'
【migration】
class CreateLikes < ActiveRecord::Migration[5.0] def change create_table :likes do |t| t.integer :user_id t.integer :micropost_id t.timestamps end end end
class AddIndexToLikesUserIdAndMicropostId < ActiveRecord::Migration[5.0] def change add_index :likes, [:user_id, :micropost_id], unique: true end end
class AddlikesCountToMicroposts < ActiveRecord::Migration[5.0] def change add_column :microposts, :likes_count, :integer end end
githubにpushしています。
色々と試行錯誤している跡がコミット追うと分かりますね。
railsチュートリアルのいいね機能の拡張で学んだこと
いいねの数が変わらない
ハートのアイコンを押したらいいねの数がカウントアップするって処理をしたいのだけど、数が変わらない。
DB見ると間違いなく登録されている。何が問題なのかなーと思っていたら...馬鹿やろうだった。
# app/controllers/likes_controller.rb def create micropost = Micropost.find(params[:micropost_id]) Like.create(user_id: micropost.user_id, micropost_id: micropost.id) @micropost = Micropost.find(params[:micropost_id]) # これしてなかった respond_to do |format| format.html { redirect_to root_path} format.js end end
create
した後にデータを再取得する処理をしてなかったので、view
に返しているデータはcreate
する前のオブジェクト。
そりゃー変わらないわ
railsチュートリアルのいいね機能の拡張で分からないこと
いいねボタンの連続で押せない
今の段階だとajaxでアイコンといいねの数だけ切り替えてしまっている。
これだと切り替えたあと、link_to
のパスとかが変わらないからおかしな状況になる。
だからきっと、その部分だけを別ファイルにしてrender hoge
みたいにしてやればいいのかなって思ってる。
いいねの数を書き換えるとアイコンが消える!
やりたいことは、いいねの数を更新させたいだけなのだけど、単純に.text("hoge")
ってやると子階層にあるタグが消えちゃう。
<div id="parent"> 親要素 <p>子要素</p> </div> <button id="button">変更する</button> $("#button").on("click", function(){ $("#parent").text("変更後の文章"); }); <div id="parent"> 変更後の文章 </div>
こんな状況の時、pタグは消えます!
これはどうすればいいじゃろうか。多分根本的には上で書いたようにview
をrender
するってのが正しいアプローチなんだとは思う。
railsチュートリアルのいいね機能の拡張のまとめ
まだ完成してないのだが、どうにかこうにか形になってきたので一先ずまとめてみました。
実装時間が長くなってしまったので当時ハマったことが少し思い出せない所もあり、もったい無いなと感じております。
解決できた時は嬉しくて先へ先へ進んじゃいがちだけど、一度立ち止まって何がダメだったのかをしっかり考える→まとめてみるってのは今後やっていこうと思いました。きっとそこが一番成長できる所だと思うので次回出会った時に「あーこの前のあれねって」思えるように。
まだまだ解決できてない部分もあるし、Likeモデルのcounter_cache
の機能もいまいち良く分かって無いので引き続き頑張ろうと思います。
railsチュートリアルのいいね機能の拡張の追記
未解決問題のajaxでの処理を追記
上で書いた2つの問題点を解決することに成功しましたので、追記でまとめます!
問題点
上に書いてある通りですが、大きく二つ
- クラスの付けかけだけしてるので、link_toのパスが変わらない
- いいねの数が消える
これに対するアプローチは、やはり
更新したい場所のみを再度renderする
うん。アプローチはやはり間違ってなかった!
解決したソース
とりあえずソースを貼ります!
【「いいね」前】
【「いいね」後】
# app/views/microposts/_micropost.html.erb <span class="action"> <span id="like"> # ajaxで処理する時に「どこを差し替える」が必要なので、それを追加 <%= render "microposts/hart", micropost: micropost %> # この部分を`_hart`としてテンプレート化する! </span> <% if current_user?(micropost.user) %> <%= link_to fa_icon("trash"), micropost, method: :delete, data: {confirm: "You sure?"}, micropost_id: micropost.id %> <% end %> </span>
# app/views/microposts/_hart.html.erb <% if micropost.like_user(current_user.id) %> <%= link_to fa_icon("heart", text: micropost.likes_count), likes_destroy_path(micropost_id: micropost.id), remote: true, class: "micropost#{micropost.id}" %> <% else %> <%= link_to fa_icon("heart-o", text: micropost.likes_count), likes_create_path(micropost_id: micropost.id), remote: true, class: "micropost#{micropost.id}" %> <% end %>
# app/views/likes/create.js.erb $("#like").html("<%= escape_javascript(render 'microposts/hart', micropost: @micropost) %>"); console.log("ajaxで処理")
# app/views/likes/destroy.js.erb $("#like").html("<%= escape_javascript(render 'microposts/hart', micropost: @micropost) %>"); console.log("ajaxで処理")
解決のポイント
アプローチの仕方は間違ってなかった。ようはhart
の部分だけ再度描画させたかったので、その部分を外出ししてそこだけ描画させる。
描画のやり方がjsのファイルで書いてあるやつだ。
like
クラスを持ったspan
タグの中身をhtml
メソッドで全て置き換えているイメージ。
<%= escape_javascript(render 'microposts/hart', micropost: @micropost) %>
の部分は極端に言うと展開されたhtmlになるようなイメージ。
だからこんな感じなのかな。
$("#like").html(<a class="micropost301" data-remote="true" href="/likes/create?micropost_id=301"> <i class="fa fa-heart-o"></i> 0 </a>)
やっていく中でハマったポイントはmicropost
インスタンスの渡し方。
最初jsのファイルにはmicropost: @micropost
を書いて無くてずっとエラーになっていた。
Completed 500 Internal Server Error in 181ms (ActiveRecord: 2.7ms) ActionView::Template::Error (undefined local variable or method `micropost' for #<#<Class:0x007fe0a1a1a368>:0x007fe0a6dc1110> Did you mean? @micropost):
micropost
がねーよってことですね。だからコントローラーで取得したインスタンスを渡してあげて解決!
railsチュートリアルのいいね機能の拡張の再まとめ
ajaxの処理にめちゃくちゃハマって、すげー時間を使ってしまった。
最終的には教えてもらう形で解決出来たのだが、本当に感謝感謝感謝ですね。
よく未経験エンジニアからエンジニアへっていう記事がありますが、そこに絶対書かれているのが
メンターを見るけること
これは絶対そー思います。僕が3日くらい悩んでいたものが30分で解決出来てしまうのですから間違い無いです。
いつでも相談出来るちゃうものや無料でプログラミグを教えてくれて転職支援までしてくれるサービスなど本当にいっぱいあるので一度覗いてみてください。
ちなみに、今回ajax処理を頑張って実装しましたが、別に無くても良いんじゃ無い?という話もありました。
普通にhtmlで返しても全く動きは同じだし、このぐらいのレベルならば正直あまりメリットは無い。だからスピードが遅くてダメだーってなってから対策するってアプローチの方で問題無いって。
一つのことにこだわることも大事だけど、別の事もやりたいとか全体が終わってないとかなるぐらいならば思い切って後回しってのは良いアプローチなのかもしれないですね。
ってな感じでした。
Ruby on Rails チュートリアルで30歳までに人生を変える(番外編:検索機能の拡張)
こんにちは。opiyoです。
今回は、番外編:検索機能の拡張をやっていきます。
ユーザーとマイクロポストをあいまい検索できるような機能を各画面に追加します。
ではでは、早速行ってみましょう。
railsチュートリアル検索機能の拡張で学んだこと
ユーザー一覧に名前をあいまい検索できる
# app/views/users/index.html.erb <% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <%= form_tag users_path, method: :get do %> <p> <%= text_field_tag :search %> <%= submit_tag "検索" %> </p> <% end %> <ul class="users"> <%= render @users %> </ul> <%= will_paginate %>
# app/controllers/users_controller.rb def index @user_form = User.new @users = User.paginate(page: params[:page]).search(params[:search]) end
# app/models/user.rb def self.search(search) if search where('name LIKE ?', "%#{sanitize_sql_like(search)}%") # Likeインジェクション対策をしてみた else all end end
ユーザー詳細のマイクロポストのコンテンツをあいまい検索できる
【検索前】
【検索後】
# app/views/users/show.html.erb <% provide(:title, @user.name) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1> </section> <section class="stats"> <%= render 'shared/stats' %> </section> </aside> <div class="col-md-8"> <%= render 'follow_form' if logged_in? %> <% if @microposts.any? %> <h3>MicroPosts (<%= @microposts.count %>)</h3> # 検索結果のマイクロポストの数を表示させる <%= form_tag user_path, method: :get do %> # ----- ここから追加した検索フォーム ----- <p> <%= text_field_tag :search %> <%= submit_tag "検索" %> </p> <% end %> # ----- ここまで ----- <ol class="microposts"> <%= render @microposts %> </ol> <%= will_paginate @microposts %> <% end %> </div> </div>
# app/controllers/users_controller.rb def show @user = User.find(params[:id]) @microposts = @user.microposts.paginate(page: params[:page]).where('content LIKE ?', "%#{params[:search]}%") # 何も入力せず検索しても「""」で渡ってくるので、エラーにはならない # @microposts = @user.microposts.paginate(page: params[:page]).search(params[:search]) # こう書いてモデル側でSQL組み立てたいのだが... end
トップページのマイクロポストのコンテンツをあいまい検索できる
# app/views/static_pages/home.html.erb <% if logged_in? %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= render 'shared/user_info' %> </section> <section class="stats"> <%= render 'shared/stats' %> </section> <section class="micropost_form"> <%= render 'shared/micropost_form' %> </section> </aside> <div class="col-md-8"> <h3>Micropost Feed</h3> <%= form_tag root_path, method: :get do %> # ----- ここからformの追加 ----- <p> <%= text_field_tag :search %> <%= submit_tag "検索" %> </p> <% end %> # ----- ここまで ----- <%= render 'shared/feed' %> </div> </div> <% else %> <% provide(:title, "Home") %> <div class="center jumbotron"> <h1>Welcome to the Sample App</h1> <h2> This is the home page for the <a href="http://railstutorial.jp/">Ruby on Rails Tutorial</a> sample application. </h2> <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %> </div> <%= link_to image_tag("rails.png", alt: "Rails logo"), "https://rubyonrails.org/" %> <% end %>
# app/controllers/static_pages_controller.rb def home if logged_in? @micropost = current_user.microposts.build if logged_in? @feed_items = current_user.feed.paginate(page: params[:page]).where('content LIKE ?', "%#{params[:search]}%") # 何も入力せず検索しても「""」で渡ってくるので、エラーにはならない end end
railsチュートリアル検索機能の拡張で学んだこと
form_forは難しい
# index.html.erb <%= form_for @user_form, url: users_path, method: :get do |f| %> <%= f.text_field :name %> <%= f.submit "Serach", class: "btn btn-primary" %> <% end %>
オブジェクトを指定するときはコントローラー側で必ずnew
すべし。検索結果があるから大丈夫って思ってたんだけど何故かエラーになる。
だからとりあえず、form_for
で使うオブジェクトは検索結果を格納したオブジェクトとは別にnew
したオブジェクトを作って指定した。
あとは、f.text_field :name
で渡すシンボルの部分がform_for
で使ったモデルにあるカラム名と同じじゃないとエラーになる。
paginateの後にwhereしてもいける!
@user.microposts.where("content LIKE ?", "%#{params[:search]}%").paginate(page: params[:page]) @user.microposts.paginate(page: params[:page]).where("content LIKE ?", "%#{params[:search]}%") # whereを後ろに持ってこれる。
これ見てもらった方が分かりやすいと思うのだけど、検索結果にはpaginate
メソッドを使ってページネーションができるようにしてます。
だから、paginate
メソッドの前にしかwhere
できないと思ってたのだけどそんなことなかった。
あいまい検索
where("検索したい項目 LIKE ?", "%#{sanitize_sql_like(hogehoge)}%")
基本的にはwhere
の第一引数に文字列を渡して、第二引数に?
に入る値を渡してやればOK
SQL文の組み立てはモデルで
コントローラーでやる場合
# users_controller.rb def index @user_form = User.new @users = User.where("name like ?", "%#{params[:user][:name]}%").paginate(page: params[:page]) # かっこ悪い。 end
モデルでやる場合
# users_controller.rb def index @user_form = User.new @users = User.paginate(page: params[:page]).search(params[:search]) # Userモデルの`search`メソッドを読んでUserモデル側で処理する end
# user.rb def self.search(search) if search where('name LIKE ?', "%#{search}%") else all end end
最初コントローラーでやってたんだけど、Railsチュートリアルにある例を見るとSQL文はモデルで組み立てる。
ユーザーモデルの方は何も問題なかったのだが、マイクロポストモデルの方では戻り値がマイクロポストじゃないから上手くモデル側で処理できなかった。 こういう場合は
# ユーザーの検索処理の場合 (byebug) User.paginate(page: params[:page]).class User::ActiveRecord_Relation (byebug) User.paginate(page: params[:page]).search("User") # Userモデルに定義した`search`メソッドが呼べる。 User Load (0.7ms) SELECT "users".* FROM "users" WHERE (name LIKE '%User%') LIMIT ? OFFSET ? [["LIMIT", 30], ["OFFSET", 0]] #<ActiveRecord::Relation [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-08-31 03:24:43", updated_at: "2017-08-31 04:05:36", password_digest: "$2a$10$c6SeUXq5hRG9FLJqNIM3EOUBiRuv/CXa3C3u6vEgRky...", remember_digest: "$2a$10$FPWHJckDrGrMyR7wTT.RjeybzbZjmR2uHBVtPPoYwGm...", admin: true, activation_digest: "$2a$10$Ww6ud8vjul5IqocU7vvQTecZYH4dqaBS0MuPAvQNDcQ...", activated: true, activated_at: "2017-08-31 03:24:43", reset_digest: nil, reset_sent_at: nil>]> # マイクロポストの検索処理の場合 (byebug) @user.microposts.paginate(page: params[:page]).search(params[:search]) # そんなメソッドねーよってなっちゃう *** NoMethodError Exception: undefined method `search' for #<Micropost::ActiveRecord_AssociationRelation:0x007fb506959698> Did you mean? each (byebug) @user.microposts.paginate(page: params[:page]).class Micropost::ActiveRecord_AssociationRelation
うーん。普通にMicropostモデルのsearch
メソッドが呼べそうだけど、何がダメなんだろうか。
railsチュートリアル検索機能の拡張のまとめ
当たり前だけど新しい機能を実装していくと全て考えて試行錯誤しながらやらないといけない。
Railsチュートリアルをやっていると訳分からずでも同じようにコピーすれば、とりあえず動く。だけどそうはいかない。エラーしか出てこない。
だけどこうやって頭使ってやっていくことが一番成長するのだろうと思う。
まだまだ全然思い通りにならない。今回の場合はお手本もあり何とか動くところまで持っていけた。
だけどきっと少しずつ出来るようにはなっているはずだから、手を止めずに前向いて頑張っていこう!
railsチュートリアル検索機能の拡張の追記
Nfm4yxnW8さんご指摘いただきました。
入門者に無粋な指摘で申し訳ないけどLIKE injectionを実行できちゃうよ。ここでは大きな問題にならないけど、他の場所では致命傷になるかも。 http://euglena1215.hatenablog.jp/entry/2016/09/22/171850 https://githubengineering.com/like-injection/
僕が書いたコードだと、「Likeインジェクション」っていう脆弱性がある。ユーザーが入力された文字をそのままSQLの条件にしてしまっているからだ。
教えていただいたサイトと同じようにやってみる
# params[:search]に「');--」が入ってくると... (byebug) params[:search] "');--" (byebug) @user.microposts.paginate(page: params[:page]).where('content LIKE ?', "%#{params[:search]}%").count (0.2ms) SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ? AND (content LIKE '%'');--%') [["user_id", 1]] 0
# params[:search]に「’ AND password LIKE ‘password’);—」が入ってくると... (byebug) params[:search] "’ AND password LIKE ‘password’);—" (byebug) @user.microposts.paginate(page: params[:page]).where('content LIKE ?', "%#{params[:search]}%").count (0.3ms) SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ? AND (content LIKE '%’ AND password LIKE ‘password’);—%') [["user_id", 1]] 0
ごめんなさい。SQL力が低すぎてどうやったらダメなのか分からなかったから、例題通りやってみたのだけど上手くいかなかった。改めて勉強します。
とはいえ、何か上手くやれば攻撃されちゃうってことなので、こういった記号はエスケープされるように処理する。
# あー多分コントローラーじゃ使えないんだなー後で直す @user.microposts.paginate(page: params[:page]).where('content LIKE ?', "%#{sanitize_sql_like(params[:search])}%") NoMethodError (undefined method `sanitize_sql_like' for #<UsersController:0x007ffcddb73ce8>):
# userモデル def self.search(search) if search where('name LIKE ?', "%#{sanitize_sql_like(search)}%") else all end end # コンソール (byebug) params[:search] "');--" (byebug) User.paginate(page: params[:page]).search(params[:search]) User Load (0.3ms) SELECT "users".* FROM "users" WHERE (name LIKE '%'');--%') LIMIT ? OFFSET ? [["LIMIT", 30], ["OFFSET", 0]] #<ActiveRecord::Relation []>
うーん。エラーにはならなかったけどエスケープされているようには見えない。 うーん。よくわからないな。
とりあえずタイムアップ。また調べてみよう。